1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 6var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); 7 8var { CalDavLegacySAXRequest } = ChromeUtils.import("resource:///modules/caldav/CalDavRequest.jsm"); 9 10/* exported CalDavEtagsHandler, CalDavWebDavSyncHandler, CalDavMultigetSyncHandler */ 11 12const EXPORTED_SYMBOLS = [ 13 "CalDavEtagsHandler", 14 "CalDavWebDavSyncHandler", 15 "CalDavMultigetSyncHandler", 16]; 17 18const XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n'; 19const MIME_TEXT_XML = "text/xml; charset=utf-8"; 20 21/** 22 * Accumulate all XML response, then parse with DOMParser. This class imitates 23 * nsISAXXMLReader by calling startDocument/endDocument and startElement/endElement. 24 */ 25class XMLResponseHandler { 26 constructor() { 27 this._inStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( 28 Ci.nsIScriptableInputStream 29 ); 30 this._xmlString = ""; 31 } 32 33 /** 34 * @see nsIStreamListener 35 */ 36 onDataAvailable(request, inputStream, offset, count) { 37 this._inStream.init(inputStream); 38 // What we get from inputStream is BinaryString, decode it to UTF-8. 39 this._xmlString += new TextDecoder("UTF-8").decode( 40 this._binaryStringToTypedArray(this._inStream.read(count)) 41 ); 42 } 43 44 /** 45 * Parse this._xmlString with DOMParser, then create a TreeWalker and start 46 * walking the node tree. 47 */ 48 async handleResponse() { 49 let parser = new DOMParser(); 50 let doc; 51 try { 52 doc = parser.parseFromString(this._xmlString, "application/xml"); 53 } catch (e) { 54 cal.ERROR("CALDAV: DOMParser parse error: ", e); 55 this.fatalError(); 56 } 57 58 let treeWalker = doc.createTreeWalker(doc.documentElement, NodeFilter.SHOW_ELEMENT); 59 this.startDocument(); 60 await this._walk(treeWalker); 61 await this.endDocument(); 62 } 63 64 /** 65 * Reset this._xmlString. 66 */ 67 resetXMLResponseHandler() { 68 this._xmlString = ""; 69 } 70 71 /** 72 * Converts a binary string into a Uint8Array. 73 * @param {BinaryString} str - The string to convert. 74 * @returns {Uint8Array}. 75 */ 76 _binaryStringToTypedArray(str) { 77 let arr = new Uint8Array(str.length); 78 for (let i = 0; i < str.length; i++) { 79 arr[i] = str.charCodeAt(i); 80 } 81 return arr; 82 } 83 84 /** 85 * Walk the tree node by node, call startElement and endElement when appropriate. 86 */ 87 async _walk(treeWalker) { 88 let currentNode = treeWalker.currentNode; 89 if (currentNode) { 90 this.startElement("", currentNode.localName, currentNode.nodeName, ""); 91 92 // Traverse children first. 93 let firstChild = treeWalker.firstChild(); 94 if (firstChild) { 95 await this._walk(treeWalker); 96 // TreeWalker has reached a leaf node, reset the cursor to continue the traversal. 97 treeWalker.currentNode = firstChild; 98 } else { 99 this.characters(currentNode.textContent); 100 await this.endElement("", currentNode.localName, currentNode.nodeName); 101 return; 102 } 103 104 // Traverse siblings next. 105 let nextSibling = treeWalker.nextSibling(); 106 while (nextSibling) { 107 await this._walk(treeWalker); 108 // TreeWalker has reached a leaf node, reset the cursor to continue the traversal. 109 treeWalker.currentNode = nextSibling; 110 nextSibling = treeWalker.nextSibling(); 111 } 112 113 await this.endElement("", currentNode.localName, currentNode.nodeName); 114 } 115 } 116} 117 118/** 119 * This is a handler for the etag request in calDavCalendar.js' getUpdatedItem. 120 * It uses XMLResponseHandler to parse the items and compose the resulting 121 * multiget. 122 */ 123class CalDavEtagsHandler extends XMLResponseHandler { 124 /** 125 * @param {calDavCalendar} aCalendar - The (unwrapped) calendar this request belongs to. 126 * @param {nsIURI} aBaseUri - The URI requested (i.e inbox or collection). 127 * @param {*=} aChangeLogListener - (optional) for cached calendars, the listener to notify. 128 */ 129 constructor(aCalendar, aBaseUri, aChangeLogListener) { 130 super(); 131 this.calendar = aCalendar; 132 this.baseUri = aBaseUri; 133 this.changeLogListener = aChangeLogListener; 134 135 this.itemsReported = {}; 136 this.itemsNeedFetching = []; 137 } 138 139 skipIndex = -1; 140 currentResponse = null; 141 tag = null; 142 calendar = null; 143 baseUri = null; 144 changeLogListener = null; 145 logXML = ""; 146 147 itemsReported = null; 148 itemsNeedFetching = null; 149 150 QueryInterface = ChromeUtils.generateQI(["nsIRequestObserver", "nsIStreamListener"]); 151 152 /** 153 * @see nsIRequestObserver 154 */ 155 onStartRequest(request) { 156 let httpchannel = request.QueryInterface(Ci.nsIHttpChannel); 157 158 let responseStatus; 159 try { 160 responseStatus = httpchannel.responseStatus; 161 } catch (ex) { 162 cal.WARN("CalDAV: No response status getting etags for calendar " + this.calendar.name); 163 } 164 165 if (responseStatus == 207) { 166 // We only need to parse 207's, anything else is probably a 167 // server error (i.e 50x). 168 httpchannel.contentType = "application/xml"; 169 } else { 170 cal.LOG("CalDAV: Error fetching item etags"); 171 this.calendar.reportDavError(Ci.calIErrors.DAV_REPORT_ERROR); 172 if (this.calendar.isCached && this.changeLogListener) { 173 this.changeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE); 174 } 175 } 176 } 177 178 async onStopRequest(request, statusCode) { 179 if (this.calendar.verboseLogging()) { 180 cal.LOG("CalDAV: recv: " + this.logXML); 181 } 182 await this.handleResponse(); 183 184 // Now that we are done, check which items need fetching. 185 this.calendar.superCalendar.startBatch(); 186 187 let needsRefresh = false; 188 try { 189 for (let path in this.calendar.mHrefIndex) { 190 if (path in this.itemsReported || path.substr(0, this.baseUri.length) == this.baseUri) { 191 // If the item is also on the server, check the next. 192 continue; 193 } 194 // If an item has been deleted from the server, delete it here too. 195 // Since the target calendar's operations are synchronous, we can 196 // safely set variables from this function. 197 let pcal = cal.async.promisifyCalendar(this.calendar.mOfflineStorage); 198 let foundItem = (await pcal.getItem(this.calendar.mHrefIndex[path]))[0]; 199 200 if (foundItem) { 201 let wasInboxItem = this.calendar.mItemInfoCache[foundItem.id].isInboxItem; 202 if ( 203 (wasInboxItem && this.calendar.isInbox(this.baseUri.spec)) || 204 (wasInboxItem === false && !this.calendar.isInbox(this.baseUri.spec)) 205 ) { 206 cal.LOG("Deleting local href: " + path); 207 delete this.calendar.mHrefIndex[path]; 208 await pcal.deleteItem(foundItem); 209 needsRefresh = true; 210 } 211 } 212 } 213 } finally { 214 this.calendar.superCalendar.endBatch(); 215 } 216 217 // Avoid sending empty multiget requests update views if something has 218 // been deleted server-side. 219 if (this.itemsNeedFetching.length) { 220 let multiget = new CalDavMultigetSyncHandler( 221 this.itemsNeedFetching, 222 this.calendar, 223 this.baseUri, 224 null, 225 false, 226 null, 227 this.changeLogListener 228 ); 229 multiget.doMultiGet(); 230 } else { 231 if (this.calendar.isCached && this.changeLogListener) { 232 this.changeLogListener.onResult({ status: Cr.NS_OK }, Cr.NS_OK); 233 } 234 235 if (needsRefresh) { 236 this.calendar.mObservers.notify("onLoad", [this.calendar]); 237 } 238 239 // but do poll the inbox 240 if (this.calendar.mShouldPollInbox && !this.calendar.isInbox(this.baseUri.spec)) { 241 this.calendar.pollInbox(); 242 } 243 } 244 } 245 246 /** 247 * @see XMLResponseHandler 248 */ 249 fatalError() { 250 cal.WARN("CalDAV: Fatal Error parsing etags for " + this.calendar.name); 251 } 252 253 /** 254 * @see XMLResponseHandler 255 */ 256 characters(aValue) { 257 if (this.calendar.verboseLogging()) { 258 this.logXML += aValue; 259 } 260 if (this.tag) { 261 this.currentResponse[this.tag] += aValue; 262 } 263 } 264 265 startDocument() { 266 this.hrefMap = {}; 267 this.currentResponse = {}; 268 this.tag = null; 269 } 270 271 endDocument() {} 272 273 startElement(aUri, aLocalName, aQName, aAttributes) { 274 switch (aLocalName) { 275 case "response": 276 this.currentResponse = {}; 277 this.currentResponse.isCollection = false; 278 this.tag = null; 279 break; 280 case "collection": 281 this.currentResponse.isCollection = true; 282 // falls through 283 case "href": 284 case "getetag": 285 case "getcontenttype": 286 this.tag = aLocalName; 287 this.currentResponse[aLocalName] = ""; 288 break; 289 } 290 if (this.calendar.verboseLogging()) { 291 this.logXML += "<" + aQName + ">"; 292 } 293 } 294 295 endElement(aUri, aLocalName, aQName) { 296 switch (aLocalName) { 297 case "response": { 298 this.tag = null; 299 let resp = this.currentResponse; 300 if ( 301 resp.getetag && 302 resp.getetag.length && 303 resp.href && 304 resp.href.length && 305 resp.getcontenttype && 306 resp.getcontenttype.length && 307 !resp.isCollection 308 ) { 309 resp.href = this.calendar.ensureDecodedPath(resp.href); 310 311 if (resp.getcontenttype.substr(0, 14) == "message/rfc822") { 312 // workaround for a Scalix bug which causes incorrect 313 // contenttype to be returned. 314 resp.getcontenttype = "text/calendar"; 315 } 316 if (resp.getcontenttype == "text/vtodo") { 317 // workaround Kerio weirdness 318 resp.getcontenttype = "text/calendar"; 319 } 320 321 // Only handle calendar items 322 if (resp.getcontenttype.substr(0, 13) == "text/calendar") { 323 if (resp.href && resp.href.length) { 324 this.itemsReported[resp.href] = resp.getetag; 325 326 let itemUid = this.calendar.mHrefIndex[resp.href]; 327 if (!itemUid || resp.getetag != this.calendar.mItemInfoCache[itemUid].etag) { 328 this.itemsNeedFetching.push(resp.href); 329 } 330 } 331 } 332 } 333 break; 334 } 335 case "href": 336 case "getetag": 337 case "getcontenttype": { 338 this.tag = null; 339 break; 340 } 341 } 342 if (this.calendar.verboseLogging()) { 343 this.logXML += "</" + aQName + ">"; 344 } 345 } 346 347 processingInstruction(aTarget, aData) {} 348} 349 350/** 351 * This is a handler for the webdav sync request in calDavCalendar.js' 352 * getUpdatedItem. It uses XMLResponseHandler to parse the items and compose the 353 * resulting multiget. 354 */ 355class CalDavWebDavSyncHandler extends XMLResponseHandler { 356 /** 357 * @param {calDavCalendar} aCalendar - The (unwrapped) calendar this request belongs to. 358 * @param {nsIURI} aBaseUri - The URI requested (i.e inbox or collection). 359 * @param {*=} aChangeLogListener - (optional) for cached calendars, the listener to notify. 360 */ 361 constructor(aCalendar, aBaseUri, aChangeLogListener) { 362 super(); 363 this.calendar = aCalendar; 364 this.baseUri = aBaseUri; 365 this.changeLogListener = aChangeLogListener; 366 367 this.itemsReported = {}; 368 this.itemsNeedFetching = []; 369 } 370 371 currentResponse = null; 372 tag = null; 373 calendar = null; 374 baseUri = null; 375 newSyncToken = null; 376 changeLogListener = null; 377 logXML = ""; 378 isInPropStat = false; 379 changeCount = 0; 380 unhandledErrors = 0; 381 itemsReported = null; 382 itemsNeedFetching = null; 383 additionalSyncNeeded = false; 384 385 QueryInterface = ChromeUtils.generateQI(["nsIRequestObserver", "nsIStreamListener"]); 386 387 async doWebDAVSync() { 388 if (this.calendar.mDisabledByDavError) { 389 // check if maybe our calendar has become available 390 this.calendar.checkDavResourceType(this.changeLogListener); 391 return; 392 } 393 394 let syncTokenString = "<sync-token/>"; 395 if (this.calendar.mWebdavSyncToken && this.calendar.mWebdavSyncToken.length > 0) { 396 let syncToken = cal.xml.escapeString(this.calendar.mWebdavSyncToken); 397 syncTokenString = "<sync-token>" + syncToken + "</sync-token>"; 398 } 399 400 let queryXml = 401 XML_HEADER + 402 '<sync-collection xmlns="DAV:">' + 403 syncTokenString + 404 "<sync-level>1</sync-level>" + 405 "<prop>" + 406 "<getcontenttype/>" + 407 "<getetag/>" + 408 "</prop>" + 409 "</sync-collection>"; 410 411 let requestUri = this.calendar.makeUri(null, this.baseUri); 412 413 if (this.calendar.verboseLogging()) { 414 cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml); 415 } 416 cal.LOG("CalDAV: webdav-sync Token: " + this.calendar.mWebdavSyncToken); 417 418 let onSetupChannel = channel => { 419 // The depth header adheres to an older version of the webdav-sync 420 // spec and has been replaced by the <sync-level> tag above. 421 // Unfortunately some servers still depend on the depth header, 422 // therefore we send both (yuck). 423 channel.setRequestHeader("Depth", "1", false); 424 channel.requestMethod = "REPORT"; 425 }; 426 let request = new CalDavLegacySAXRequest( 427 this.calendar.session, 428 this.calendar, 429 requestUri, 430 queryXml, 431 MIME_TEXT_XML, 432 this, 433 onSetupChannel 434 ); 435 436 await request.commit().catch(() => { 437 // Something went wrong with the OAuth token, notify failure 438 if (this.calendar.isCached && this.changeLogListener) { 439 this.changeLogListener.onResult( 440 { status: Cr.NS_ERROR_NOT_AVAILABLE }, 441 Cr.NS_ERROR_NOT_AVAILABLE 442 ); 443 } 444 }); 445 } 446 447 /** 448 * @see nsIRequestObserver 449 */ 450 onStartRequest(request) { 451 let httpchannel = request.QueryInterface(Ci.nsIHttpChannel); 452 453 let responseStatus; 454 try { 455 responseStatus = httpchannel.responseStatus; 456 } catch (ex) { 457 cal.WARN("CalDAV: No response status doing webdav sync for calendar " + this.calendar.name); 458 } 459 460 if (responseStatus == 207) { 461 // We only need to parse 207's, anything else is probably a 462 // server error (i.e 50x). 463 httpchannel.contentType = "application/xml"; 464 } else if ( 465 this.calendar.mWebdavSyncToken != null && 466 responseStatus >= 400 && 467 responseStatus <= 499 468 ) { 469 // Invalidate sync token with 4xx errors that could indicate the 470 // sync token has become invalid and do a refresh 471 cal.LOG( 472 "CalDAV: Resetting sync token because server returned status code: " + responseStatus 473 ); 474 this.calendar.mWebdavSyncToken = null; 475 this.calendar.saveCalendarProperties(); 476 this.calendar.safeRefresh(this.changeLogListener); 477 } else { 478 cal.WARN("CalDAV: Error doing webdav sync: " + responseStatus); 479 this.calendar.reportDavError(Ci.calIErrors.DAV_REPORT_ERROR); 480 if (this.calendar.isCached && this.changeLogListener) { 481 this.changeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE); 482 } 483 } 484 } 485 486 async onStopRequest(request, statusCode) { 487 if (this.calendar.verboseLogging()) { 488 cal.LOG("CalDAV: recv: " + this.logXML); 489 } 490 491 await this.handleResponse(); 492 } 493 494 /** 495 * @see XMLResponseHandler 496 */ 497 fatalError() { 498 cal.WARN("CalDAV: Fatal Error doing webdav sync for " + this.calendar.name); 499 } 500 501 /** 502 * @see XMLResponseHandler 503 */ 504 characters(aValue) { 505 if (this.calendar.verboseLogging()) { 506 this.logXML += aValue; 507 } 508 this.currentResponse[this.tag] += aValue; 509 } 510 511 startDocument() { 512 this.hrefMap = {}; 513 this.currentResponse = {}; 514 this.tag = null; 515 this.calendar.superCalendar.startBatch(); 516 } 517 518 async endDocument() { 519 if (this.unhandledErrors) { 520 this.calendar.superCalendar.endBatch(); 521 this.calendar.reportDavError(Ci.calIErrors.DAV_REPORT_ERROR); 522 if (this.calendar.isCached && this.changeLogListener) { 523 this.changeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE); 524 } 525 return; 526 } 527 528 if (this.calendar.mWebdavSyncToken == null) { 529 // null token means reset or first refresh indicating we did 530 // a full sync; remove local items that were not returned in this full 531 // sync 532 for (let path in this.calendar.mHrefIndex) { 533 if (!this.itemsReported[path]) { 534 await this.calendar.deleteTargetCalendarItem(path); 535 } 536 } 537 } 538 this.calendar.superCalendar.endBatch(); 539 540 if (this.itemsNeedFetching.length) { 541 let multiget = new CalDavMultigetSyncHandler( 542 this.itemsNeedFetching, 543 this.calendar, 544 this.baseUri, 545 this.newSyncToken, 546 this.additionalSyncNeeded, 547 null, 548 this.changeLogListener 549 ); 550 multiget.doMultiGet(); 551 } else { 552 if (this.newSyncToken) { 553 this.calendar.mWebdavSyncToken = this.newSyncToken; 554 this.calendar.saveCalendarProperties(); 555 cal.LOG("CalDAV: New webdav-sync Token: " + this.calendar.mWebdavSyncToken); 556 } 557 this.calendar.finalizeUpdatedItems(this.changeLogListener, this.baseUri); 558 } 559 } 560 561 startElement(aUri, aLocalName, aQName, aAttributes) { 562 switch (aLocalName) { 563 case "response": // WebDAV Sync draft 3 564 this.currentResponse = {}; 565 this.tag = null; 566 this.isInPropStat = false; 567 break; 568 case "propstat": 569 this.isInPropStat = true; 570 break; 571 case "status": 572 if (this.isInPropStat) { 573 this.tag = "propstat_" + aLocalName; 574 } else { 575 this.tag = aLocalName; 576 } 577 this.currentResponse[this.tag] = ""; 578 break; 579 case "href": 580 case "getetag": 581 case "getcontenttype": 582 case "sync-token": 583 this.tag = aLocalName.replace(/-/g, ""); 584 this.currentResponse[this.tag] = ""; 585 break; 586 } 587 if (this.calendar.verboseLogging()) { 588 this.logXML += "<" + aQName + ">"; 589 } 590 } 591 592 async endElement(aUri, aLocalName, aQName) { 593 switch (aLocalName) { 594 case "response": // WebDAV Sync draft 3 595 case "sync-response": { 596 // WebDAV Sync draft 0,1,2 597 let resp = this.currentResponse; 598 if (resp.href && resp.href.length) { 599 resp.href = this.calendar.ensureDecodedPath(resp.href); 600 } 601 602 if ( 603 (!resp.getcontenttype || resp.getcontenttype == "text/plain") && 604 resp.href && 605 resp.href.endsWith(".ics") 606 ) { 607 // If there is no content-type (iCloud) or text/plain was passed 608 // (iCal Server) for the resource but its name ends with ".ics" 609 // assume the content type to be text/calendar. Apple 610 // iCloud/iCal Server interoperability fix. 611 resp.getcontenttype = "text/calendar"; 612 } 613 614 // Deleted item 615 if ( 616 resp.href && 617 resp.href.length && 618 resp.status && 619 resp.status.length && 620 resp.status.indexOf(" 404") > 0 621 ) { 622 if (this.calendar.mHrefIndex[resp.href]) { 623 this.changeCount++; 624 await this.calendar.deleteTargetCalendarItem(resp.href); 625 } else { 626 cal.LOG("CalDAV: skipping unfound deleted item : " + resp.href); 627 } 628 // Only handle Created or Updated calendar items 629 } else if ( 630 resp.getcontenttype && 631 resp.getcontenttype.substr(0, 13) == "text/calendar" && 632 resp.getetag && 633 resp.getetag.length && 634 resp.href && 635 resp.href.length && 636 (!resp.status || // Draft 3 does not require 637 resp.status.length == 0 || // a status for created or updated items but 638 resp.status.indexOf(" 204") || // draft 0, 1 and 2 needed it so treat no status 639 resp.status.indexOf(" 200") || // Apple iCloud returns 200 status for each item 640 resp.status.indexOf(" 201")) 641 ) { 642 // and status 201 and 204 the same 643 this.itemsReported[resp.href] = resp.getetag; 644 let itemId = this.calendar.mHrefIndex[resp.href]; 645 let oldEtag = itemId && this.calendar.mItemInfoCache[itemId].etag; 646 647 if (!oldEtag || oldEtag != resp.getetag) { 648 // Etag mismatch, getting new/updated item. 649 this.itemsNeedFetching.push(resp.href); 650 } 651 } else if (resp.status && resp.status.includes(" 507")) { 652 // webdav-sync says that if a 507 is encountered and the 653 // url matches the request, the current token should be 654 // saved and another request should be made. We don't 655 // actually compare the URL, its too easy to get this 656 // wrong. 657 658 // The 507 doesn't mean the data received is invalid, so 659 // continue processing. 660 this.additionalSyncNeeded = true; 661 } else if ( 662 resp.status && 663 resp.status.indexOf(" 200") && 664 resp.href && 665 resp.href.endsWith("/") 666 ) { 667 // iCloud returns status responses for directories too 668 // so we just ignore them if they have status code 200. We 669 // want to make sure these are not counted as unhandled 670 // errors in the next block 671 } else if ( 672 (resp.getcontenttype && resp.getcontenttype.startsWith("text/calendar")) || 673 (resp.status && !resp.status.includes(" 404")) 674 ) { 675 // If the response element is still not handled, log an 676 // error only if the content-type is text/calendar or the 677 // response status is different than 404 not found. We 678 // don't care about response elements on non-calendar 679 // resources or whose status is not indicating a deleted 680 // resource. 681 cal.WARN("CalDAV: Unexpected response, status: " + resp.status + ", href: " + resp.href); 682 this.unhandledErrors++; 683 } else { 684 cal.LOG( 685 "CalDAV: Unhandled response element, status: " + 686 resp.status + 687 ", href: " + 688 resp.href + 689 " contenttype:" + 690 resp.getcontenttype 691 ); 692 } 693 break; 694 } 695 case "sync-token": { 696 this.newSyncToken = this.currentResponse[this.tag]; 697 break; 698 } 699 case "propstat": { 700 this.isInPropStat = false; 701 break; 702 } 703 } 704 this.tag = null; 705 if (this.calendar.verboseLogging()) { 706 this.logXML += "</" + aQName + ">"; 707 } 708 } 709 710 processingInstruction(aTarget, aData) {} 711} 712 713/** 714 * This is a handler for the multiget request. It uses XMLResponseHandler to 715 * parse the items and compose the resulting multiget. 716 */ 717class CalDavMultigetSyncHandler extends XMLResponseHandler { 718 /** 719 * @param {String[]} aItemsNeedFetching - Array of items to fetch, an array of 720 * un-encoded paths. 721 * @param {calDavCalendar} aCalendar - The (unwrapped) calendar this request belongs to. 722 * @param {nsIURI} aBaseUri - The URI requested (i.e inbox or collection). 723 * @param {*=} aNewSyncToken - (optional) New Sync token to set if operation successful. 724 * @param {Boolean=} aAdditionalSyncNeeded - (optional) If true, the passed sync token is not the 725 * latest, another webdav sync run should be 726 * done after completion. 727 * @param {*=} aListener - (optional) The listener to notify. 728 * @param {*=} aChangeLogListener - (optional) For cached calendars, the listener to 729 * notify. 730 */ 731 constructor( 732 aItemsNeedFetching, 733 aCalendar, 734 aBaseUri, 735 aNewSyncToken, 736 aAdditionalSyncNeeded, 737 aListener, 738 aChangeLogListener 739 ) { 740 super(); 741 this.calendar = aCalendar; 742 this.baseUri = aBaseUri; 743 this.listener = aListener; 744 this.newSyncToken = aNewSyncToken; 745 this.changeLogListener = aChangeLogListener; 746 this.itemsNeedFetching = aItemsNeedFetching; 747 this.additionalSyncNeeded = aAdditionalSyncNeeded; 748 } 749 750 currentResponse = null; 751 tag = null; 752 calendar = null; 753 baseUri = null; 754 newSyncToken = null; 755 listener = null; 756 changeLogListener = null; 757 logXML = null; 758 unhandledErrors = 0; 759 itemsNeedFetching = null; 760 additionalSyncNeeded = false; 761 timer = null; 762 763 QueryInterface = ChromeUtils.generateQI(["nsIRequestObserver", "nsIStreamListener"]); 764 765 doMultiGet() { 766 if (this.calendar.mDisabledByDavError) { 767 // check if maybe our calendar has become available 768 this.calendar.checkDavResourceType(this.changeLogListener); 769 return; 770 } 771 772 let batchSize = Services.prefs.getIntPref("calendar.caldav.multigetBatchSize", 100); 773 let hrefString = ""; 774 while (this.itemsNeedFetching.length && batchSize > 0) { 775 batchSize--; 776 // ensureEncodedPath extracts only the path component of the item and 777 // encodes it before it is sent to the server 778 let locpath = this.calendar.ensureEncodedPath(this.itemsNeedFetching.pop()); 779 hrefString += "<D:href>" + cal.xml.escapeString(locpath) + "</D:href>"; 780 } 781 782 let queryXml = 783 XML_HEADER + 784 '<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">' + 785 "<D:prop>" + 786 "<D:getetag/>" + 787 "<C:calendar-data/>" + 788 "</D:prop>" + 789 hrefString + 790 "</C:calendar-multiget>"; 791 792 let requestUri = this.calendar.makeUri(null, this.baseUri); 793 if (this.calendar.verboseLogging()) { 794 cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml); 795 } 796 797 let onSetupChannel = channel => { 798 channel.requestMethod = "REPORT"; 799 channel.setRequestHeader("Depth", "1", false); 800 }; 801 let request = new CalDavLegacySAXRequest( 802 this.calendar.session, 803 this.calendar, 804 requestUri, 805 queryXml, 806 MIME_TEXT_XML, 807 this, 808 onSetupChannel 809 ); 810 811 request.commit().catch(() => { 812 // Something went wrong with the OAuth token, notify failure 813 if (this.calendar.isCached && this.changeLogListener) { 814 this.changeLogListener.onResult( 815 { status: Cr.NS_ERROR_NOT_AVAILABLE }, 816 Cr.NS_ERROR_NOT_AVAILABLE 817 ); 818 } 819 }); 820 } 821 822 /** 823 * @see nsIRequestObserver 824 */ 825 onStartRequest(request) { 826 let httpchannel = request.QueryInterface(Ci.nsIHttpChannel); 827 828 let responseStatus; 829 try { 830 responseStatus = httpchannel.responseStatus; 831 } catch (ex) { 832 cal.WARN("CalDAV: No response status doing multiget for calendar " + this.calendar.name); 833 } 834 835 if (responseStatus == 207) { 836 // We only need to parse 207's, anything else is probably a 837 // server error (i.e 50x). 838 httpchannel.contentType = "application/xml"; 839 } else { 840 let errorMsg = 841 "CalDAV: Error: got status " + 842 responseStatus + 843 " fetching calendar data for " + 844 this.calendar.name + 845 ", " + 846 this.listener; 847 this.calendar.notifyGetFailed(errorMsg, this.listener, this.changeLogListener); 848 } 849 } 850 851 async onStopRequest(request, statusCode) { 852 if (this.calendar.verboseLogging()) { 853 cal.LOG("CalDAV: recv: " + this.logXML); 854 } 855 if (this.unhandledErrors) { 856 this.calendar.superCalendar.endBatch(); 857 this.calendar.notifyGetFailed("multiget error", this.listener, this.changeLogListener); 858 return; 859 } 860 if (this.itemsNeedFetching.length == 0) { 861 if (this.newSyncToken) { 862 this.calendar.mWebdavSyncToken = this.newSyncToken; 863 this.calendar.saveCalendarProperties(); 864 cal.LOG("CalDAV: New webdav-sync Token: " + this.calendar.mWebdavSyncToken); 865 } 866 } 867 await this.handleResponse(); 868 if (this.itemsNeedFetching.length > 0) { 869 cal.LOG("CalDAV: Still need to fetch " + this.itemsNeedFetching.length + " elements."); 870 this.resetXMLResponseHandler(); 871 let timerCallback = { 872 requestHandler: this, 873 notify(timer) { 874 // Call multiget again to get another batch 875 this.requestHandler.doMultiGet(); 876 }, 877 }; 878 this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 879 this.timer.initWithCallback(timerCallback, 0, Ci.nsITimer.TYPE_ONE_SHOT); 880 } else if (this.additionalSyncNeeded) { 881 let wds = new CalDavWebDavSyncHandler(this.calendar, this.baseUri, this.changeLogListener); 882 wds.doWebDAVSync(); 883 } else { 884 this.calendar.finalizeUpdatedItems(this.changeLogListener, this.baseUri); 885 } 886 } 887 888 /** 889 * @see XMLResponseHandler 890 */ 891 fatalError(error) { 892 cal.WARN("CalDAV: Fatal Error doing multiget for " + this.calendar.name + ": " + error); 893 } 894 895 /** 896 * @see XMLResponseHandler 897 */ 898 characters(aValue) { 899 if (this.calendar.verboseLogging()) { 900 this.logXML += aValue; 901 } 902 if (this.tag) { 903 this.currentResponse[this.tag] += aValue; 904 } 905 } 906 907 startDocument() { 908 this.hrefMap = {}; 909 this.currentResponse = {}; 910 this.tag = null; 911 this.logXML = ""; 912 this.calendar.superCalendar.startBatch(); 913 } 914 915 endDocument() { 916 this.calendar.superCalendar.endBatch(); 917 } 918 919 startElement(aUri, aLocalName, aQName, aAttributes) { 920 switch (aLocalName) { 921 case "response": 922 this.currentResponse = {}; 923 this.tag = null; 924 this.isInPropStat = false; 925 break; 926 case "propstat": 927 this.isInPropStat = true; 928 break; 929 case "status": 930 if (this.isInPropStat) { 931 this.tag = "propstat_" + aLocalName; 932 } else { 933 this.tag = aLocalName; 934 } 935 this.currentResponse[this.tag] = ""; 936 break; 937 case "calendar-data": 938 case "href": 939 case "getetag": 940 this.tag = aLocalName.replace(/-/g, ""); 941 this.currentResponse[this.tag] = ""; 942 break; 943 } 944 if (this.calendar.verboseLogging()) { 945 this.logXML += "<" + aQName + ">"; 946 } 947 } 948 949 async endElement(aUri, aLocalName, aQName) { 950 switch (aLocalName) { 951 case "response": { 952 let resp = this.currentResponse; 953 if (resp.href && resp.href.length) { 954 resp.href = this.calendar.ensureDecodedPath(resp.href); 955 } 956 if ( 957 resp.href && 958 resp.href.length && 959 resp.status && 960 resp.status.length && 961 resp.status.indexOf(" 404") > 0 962 ) { 963 if (this.calendar.mHrefIndex[resp.href]) { 964 await this.calendar.deleteTargetCalendarItem(resp.href); 965 } else { 966 cal.LOG("CalDAV: skipping unfound deleted item : " + resp.href); 967 } 968 // Created or Updated item 969 } else if ( 970 resp.getetag && 971 resp.getetag.length && 972 resp.href && 973 resp.href.length && 974 resp.calendardata && 975 resp.calendardata.length 976 ) { 977 let oldEtag; 978 let itemId = this.calendar.mHrefIndex[resp.href]; 979 if (itemId) { 980 oldEtag = this.calendar.mItemInfoCache[itemId].etag; 981 } else { 982 oldEtag = null; 983 } 984 if (!oldEtag || oldEtag != resp.getetag) { 985 await this.calendar.addTargetCalendarItem( 986 resp.href, 987 resp.calendardata, 988 this.baseUri, 989 resp.getetag, 990 this.listener 991 ); 992 } else { 993 cal.LOG("CalDAV: skipping item with unmodified etag : " + oldEtag); 994 } 995 } else { 996 cal.WARN( 997 "CalDAV: Unexpected response, status: " + 998 resp.status + 999 ", href: " + 1000 resp.href + 1001 " calendar-data:\n" + 1002 resp.calendardata 1003 ); 1004 this.unhandledErrors++; 1005 } 1006 break; 1007 } 1008 case "propstat": { 1009 this.isInPropStat = false; 1010 break; 1011 } 1012 } 1013 this.tag = null; 1014 if (this.calendar.verboseLogging()) { 1015 this.logXML += "</" + aQName + ">"; 1016 } 1017 } 1018 1019 processingInstruction(aTarget, aData) {} 1020} 1021