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