1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 *
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
8 *
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
12 * License.
13 *
14 * The Original Code is Oracle Corporation code.
15 *
16 * The Initial Developer of the Original Code is
17 * Oracle Corporation
18 * Portions created by the Initial Developer are Copyright (C) 2004
19 * the Initial Developer. All Rights Reserved.
20 *
21 * Contributor(s):
22 * Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
23 * Dan Mosedale <dan.mosedale@oracle.com>
24 * Mike Shaver <mike.x.shaver@oracle.com>
25 * Gary van der Merwe <garyvdm@gmail.com>
26 * Bruno Browning <browning@uwalumni.com>
27 * Matthew Willis <lilmatt@mozilla.com>
28 * Daniel Boelzle <daniel.boelzle@sun.com>
29 * Philipp Kewisch <mozilla@kewis.ch>
30 *
31 * Alternatively, the contents of this file may be used under the terms of
32 * either the GNU General Public License Version 2 or later (the "GPL"), or
33 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
34 * in which case the provisions of the GPL or the LGPL are applicable instead
35 * of those above. If you wish to allow use of your version of this file only
36 * under the terms of either the GPL or the LGPL, and not to allow others to
37 * use your version of this file under the terms of the MPL, indicate your
38 * decision by deleting the provisions above and replace them with the notice
39 * and other provisions required by the GPL or the LGPL. If you do not delete
40 * the provisions above, a recipient may use your version of this file under
41 * the terms of any one of the MPL, the GPL or the LGPL.
42 *
43 * ***** END LICENSE BLOCK ***** */
44
45 //
46 // calDavCalendar.js
47 //
48
49 // XXXdmose deal with generation goop
50
51 // XXXdmose deal with locking
52
53 // XXXdmose need to make and use better error reporting interface for webdav
54 // (all uses of aStatusCode, probably)
55
56 // XXXdmose use real calendar result codes, not NS_ERROR_FAILURE for everything
57
58 const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n';
59
60 function calDavCalendar() {
61 this.initProviderBase();
62 this.unmappedProperties = [];
63 this.mUriParams = null;
64 this.mItemInfoCache = {};
65 this.mDisabled = false;
66 this.mCalHomeSet = null;
67 this.mPrincipalsNS = null;
68 this.mInBoxUrl = null;
69 this.mOutBoxUrl = null;
70 this.mHaveScheduling = false;
71 this.mMailToUrl = null;
72 this.mHrefIndex = [];
73 this.mAuthScheme = null;
74 this.mAuthRealm = null;
75 this.mObserver = null;
76 }
77
78 // some shorthand
79 const nsIWebDAVOperationListener =
80 Components.interfaces.nsIWebDAVOperationListener;
81 const calICalendar = Components.interfaces.calICalendar;
82 const nsISupportsCString = Components.interfaces.nsISupportsCString;
83 const calIErrors = Components.interfaces.calIErrors;
84 const calIFreeBusyInterval = Components.interfaces.calIFreeBusyInterval;
85 const calICalDavCalendar = Components.interfaces.calICalDavCalendar;
86 const calIDateTime = Components.interfaces.calIDateTime;
87
88 var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
89 .getService(Components.interfaces.nsIXULAppInfo);
90 var isOnBranch = appInfo.platformVersion.indexOf("1.8") == 0;
91
92
93 function getLocationPath(item) {
94
95 var locPath = this.mItemInfoCache[item.id].locationPath;
96 if (locPath) {
97 LOG("using locationPath: " + locPath);
98 } else {
99 locPath = item.id;
100 if (locPath) {
101 locPath += ".ics";
102 }
103 LOG("using locationPath: " + locPath);
104 }
105 return locPath;
106 }
107
108 // END_OF_TIME needs to be the max value a PRTime can be
109 const START_OF_TIME = -0x7fffffffffffffff;
110 const END_OF_TIME = 0x7fffffffffffffff;
111
112 // used in checking calendar URI for (Cal)DAV-ness
113 const kDavResourceTypeNone = 0;
114 const kDavResourceTypeCollection = 1;
115 const kDavResourceTypeCalendar = 2;
116
117 // used for etag checking
118 const CALDAV_ADOPT_ITEM = 1;
119 const CALDAV_MODIFY_ITEM = 2;
120 const CALDAV_DELETE_ITEM = 3;
121
122 calDavCalendar.prototype = {
123 __proto__: calProviderBase.prototype,
124 //
125 // nsISupports interface
126 //
127 QueryInterface: function (aIID) {
128 return doQueryInterface(this, calDavCalendar.prototype, aIID,
129 [Components.interfaces.calICalendarProvider,
130 Components.interfaces.nsIInterfaceRequestor,
131 Components.interfaces.calIFreeBusyProvider,
132 calICalDavCalendar]);
133 },
134
135 initMemoryCalendar: function caldav_iMC() {
136 this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
137 .createInstance(Components.interfaces.calICalendar);
138
139 this.mMemoryCalendar.superCalendar = this;
140 this.mObserver = new calDavObserver(this);
141 this.mMemoryCalendar.addObserver(this.mObserver);
142 this.mMemoryCalendar.setProperty("relaxedMode", true);
143 },
144
145 //
146 // calICalendarProvider interface
147 //
148 get prefChromeOverlay() {
149 return null;
150 },
151
152 get displayName() {
153 return calGetString("calendar", "caldavName");
154 },
155
156 createCalendar: function caldav_createCal() {
157 throw NS_ERROR_NOT_IMPLEMENTED;
158 },
159
160 deleteCalendar: function caldav_deleteCal(cal, listener) {
161 throw NS_ERROR_NOT_IMPLEMENTED;
162 },
163
164 //
165 // calICalendar interface
166 //
167
168 // readonly attribute AUTF8String type;
169 get type() { return "caldav"; },
170
171 mDisabled: false,
172
173 mPrincipalsNS: null,
174
175 mHaveScheduling: false,
176
177 mMailToUrl: null,
178
179 get canRefresh() {
180 return true;
181 },
182
183 // mUriParams stores trailing ?parameters from the
184 // supplied calendar URI. Needed for (at least) Cosmo
185 // tickets
186 mUriParams: null,
187
188 get uri() { return this.mUri },
189
190 set uri(aUri) {
191 this.mUri = aUri;
192 this.initMemoryCalendar();
193
194 this.checkDavResourceType();
195 return aUri;
196 },
197
198 get mCalendarUri() {
199 calUri = this.mUri.clone();
200 var parts = calUri.spec.split('?');
201 if (parts.length > 1) {
202 calUri.spec = parts.shift();
203 this.mUriParams = '?' + parts.join('?');
204 }
205 if (calUri.spec.charAt(calUri.spec.length-1) != '/') {
206 calUri.spec += "/";
207 }
208 return calUri;
209 },
210
211 setCalHomeSet: function caldav_setCalHomeSet() {
212 var calUri = this.mUri.clone();
213 var split1 = calUri.spec.split('?');
214 var baseUrl = split1[0];
215 if (baseUrl.charAt(baseUrl.length-1) == '/') {
216 baseUrl = baseUrl.substring(0, baseUrl.length-2);
217 }
218 var split2 = baseUrl.split('/');
219 split2.pop();
220 calUri.spec = split2.join('/') + '/';
221 this.mCalHomeSet = calUri;
222 },
223
224 mOutBoxUrl: null,
225
226 mInBoxUrl: null,
227
228 mAuthScheme: null,
229
230 mAuthRealm: null,
231
232 get authRealm() {
233 return this.mAuthRealm;
234 },
235
236 makeUri: function caldav_makeUri(aInsertString) {
237 var spec = this.mCalendarUri.spec + aInsertString;
238 if (this.mUriParams) {
239 return spec + this.mUriParams;
240 }
241 return spec;
242 },
243
244 get mLocationPath() {
245 return decodeURIComponent(this.mCalendarUri.path);
246 },
247
248 // XXX todo: in general we want to do CalDAV scheduling, but for servers
249 // that don't support it, we want Itip
250 // sendItipInvitations is now used from calProviderBase.
251
252 promptOverwrite: function caldavPO(aMethod, aItem, aListener, aOldItem) {
253 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
254 getService(Components.interfaces.nsIPromptService);
255
256 var promptTitle = calGetString("calendar", "itemModifiedOnServerTitle");
257 var promptMessage = calGetString("calendar", "itemModifiedOnServer");
258 var buttonLabel1;
259
260 if (aMethod == CALDAV_MODIFY_ITEM) {
261 promptMessage += calGetString("calendar", "modifyWillLoseData");
262 buttonLabel1 = calGetString("calendar", "proceedModify");
263 } else {
264 promptMessage += calGetString("calendar", "deleteWillLoseData");
265 buttonLabel1 = calGetString("calendar", "proceedDelete");
266 }
267
268 var buttonLabel2 = calGetString("calendar", "updateFromServer");
269
270 var flags = promptService.BUTTON_TITLE_IS_STRING *
271 promptService.BUTTON_POS_0 +
272 promptService.BUTTON_TITLE_IS_STRING *
273 promptService.BUTTON_POS_1;
274
275 var choice = promptService.confirmEx(null, promptTitle, promptMessage,
276 flags, buttonLabel1, buttonLabel2,
277 null, null, {});
278
279 if (choice == 0) {
280 if (aMethod == CALDAV_MODIFY_ITEM) {
281 this.doModifyItem(aItem, aOldItem, aListener, true);
282 } else {
283 this.doDeleteItem(aItem, aListener, true);
284 }
285 } else {
286 this.getUpdatedItem(aItem, aListener);
287 }
288
289 },
290
291 mItemInfoCache: null,
292
293 mHrefIndex: null,
294
295 /**
296 * prepare channel with standard request headers
297 * and upload data/content-type if needed
298 *
299 * @param arUri channel Uri
300 * @param aUploadData data to be uploaded, if any
301 * @param aContentType value for Content-Type header, if any
302 */
303
304 prepChannel: function caldavPC(aUri, aUploadData, aContentType) {
305 var ioService = Components.classes["@mozilla.org/network/io-service;1"]
306 .getService(Components.interfaces.nsIIOService);
307 var channel = ioService.newChannelFromURI(aUri);
308
309 var httpchannel = channel.QueryInterface(Components.interfaces
310 .nsIHttpChannel);
311
312 httpchannel.setRequestHeader("Accept", "text/xml", false);
313 httpchannel.setRequestHeader("Accept-Charset", "utf-8,*;q=0.1", false);
314 httpchannel.notificationCallbacks = this;
315
316 if (aUploadData) {
317 httpchannel = httpchannel.QueryInterface(Components.interfaces.
318 nsIUploadChannel);
319 var converter =
320 Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
321 .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
322 converter.charset = "UTF-8";
323 var stream = converter.convertToInputStream(aUploadData);
324 httpchannel.setUploadStream(stream, aContentType, -1);
325 }
326 return httpchannel;
327 },
328
329 /**
330 * addItem(); required by calICalendar.idl
331 * we actually use doAdoptItem()
332 *
333 * @param aItem item to add
334 * @param aListener listener for method completion
335 */
336
337 addItem: function caldavAI(aItem, aListener) {
338 var newItem = aItem.clone();
339 return this.doAdoptItem(newItem, aListener, false);
340 },
341
342 /**
343 * adooptItem(); required by calICalendar.idl
344 * we actually use doAdoptItem()
345 *
346 * @param aItem item to check
347 * @param aListener listener for method completion
348 */
349 adoptItem: function caldavAtI(aItem, aListener) {
350 var newItem = aItem.clone();
351 return this.doAdoptItem(newItem, aListener, false);
352 },
353
354 /**
355 * Performs the actual addition of the item to CalDAV store
356 *
357 * @param aItem item to add
358 * @param aListener listener for method completion
359 * @param aIgnoreEtag ignore item etag
360 */
361 doAdoptItem: function caldavaDAI(aItem, aListener, aIgnoreEtag) {
362 if (aItem.id == null && aItem.isMutable) {
363 aItem.id = getUUID();
364 }
365
366 if (aItem.id == null) {
367 if (aListener)
368 aListener.onOperationComplete (this.superCalendar,
369 Components.results.NS_ERROR_FAILURE,
370 aListener.ADD,
371 aItem.id,
372 "Can't set ID on non-mutable item to addItem");
373 return;
374 }
375
376 var locationPath = aItem.id + ".ics";
377 var itemUri = this.mCalendarUri.clone();
378 itemUri.spec = this.makeUri(locationPath);
379 LOG("itemUri.spec = " + itemUri.spec);
380
381 var addListener = {};
382 var thisCalendar = this;
383 addListener.onStreamComplete =
384 function onPutComplete(aLoader, aContext, aStatus, aResultLength,
385 aResult) {
386 var status = aContext.responseStatus;
387 // 201 = HTTP "Created"
388 //
389 if (status == 201) {
390 LOG("Item added successfully");
391
392 var retVal = Components.results.NS_OK;
393 // Some CalDAV servers will modify items on PUT (add X-props,
394 // change location, etc) so we'd best re-fetch in order to know
395 // the current state of the item
396 // Observers will be notified in getUpdatedItem()
397 thisCalendar.getUpdatedItem(aItem, aListener);
398
399 } else if (status == 200) {
400 LOG("CalDAV: 200 received from server: server malfunction");
401 retVal = Components.results.NS_ERROR_FAILURE;
402 } else if (status == 412) {
403 LOG("CalDAV: etag exists on adopt item: server malfunction");
404 retVal = Components.results.NS_ERROR_FAILURE;
405 } else {
406 if (status > 999) {
407 status = "0x" + aStatusCode.toString(16);
408 }
409
410 // XXX real error handling
411 LOG("Error adding item: " + status);
412 retVal = Components.results.NS_ERROR_FAILURE;
413 }
414 }
415
416 aItem.calendar = this.superCalendar;
417 aItem.generation = 1;
418
419 // LOG("icalString = " + aItem.icalString);
420
421 var httpchannel = this.prepChannel(itemUri, aItem.icalString,
422 "text/calendar; charset=utf-8");
423
424
425 if (!aIgnoreEtag) {
426 httpchannel.setRequestHeader("If-None-Match", "*", false);
427 }
428
429 var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
430 .createInstance(Components.interfaces
431 .nsIStreamLoader);
432
433 if (isOnBranch) {
434 streamLoader.init(httpchannel, addListener, httpchannel);
435 } else {
436 streamLoader.init(addListener);
437 httpchannel.asyncOpen(streamLoader, httpchannel);
438 }
439
440 return;
441 },
442
443 /**
444 * modifyItem(); required by calICalendar.idl
445 * we actually use doModifyItem()
446 *
447 * @param aItem item to check
448 * @param aListener listener for method completion
449 */
450 modifyItem: function caldavMI(aNewItem, aOldItem, aListener) {
451 return this.doModifyItem(aNewItem, aOldItem, aListener, false);
452 },
453
454 /**
455 * Modifies existing item in CalDAV store.
456 *
457 * @param aItem item to check
458 * @param aOldItem previous version of item to be modified
459 * @param aListener listener from original request
460 * @param aIgnoreEtag ignore item etag
461 */
462 doModifyItem: function caldavMI(aNewItem, aOldItem, aListener, aIgnoreEtag) {
463
464 if (aNewItem.id == null) {
465
466 // XXXYYY fix to match iface spec
467 // this is definitely an error
468 if (aListener) {
469 try {
470 aListener.onOperationComplete(this.superCalendar,
471 Components.results.NS_ERROR_FAILURE,
472 aListener.MODIFY,
473 aItem.id,
474 "ID for modifyItem doesn't exist or is null");
475 } catch (ex) {
476 LOG("modifyItem's onOperationComplete threw an"
477 + " exception " + ex + "; ignoring");
478 }
479 }
480
481 return;
482 }
483
484 if (aNewItem.parentItem != aNewItem) {
485 aNewItem.parentItem.recurrenceInfo.modifyException(aNewItem);
486 aNewItem = aNewItem.parentItem;
487 }
488
489 var eventUri = this.mCalendarUri.clone();
490 eventUri.spec = this.makeUri(this.mItemInfoCache[aNewItem.id].locationPath);
491
492 var modListener = {};
493 var thisCalendar = this;
494
495 var modifiedItem = getIcsService().createIcalComponent("VCALENDAR");
496 calSetProdidVersion(modifiedItem);
497 modifiedItem.addSubcomponent(aNewItem.icalComponent);
498 if (aNewItem.recurrenceInfo) {
499 var exceptions = aNewItem.recurrenceInfo.getExceptionIds({});
500 for each (var exc in exceptions) {
501 modifiedItem.addSubcomponent(aNewItem.recurrenceInfo.getExceptionFor(exc, true).icalComponent);
502 }
503 }
504 var modifiedItemICS = modifiedItem.serializeToICS();
505
506 modListener.onStreamComplete = function(aLoader, aContext, aStatus,
507 aResultLength, aResult) {
508 // 201 = HTTP "Created"
509 // 204 = HTTP "No Content"
510 //
511 var status = aContext.responseStatus;
512 if (status == 204 || status == 201) {
513 LOG("Item modified successfully.");
514 var retVal = Components.results.NS_OK;
515 // Some CalDAV servers will modify items on PUT (add X-props,
516 // change location, etc) so we'd best re-fetch in order to know
517 // the current state of the item
518 // Observers will be notified in getUpdatedItem()
519 thisCalendar.getUpdatedItem(aNewItem, aListener);
520 } else if (status == 412) {
521 thisCalendar.promptOverwrite(CALDAV_MODIFY_ITEM, aNewItem,
522 aListener, aOldItem);
523 } else {
524 if (status > 999) {
525 status = "0x " + status.toString(16);
526 }
527 LOG("Error modifying item: " + status);
528
529 // XXX deal with non-existent item here, other
530 // real error handling
531
532 // XXX aStatusCode will be 201 Created for a PUT on an item
533 // that didn't exist before.
534
535 retVal = Components.results.NS_ERROR_FAILURE;
536 }
537 return;
538 }
539
540 // XXX use etag as generation
541
542 var httpchannel = this.prepChannel(eventUri, modifiedItemICS,
543 "text/calendar; charset=utf-8");
544
545 if (!aIgnoreEtag) {
546 httpchannel.setRequestHeader("If-Match",
547 this.mItemInfoCache[aNewItem.id].etag,
548 false);
549 }
550
551 LOG("modifyItem: PUTting = " + modifiedItemICS);
552 var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
553 .createInstance(Components.interfaces
554 .nsIStreamLoader);
555
556 if (isOnBranch) {
557 streamLoader.init(httpchannel, modListener, httpchannel);
558 } else {
559 streamLoader.init(modListener);
560 httpchannel.asyncOpen(streamLoader, httpchannel);
561 }
562
563 return;
564 },
565
566 /**
567 * deleteItem(); required by calICalendar.idl
568 * the actual deletion is done in doDeleteItem()
569 *
570 * @param aItem item to delete
571 * @param aListener listener for method completion
572 */
573 deleteItem: function caldavDI(aItem, aListener) {
574 return this.doDeleteItem(aItem, aListener, false);
575 },
576
577 /**
578 * Deletes item from CalDAV store.
579 *
580 * @param aItem item to delete
581 * @param aListener listener for method completion
582 * @param aIgnoreEtag ignore item etag
583 */
584 doDeleteItem: function caldavDDI(aItem, aListener, aIgnoreEtag) {
585
586 if (aItem.id == null) {
587 if (aListener)
588 aListener.onOperationComplete (this.superCalendar,
589 Components.results.NS_ERROR_FAILURE,
590 aListener.DELETE,
591 aItem.id,
592 "ID doesn't exist for deleteItem");
593 return;
594 }
595
596 var eventUri = this.mCalendarUri.clone();
597 eventUri.spec = this.makeUri(this.mItemInfoCache[aItem.id].locationPath);
598
599 var delListener = {};
600 var thisCalendar = this;
601 var realListener = aListener; // need to access from callback
602
603 delListener.onStreamComplete =
604 function caldavDLoSC(aLoader, aContext, aStatus, aResultLength, aResult) {
605
606 var status = aContext.responseStatus;
607 // 204 = HTTP "No content"
608 //
609 if (status == 204) {
610 thisCalendar.mMemoryCalendar.deleteItem(aItem, aListener);
611 delete thisCalendar.mHrefIndex[eventUri.path];
612 delete thisCalendar.mItemInfoCache[aItem.id];
613 LOG("Item deleted successfully.");
614 var retVal = Components.results.NS_OK;
615 }
616 else if (status == 412) {
617 // item has either been modified or deleted by someone else
618 // check to see which
619
620 var httpchannel2 = thisCalendar.prepChannel(eventUri, null, null);
621 httpchannel2.requestMethod = "HEAD";
622 var streamLoader2 = Components.classes
623 ["@mozilla.org/network/stream-loader;1"]
624 .createInstance(Components.interfaces
625 .nsIStreamLoader);
626 if (isOnBranch) {
627 streamLoader2.init(httpchannel2, delListener2, httpchannel2);
628 } else {
629 streamLoader2.init(streamListener2);
630 httpchannel2.asyncOpen(streamLoader2, httpchannel2);
631 }
632
633 } else {
634 LOG("Error deleting item: " + status);
635 // XXX real error handling here
636 retVal = Components.results.NS_ERROR_FAILURE;
637 }
638 }
639 var delListener2 = {};
640 delListener2.onStreamComplete =
641 function caldavDL2oSC(aLoader, aContext, aStatus, aResultLength, aResult) {
642 var status2 = aContext.responseStatus;
643 if (status2 == 404) {
644 // someone else already deleted it
645 return;
646 } else {
647 thisCalendar.promptOverwrite(CALDAV_DELETE_ITEM, aItem,
648 realListener, null);
649 }
650 }
651
652 // XXX check generation
653 var httpchannel = this.prepChannel(eventUri, null, null);
654 if (!aIgnoreEtag) {
655 httpchannel.setRequestHeader("If-Match",
656 this.mItemInfoCache[aItem.id].etag,
657 false);
658 }
659 httpchannel.requestMethod = "DELETE";
660
661 var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
662 .createInstance(Components.interfaces
663 .nsIStreamLoader);
664
665 if (isOnBranch) {
666 streamLoader.init(httpchannel, delListener, httpchannel);
667 } else {
668 streamLoader.init(delListener);
669 httpchannel.asyncOpen(streamLoader, httpchannel);
670 }
671
672 return;
673 },
674
675 /**
676 * Retrieves a specific item from the CalDAV store.
677 * Use when an outdated copy of the item is in hand.
678 *
679 * @param aItem item to fetch
680 * @param aListener listener for method completion
681 */
682 getUpdatedItem: function caldavGUI(aItem, aListener) {
683
684 if (aItem == null) {
685 if (aListener) {
686 aListener.onOperationComplete(this.superCalendar,
687 Components.results.NS_ERROR_FAILURE,
688 aListener.GET,
689 null,
690 "passed in null item");
691 }
692 return;
693 }
694
695 var itemType = "VEVENT";
696 if (aItem instanceof Components.interfaces.calITodo) {
697 itemType = "VTODO";
698 }
699
700 var queryStatuses = new Array();
701
702 var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
703 var D = new Namespace("D", "DAV:");
704 default xml namespace = C;
705
706 queryXml =
707 <calendar-query xmlns:D="DAV:">
708 <D:prop>
709 <D:getetag/>
710 <calendar-data/>
711 </D:prop>
712 <filter>
713 <comp-filter name="VCALENDAR">
714 <comp-filter name={itemType}>
715 <prop-filter name="UID">
716 <text-match collation="i;octet">
717 {aItem.id}
718 </text-match>
719 </prop-filter>
720 </comp-filter>
721 </comp-filter>
722 </filter>
723 </calendar-query>;
724
725 this.reportInternal(xmlHeader + queryXml.toXMLString(), aItem, aListener);
726 return;
727
728 },
729
730 // void getItem( in string id, in calIOperationListener aListener );
731 getItem: function (aId, aListener) {
732 this.mMemoryCalendar.getItem(aId, aListener);
733 return;
734 },
735
736 reportInternal: function caldavRI(aQuery, aItem, aListener)
737 {
738 var reportListener = new WebDavListener();
739 var thisCalendar = this; // need to access from inside the callback
740
741 reportListener.onOperationDetail = function(aStatusCode, aResource,
742 aOperation, aDetail,
743 aClosure) {
744 var rv;
745 var errString;
746
747 // is this detail the entire search result, rather than a single
748 // detail within a set?
749 //
750 if (aResource.path == calendarDirUri.path) {
751 // XXX is this even valid? what should we do here?
752 // XXX if it's an error, it might be valid?
753 LOG("XXX report result for calendar, not event\n");
754 throw("XXX report result for calendar, not event\n");
755 }
756
757 var items = null;
758
759 // XXX need to do better than looking for just 200
760 if (aStatusCode == 200) {
761
762 // aDetail is the response element from the multi-status
763 // XXX try-catch
764 var xSerializer = Components.classes
765 ['@mozilla.org/xmlextras/xmlserializer;1']
766 .getService(Components.interfaces.nsIDOMSerializer);
767 // libical needs to see \r\n instead on \n\n in the case of "folded" lines
768 var response = xSerializer.serializeToString(aDetail).replace(/\n\n/g, "\r\n");
769 var responseElement = new XML(response);
770
771 // create calIItemBase from e4x object
772 // XXX error-check that we only have one result, etc
773 var C = new Namespace("urn:ietf:params:xml:ns:caldav");
774 var D = new Namespace("DAV:");
775
776 var etag = responseElement..D::["getetag"];
777
778 // cause returned data to be parsed into the item
779 var calData = responseElement..C::["calendar-data"];
780 if (!calData.toString().length) {
781 Components.utils.reportError(
782 "Empty or non-existent <calendar-data> element returned" +
783 " by CalDAV server for URI <" + aResource.spec +
784 ">; ignoring");
785 return;
786 }
787 // LOG("item result = \n" + calData);
788 var rootComp = getIcsService().parseICS(calData, null);
789
790 var calComp;
791 if (rootComp.componentType == 'VCALENDAR') {
792 calComp = rootComp;
793 } else {
794 calComp = rootComp.getFirstSubcomponent('VCALENDAR');
795 }
796
797 var unexpandedItems = [];
798 var uid2parent = {};
799 var excItems = [];
800
801 while (calComp) {
802 // Get unknown properties
803 var prop = calComp.getFirstProperty("ANY");
804 while (prop) {
805 thisCalendar.unmappedProperties.push(prop);
806 prop = calComp.getNextProperty("ANY");
807 }
808
809 var subComp = calComp.getFirstSubcomponent("ANY");
810 while (subComp) {
811 // Place each subcomp in a try block, to hopefully get as
812 // much of a bad calendar as possible
813 try {
814 var item = null;
815 switch (subComp.componentType) {
816 case "VEVENT":
817 item = Components.classes["@mozilla.org/calendar/event;1"].
818 createInstance(Components.interfaces.calIEvent);
819 break;
820 case "VTODO":
821 item = Components.classes["@mozilla.org/calendar/todo;1"].
822 createInstance(Components.interfaces.calITodo);
823 break;
824 case "VTIMEZONE":
825 // we should already have this, so there's no need to
826 // do anything with it here.
827 break;
828 default:
829 thisCalendar.unmappedComponents.push(subComp);
830 break;
831 }
832 if (item != null) {
833
834 item.icalComponent = subComp;
835 // save the location name in case we need to modify
836 // need to build using thisCalendar since aResource.spec
837 // won't contain any auth info embedded in the URI
838 var locationPath = decodeURIComponent(aResource.path)
839 .substr(thisCalendar.mLocationPath.length);
840 if (!thisCalendar.mItemInfoCache[item.id]) {
841 thisCalendar.mItemInfoCache[item.id] = {};
842 }
843 thisCalendar.mItemInfoCache[item.id].locationPath =
844 locationPath;
845 thisCalendar.mHrefIndex[aResource.path] = item.id;
846 var rid = item.recurrenceId;
847 if (rid == null) {
848 unexpandedItems.push( item );
849 if (item.recurrenceInfo != null) {
850 uid2parent[item.id] = item;
851 }
852 } else {
853 item.calendar = thisCalendar.superCalendar;
854 // force no recurrence info so we can
855 // rebuild it cleanly below
856 item.recurrenceInfo = null;
857 excItems.push(item);
858 }
859 }
860 } catch (ex) {
861 thisCalendar.mObservers.notify("onError", [ex.result, ex.toString()]);
862 }
863 subComp = calComp.getNextSubcomponent("ANY");
864 }
865 calComp = rootComp.getNextSubcomponent('VCALENDAR');
866 }
867
868 // tag "exceptions", i.e. items with rid:
869 for each (var item in excItems) {
870 var parent = uid2parent[item.id];
871 if (parent == null) {
872 LOG( "no parent item for rid=" + item.recurrenceId );
873 } else {
874 item.parentItem = parent;
875 item.parentItem.recurrenceInfo.modifyException(item);
876 }
877 }
878 // if we loop over both excItems and unexpandedItems using 'item'
879 // we can be confident that 'item' means something below
880 for each (var item in unexpandedItems) {
881 item.calendar = thisCalendar.superCalendar;
882 }
883
884 thisCalendar.mItemInfoCache[item.id].etag = etag;
885
886 // figure out what type of item to return
887 var iid;
888 if (item instanceof Components.interfaces.calIEvent) {
889 iid = Components.interfaces.calIEvent;
890 rv = Components.results.NS_OK;
891 items = [ item ];
892 } else if (item instanceof Components.interfaces.calITodo) {
893 iid = Components.interfaces.calITodo;
894 rv = Components.results.NS_OK;
895 items = [ item ];
896 } else {
897 errString = "Can't deduce item type based on query";
898 rv = Components.results.NS_ERROR_FAILURE;
899 }
900
901 } else {
902 // XXX
903 LOG("aStatusCode = " + aStatusCode);
904 errString = "XXX";
905 rv = Components.results.NS_ERROR_FAILURE;
906 }
907
908 if (errString) {
909 LOG("errString = " + errString);
910 }
911
912 for (var i = 0; i < items.length; i++) {
913 if (thisCalendar.mItemInfoCache[items[i].id]) {
914 thisCalendar.mMemoryCalendar.modifyItem(items[i], null,
915 aListener);
916 } else {
917 thisCalendar.mMemoryCalendar.adoptItem(items[i], aListener);
918 }
919 }
920 return;
921 };
922
923 reportListener.onOperationComplete = function(aStatusCode, aResource,
924 aOperation, aClosure) {
925 LOG("refresh completed with status " + aStatusCode);
926 thisCalendar.mObservers.notify("onLoad", [thisCalendar]);
927 };
928
929 // convert this into a form the WebDAV service can use
930 var xParser = Components.classes['@mozilla.org/xmlextras/domparser;1']
931 .getService(Components.interfaces.nsIDOMParser);
932 queryDoc = xParser.parseFromString(aQuery, "application/xml");
933
934 // construct the resource we want to search against
935 var calendarDirUri = this.mCalendarUri.clone();
936 calendarDirUri.spec = this.makeUri('');
937 // LOG("report uri = " + calendarDirUri.spec);
938 var calendarDirResource = new WebDavResource(calendarDirUri);
939
940 var webSvc = Components.classes['@mozilla.org/webdav/service;1']
941 .getService(Components.interfaces.nsIWebDAVService);
942 webSvc.report(calendarDirResource, queryDoc, true, reportListener,
943 this, null);
944 return;
945
946 },
947
948 // void getItems( in unsigned long aItemFilter, in unsigned long aCount,
949 // in calIDateTime aRangeStart, in calIDateTime aRangeEnd,
950 // in calIOperationListener aListener );
951 getItems: function caldav_getItems(aItemFilter, aCount, aRangeStart,
952 aRangeEnd, aListener) {
953
954
955 this.mMemoryCalendar.getItems(aItemFilter, aCount, aRangeStart,
956 aRangeEnd, aListener);
957 },
958
959 refresh: function caldav_refresh() {
960 if (this.mAuthScheme != "Digest") {
961 // Basic HTTP Auth will not have timed out, we can just refresh
962 // Same for Cosmo ticket-based authentication
963 this.safeRefresh();
964 } else {
965 // Digest auth may have timed out, and we need to make sure that
966 // several calendars in this realm do not attempt re-auth simultaneously
967 if (this.firstInRealm()) {
968 this.safeRefresh();
969 }
970 }
971 },
972
973 firstInRealm: function caldav_firstInRealm() {
974 var calendars = getCalendarManager().getCalendars({});
975 for (var i = 0; i < calendars.length ; i++) {
976 if (calendars[i].type != "caldav") {
977 continue;
978 }
979 if (calendars[i].uri.prePath == this.uri.prePath &&
980 calendars[i].QueryInterface(calICalDavCalendar)
981 .authRealm == this.mAuthRealm) {
982 if (calendars[i].id == this.id) {
983 return true;
984 }
985 break;
986 }
987 }
988 return false;
989 },
990
991 refreshOtherCals: function caldav_refreshOtherCals() {
992 var calendars = getCalendarManager().getCalendars({});
993 for (var i = 0; i < calendars.length ; i++) {
994 if (calendars[i].type == "caldav" &&
995 calendars[i].uri.prePath == this.uri.prePath &&
996 calendars[i].QueryInterface(calICalDavCalendar)
997 .authRealm == this.mAuthRealm &&
998 calendars[i].id != this.id) {
999 calendars[i].safeRefresh();
1000 }
1001 }
1002 },
1003
1004 safeRefresh: function caldav_safeRefresh() {
1005
1006 var itemTypes = new Array("VEVENT", "VTODO");
1007 var typesCount = itemTypes.length;
1008 var refreshEvent = {};
1009 refreshEvent.itemTypes = itemTypes;
1010 refreshEvent.typesCount = typesCount;
1011 refreshEvent.queryStatuses = [];
1012 refreshEvent.itemsNeedFetching = [];
1013 refreshEvent.itemsReported = [];
1014
1015 this.getUpdatedItems(refreshEvent);
1016
1017 },
1018
1019
1020 getUpdatedItems: function caldav_GUIs(aRefreshEvent) {
1021
1022 if (this.mDisabled) {
1023 // check if maybe our calendar has become available
1024 this.checkDavResourceType();
1025 return;
1026 }
1027
1028 if (aRefreshEvent.itemTypes.length) {
1029 var itemType = aRefreshEvent.itemTypes.pop();
1030 } else {
1031 return;
1032 }
1033
1034 var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
1035 var D = new Namespace("D", "DAV:");
1036 default xml namespace = C;
1037
1038 var queryXml =
1039 <calendar-query xmlns:D={D}>
1040 <D:prop>
1041 <D:getetag/>
1042 </D:prop>
1043 <filter>
1044 <comp-filter name="VCALENDAR">
1045 <comp-filter/>
1046 </comp-filter>
1047 </filter>
1048 </calendar-query>;
1049
1050 queryXml[0].C::filter.C::["comp-filter"]
1051 .C::["comp-filter"] =
1052 <comp-filter name={itemType}/>;
1053
1054
1055 var queryString = xmlHeader + queryXml.toXMLString();
1056
1057 var multigetQueryXml =
1058 <calendar-multiget xmlns:D={D}>
1059 <D:prop>
1060 <D:getetag/>
1061 <calendar-data/>
1062 </D:prop>
1063 </calendar-multiget>;
1064
1065 var etagListener = new WebDavListener();
1066 var thisCalendar = this;
1067
1068 etagListener.onOperationDetail = function(aStatusCode, aResource,
1069 aOperation, aDetail,
1070 aClosure) {
1071 var xSerializer = Components.classes
1072 ['@mozilla.org/xmlextras/xmlserializer;1']
1073 .getService(Components.interfaces.nsIDOMSerializer);
1074 // libical needs to see \r\n instead on \n\n in the case of
1075 // "folded" lines
1076 var response = xSerializer.serializeToString(aDetail).
1077 replace(/\n\n/g, "\r\n");
1078 var responseElement = new XML(response);
1079
1080 var etag = responseElement..D::["getetag"];
1081
1082 aRefreshEvent.itemsReported.push(aResource.path);
1083
1084 if (thisCalendar.mHrefIndex[aResource.path]) {
1085 var itemuid = thisCalendar.mHrefIndex[aResource.path];
1086 if (etag != thisCalendar.mItemInfoCache[itemuid].etag) {
1087 // we don't have a current copy in cache; fetch the item
1088 aRefreshEvent.itemsNeedFetching.push(aResource.path);
1089 }
1090 } else {
1091 aRefreshEvent.itemsNeedFetching.push(aResource.path);
1092 }
1093 }
1094
1095 etagListener.onOperationComplete = function(aStatusCode, aResource,
1096 aOperation, aClosure) {
1097 aRefreshEvent.queryStatuses.push(aStatusCode);
1098 var needsRefresh = false;
1099 if (aRefreshEvent.queryStatuses.length == aRefreshEvent.typesCount) {
1100
1101 for each (var statusCode in aRefreshEvent.queryStatuses) {
1102 if (statusCode != 207) { // XXX better error checking
1103 LOG("error fetching item etags: " + statusCode);
1104 }
1105 }
1106
1107 // if an item has been deleted from the server, delete it here too
1108 for (var path in thisCalendar.mHrefIndex) {
1109 if (aRefreshEvent.itemsReported.indexOf(path) < 0) {
1110
1111 var getItemListener = {};
1112 getItemListener.onGetResult = function caldav_gUIs_oGR(aCalendar,
1113 aStatus, aItemType, aDetail, aCount, aItems) {
1114 var itemToDelete = aItems[0];
1115 delete thisCalendar.mItemInfoCache[itemToDelete.id];
1116 thisCalendar.mMemoryCalendar.deleteItem(itemToDelete,
1117 getItemListener);
1118 delete thisCalendar.mHrefIndex[path];
1119 needsRefresh = true;
1120 }
1121 getItemListener.onOperationComplete = function
1122 caldav_gUIs_oOC(aCalendar, aStatus, aOperationType,
1123 aId, aDetail) {}
1124 thisCalendar.mMemoryCalendar.getItem(thisCalendar.mHrefIndex[path],
1125 getItemListener);
1126 }
1127 }
1128
1129 // avoid sending empty multiget requests
1130 // update views if something has been deleted server-side
1131 if (!aRefreshEvent.itemsNeedFetching.length) {
1132 if (needsRefresh) {
1133 thisCalendar.mObservers.notify("onLoad", [thisCalendar]);
1134 }
1135 return;
1136 }
1137
1138 while (aRefreshEvent.itemsNeedFetching.length > 0) {
1139 var locpath = aRefreshEvent.itemsNeedFetching.pop();
1140 var hrefXml = new XML();
1141 hrefXml = <hr xmlns:D={D}/>
1142 hrefXml.D::href = locpath;
1143 multigetQueryXml[0].appendChild(hrefXml.D::href);
1144 }
1145
1146 var multigetQueryString = xmlHeader +
1147 multigetQueryXml.toXMLString();
1148 thisCalendar.reportInternal(multigetQueryString, null, null);
1149
1150 if (thisCalendar.mAuthScheme == "Digest" &&
1151 thisCalendar.firstInRealm()) {
1152 thisCalendar.refreshOtherCals();
1153 }
1154
1155 } else {
1156 thisCalendar.getUpdatedItems(aRefreshEvent);
1157 }
1158 }
1159 var xParser = Components.classes['@mozilla.org/xmlextras/domparser;1']
1160 .getService(Components.interfaces.nsIDOMParser);
1161 queryDoc = xParser.parseFromString(queryString, "application/xml");
1162
1163 // construct the resource we want to search against
1164 var calendarDirUri = this.mCalendarUri.clone();
1165 calendarDirUri.spec = this.makeUri('');
1166 var calendarDirResource = new WebDavResource(calendarDirUri);
1167
1168 var webSvc = Components.classes['@mozilla.org/webdav/service;1']
1169 .getService(Components.interfaces.nsIWebDAVService);
1170 webSvc.report(calendarDirResource, queryDoc, true, etagListener,
1171 this, null);
1172 return;
1173 },
1174
1175 // nsIInterfaceRequestor impl
1176 getInterface: function(iid) {
1177 if (iid.equals(Components.interfaces.nsIAuthPrompt)) {
1178 return new calAuthPrompt();
1179 }
1180 else if (iid.equals(Components.interfaces.nsIPrompt)) {
1181 // use the window watcher service to get a nsIPrompt impl
1182 return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
1183 .getService(Components.interfaces.nsIWindowWatcher)
1184 .getNewPrompter(null);
1185 } else if (iid.equals(Components.interfaces.nsIProgressEventSink)) {
1186 return this;
1187 // Needed for Lightning on branch vvv
1188 } else if (iid.equals(Components.interfaces.nsIDocShellTreeItem)) {
1189 return this;
1190 } else if (iid.equals(Components.interfaces.nsIAuthPromptProvider)) {
1191 return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
1192 .getService(Components.interfaces.nsIWindowWatcher)
1193 .getNewPrompter(null);
1194 } else if (!isOnBranch && iid.equals(Components.interfaces.nsIAuthPrompt2)) {
1195 return new calAuthPrompt();
1196 }
1197 throw Components.results.NS_ERROR_NO_INTERFACE;
1198 },
1199
1200 //
1201 // Helper functions
1202 //
1203
1204 // Unless an error number is in this array, we consider it very bad, set
1205 // the calendar to readOnly, and give up.
1206 acceptableErrorNums: [],
1207
1208 onError: function caldav_onError(aErrNo, aMessage) {
1209 var errorIsOk = false;
1210 for each (num in this.acceptableErrorNums) {
1211 if (num == aErrNo) {
1212 errorIsOk = true;
1213 break;
1214 }
1215 }
1216 if (!errorIsOk) {
1217 this.mReadOnly = true;
1218 this.mDisabled = true;
1219 }
1220
1221 var paramBlock = Components.classes["@mozilla.org/embedcomp/dialogparam;1"]
1222 .createInstance(Components.interfaces
1223 .nsIDialogParamBlock);
1224 paramBlock.SetNumberStrings(3);
1225
1226 var promptMessage = calGetString("calendar", "disabledMode", [this.name]);
1227 paramBlock.SetString(0, promptMessage);
1228 var errCode = "0x"+aErrNo.toString(16);
1229 paramBlock.SetString(1, errCode);
1230 paramBlock.SetString(2, aMessage);
1231 var wWatcher = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
1232 .getService(Components.interfaces.nsIWindowWatcher);
1233 wWatcher.openWindow(null,
1234 "chrome://calendar/content/calErrorPrompt.xul",
1235 "_blank",
1236 "chrome,dialog=yes",
1237 paramBlock);
1238
1239 if (this.mDisabled) {
1240 this.refresh();
1241 }
1242 },
1243
1244 /**
1245 * Checks that the calendar URI exists and is a CalDAV calendar
1246 *
1247 */
1248 checkDavResourceType: function checkDavResourceType() {
1249 var resourceTypeXml = null;
1250 var resourceType = kDavResourceTypeNone;
1251 var thisCalendar = this;
1252
1253 var D = new Namespace("D", "DAV:");
1254 var queryXml = <D:propfind xmlns:D="DAV:">
1255 <D:prop>
1256 <D:resourcetype/>
1257 </D:prop>
1258 </D:propfind>;
1259
1260 var httpchannel = this.prepChannel(this.mUri,queryXml,
1261 "text/xml; charset=utf-8");
1262 httpchannel.setRequestHeader("Depth", "0", false);
1263 httpchannel.requestMethod = "PROPFIND";
1264
1265 var streamListener = {};
1266
1267 streamListener.onStreamComplete =
1268 function checkDavResourceType_oSC(aLoader, aContext, aStatus,
1269 aResultLength, aResult) {
1270 var resultConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
1271 .createInstance(Components
1272 .interfaces.nsIScriptableUnicodeConverter);
1273
1274 var wwwauth = aContext.getRequestHeader("Authorization");
1275
1276 if (this.mUriParams) {
1277 thisCalendar.mAuthScheme = "Ticket";
1278 } else {
1279 thisCalendar.mAuthScheme = wwwauth.split(" ")[0];
1280 }
1281
1282 // we only really need the authrealm for Digest auth
1283 // since only Digest is going to time out on us
1284 if (thisCalendar.mAuthScheme == "Digest") {
1285 var realmChop = wwwauth.split("realm=\"")[1];
1286 thisCalendar.mAuthRealm = realmChop.split("\", ")[0];
1287 }
1288
1289 resultConverter.charset = "UTF-8";
1290 var str;
1291 try {
1292 str = resultConverter.convertFromByteArray(aResult, aResultLength);
1293 } catch(e) {
1294 LOG("Failed to determine resource type");
1295 }
1296 str = str.substring(str.indexOf('\n'));
1297 var multistatus = new XML(str);
1298
1299 var resourceTypeXml = multistatus..D::["resourcetype"];
1300 if (resourceTypeXml.length == 0) {
1301 resourceType = kDavResourceTypeNone;
1302 } else if (resourceTypeXml.toString().indexOf("calendar") != -1) {
1303 resourceType = kDavResourceTypeCalendar;
1304 } else if (resourceTypeXml.toString().indexOf("collection") != -1) {
1305 resourceType = kDavResourceTypeCollection;
1306 }
1307
1308 if ((resourceType == null || resourceType == kDavResourceTypeNone) &&
1309 !thisCalendar.mDisabled) {
1310 thisCalendar.reportDavError(Components.interfaces.calIErrors.DAV_NOT_DAV,
1311 "dav_notDav");
1312 }
1313
1314 if ((resourceType == kDavResourceTypeCollection) &&
1315 !thisCalendar.mDisabled) {
1316 thisCalendar.reportDavError(Components.interfaces.calIErrors.DAV_DAV_NOT_CALDAV,
1317 "dav_davNotCaldav");
1318 }
1319
1320 // if this calendar was previously offline we want to recover
1321 if ((resourceType == kDavResourceTypeCalendar) &&
1322 thisCalendar.mDisabled) {
1323 thisCalendar.mDisabled = false;
1324 thisCalendar.mReadOnly = false;
1325 }
1326
1327 // we've authenticated in the process of PROPFINDing and can flush
1328 // the getItems request queue
1329 thisCalendar.setCalHomeSet();
1330 thisCalendar.checkServerCaps();
1331 }
1332 var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
1333 .createInstance(Components.interfaces
1334 .nsIStreamLoader);
1335
1336 if (isOnBranch) {
1337 streamLoader.init(httpchannel, streamListener, httpchannel);
1338 } else {
1339 streamLoader.init(streamListener);
1340 httpchannel.asyncOpen(streamLoader, httpchannel);
1341 }
1342 },
1343
1344 reportDavError: function caldav_rDE(aErrNo, aMessage) {
1345 this.onError(aErrNo, calGetString("calendar", aMessage, [this.mUri.spec]));
1346 },
1347
1348 /**
1349 * Checks server capabilities
1350 * currently just calendar-schedule
1351 *
1352 */
1353 checkServerCaps: function caldav_checkServerCaps() {
1354
1355 var homeSet = this.mCalHomeSet.clone();
1356 var thisCalendar = this;
1357
1358 var httpchannel = this.prepChannel(homeSet, null, null);
1359
1360 httpchannel.requestMethod = "OPTIONS";
1361
1362 var streamListener = {};
1363
1364 streamListener.onStreamComplete =
1365 function checkServerCaps_oSC(aLoader, aContext, aStatus,
1366 aResultLength, aResult) {
1367 var dav = aContext.getResponseHeader("DAV");
1368
1369 if (dav.indexOf("calendar-schedule") != -1) {
1370 thisCalendar.mHaveScheduling = true;
1371 // XXX - we really shouldn't register with the fb service
1372 // if another calendar with the same principal-URL has already
1373 // done so
1374 getFreeBusyService().addProvider(thisCalendar);
1375 thisCalendar.findPrincipalNS();
1376 } else {
1377 LOG("Server does not support CalDAV scheduling.");
1378 thisCalendar.refresh();
1379 }
1380 }
1381
1382 var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
1383 .createInstance(Components.interfaces
1384 .nsIStreamLoader);
1385
1386 if (isOnBranch) {
1387 streamLoader.init(httpchannel, streamListener, httpchannel);
1388 } else {
1389 streamLoader.init(streamListener);
1390 httpchannel.asyncOpen(streamLoader, httpchannel);
1391 }
1392
1393 },
1394
1395 /**
1396 * Locates the principal namespace
1397 */
1398 findPrincipalNS: function caldav_findPrincipalNS() {
1399
1400 var homeSet = this.mCalHomeSet.clone();
1401 var thisCalendar = this;
1402
1403 var D = new Namespace("D", "DAV:");
1404 var queryXml = <D:propfind xmlns:D="DAV:">
1405 <D:prop>
1406 <D:principal-collection-set/>
1407 </D:prop>
1408 </D:propfind>
1409
1410 var httpchannel = this.prepChannel(homeSet, queryXml,
1411 "text/xml; charset=utf-8");
1412
1413 httpchannel.setRequestHeader("Depth", "0", false);
1414 httpchannel.requestMethod = "PROPFIND";
1415
1416 var streamListener = {};
1417
1418 streamListener.onStreamComplete =
1419 function findInOutBoxes_oSC(aLoader, aContext, aStatus,
1420 aResultLength, aResult) {
1421 var resultConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
1422 .createInstance(Components
1423 .interfaces.nsIScriptableUnicodeConverter);
1424
1425 resultConverter.charset = "UTF-8";
1426 var str;
1427 try {
1428 str = resultConverter.convertFromByteArray(aResult, aResultLength);
1429 } catch(e) {
1430 LOG("Failed to propstat principal namespace");
1431 }
1432 str = str.substring(str.indexOf('\n'));
1433 var multistatus = new XML(str);
1434 var pnsUri = thisCalendar.mUri.clone();
1435 var pcs = multistatus..D::["principal-collection-set"]..D::href;
1436 if (pcs.charAt(pcs.length-1) != '/') {
1437 pcs += "/";
1438 }
1439
1440 pnsUri.path = thisCalendar.ensurePath(pcs);
1441 thisCalendar.mPrincipalsNS = pnsUri;
1442 thisCalendar.checkPrincipalsNameSpace();
1443 }
1444
1445 var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
1446 .createInstance(Components.interfaces
1447 .nsIStreamLoader);
1448
1449 if (isOnBranch) {
1450 streamLoader.init(httpchannel, streamListener, httpchannel);
1451 } else {
1452 streamLoader.init(streamListener);
1453 httpchannel.asyncOpen(streamLoader, httpchannel);
1454 }
1455 },
1456
1457 /**
1458 * Checks the principals namespace for scheduling info
1459 */
1460 checkPrincipalsNameSpace: function caldav_cPNS() {
1461
1462 var pns = this.mPrincipalsNS.clone();
1463 var thisCalendar = this;
1464
1465 var homePath = this.mCalHomeSet.path;
1466 if (homePath.charAt(homePath.length-1) == '/') {
1467 homePath = homePath.substr(0, homePath.length-1);
1468 }
1469
1470 var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
1471 var D = new Namespace("D", "DAV:");
1472 default xml namespace = C;
1473
1474 var queryXml = <D:principal-property-search xmlns:D="DAV:"
1475 xmlns:C="urn:ietf:params:xml:ns:caldav">
1476 <D:property-search>
1477 <D:prop>
1478 <C:calendar-home-set/>
1479 </D:prop>
1480 <D:match>{homePath}</D:match>
1481 </D:property-search>
1482 <D:prop>
1483 <C:calendar-home-set/>
1484 <C:calendar-user-address-set/>
1485 <C:schedule-inbox-URL/>
1486 <C:schedule-outbox-URL/>
1487 </D:prop>
1488 </D:principal-property-search>;
1489
1490 var httpchannel = this.prepChannel(pns, queryXml,
1491 "text/xml; charset=utf-8");
1492
1493 httpchannel.requestMethod = "REPORT";
1494
1495 var streamListener = {};
1496
1497 streamListener.onStreamComplete =
1498 function caldav_cPNS_oSC(aLoader, aContext, aStatus,
1499 aResultLength, aResult) {
1500 if (aContext.responseStatus != 207) {
1501 thisCalendar.mHaveScheduling = false;
1502 thisCalendar.mInBoxUrl = null;
1503 thisCalendar.mOutBoxUrl = null;
1504 return;
1505 }
1506 var resultConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
1507 .createInstance(Components
1508 .interfaces.nsIScriptableUnicodeConverter);
1509
1510 resultConverter.charset = "UTF-8";
1511 var str;
1512 try {
1513 str = resultConverter.convertFromByteArray(aResult, aResultLength);
1514 } catch(e) {
1515 LOG("Failed to report principals namespace");
1516 }
1517 thisCalendar.mMailToUrl = thisCalendar.mCalendarUri.spec;
1518
1519 if (str.substr(0,6) == "<?xml ") {
1520 str = str.substring(str.indexOf('\n'));
1521 }
1522 var multistatus = new XML(str);
1523
1524 for (var i = 0; i < multistatus.*.length(); i++) {
1525 var response = new XML(multistatus.*[i]);
1526
1527 var responseCHS = response..C::["calendar-home-set"]..D::href[0];
1528 if (!responseCHS) {
1529 responseCHS = response..D::["calendar-home-set"]..D::href[0];
1530 }
1531
1532 if (responseCHS.charAt(responseCHS.toString().length -1) != "/") {
1533 responseCHS += "/";
1534 }
1535
1536 if (responseCHS != thisCalendar.mCalHomeSet.path &&
1537 responseCHS != thisCalendar.mCalHomeSet.spec) {
1538 continue;
1539 }
1540 var addrHrefs =
1541 response..C::["calendar-user-address-set"]..D::href;
1542 if (!addrHrefs.toString().length) {
1543 var addrHrefs =
1544 response..D::propstat..D::["calendar-user-address-set"]..D::href;
1545 }
1546 for (var j = 0; j < addrHrefs.*.length(); j++) {
1547 if (addrHrefs[j].substr(0,7).toLowerCase() == "mailto:") {
1548 thisCalendar.mMailToUrl = addrHrefs[j];
1549 }
1550 }
1551 var ibUrl = thisCalendar.mUri.clone();
1552 var ibPath =
1553 response..C::["schedule-inbox-URL"]..D::href[0];
1554 if (!ibPath) {
1555 var ibPath = response..D::["schedule-inbox-URL"]..D::href[0];
1556 }
1557 ibUrl.path = thisCalendar.ensurePath(ibPath);
1558 thisCalendar.mInBoxUrl = ibUrl;
1559 var obUrl = thisCalendar.mUri.clone();
1560 var obPath =
1561 response..C::["schedule-outbox-URL"]..D::href[0];
1562 if (!obPath) {
1563 var obPath = response..D::["schedule-outbox-URL"]..D::href[0];
1564 }
1565 obUrl.path = thisCalendar.ensurePath(obPath);
1566 thisCalendar.mOutBoxUrl = obUrl;
1567 }
1568 thisCalendar.refresh();
1569 }
1570
1571 var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
1572 .createInstance(Components.interfaces
1573 .nsIStreamLoader);
1574
1575 if (isOnBranch) {
1576 streamLoader.init(httpchannel, streamListener, httpchannel);
1577 } else {
1578 streamLoader.init(streamListener);
1579 httpchannel.asyncOpen(streamLoader, httpchannel);
1580 }
1581 return;
1582 },
1583
1584 //
1585 // calIFreeBusyProvider interface
1586 //
1587
1588 getFreeBusyIntervals: function caldav_getFreeBusyIntervals(
1589 aCalId, aRangeStart, aRangeEnd, aBusyTypes, aListener) {
1590
1591 if (!this.mHaveScheduling || !this.mOutBoxUrl || !this.mMailToUrl) {
1592 LOG("Server does not support scheduling; freebusy query not possible");
1593 return;
1594 }
1595
1596 // the caller prepends MAILTO: to calid strings containing @
1597 // but apple needs that to be mailto:
1598 var aCalIdParts = aCalId.split(":");
1599 aCalIdParts[0] = aCalIdParts[0].toLowerCase();
1600
1601 if (aCalIdParts[0] != "mailto"
1602 && aCalIdParts[0] != "http"
1603 && aCalIdParts[0] != "https" ) {
1604 return;
1605 }
1606 mailto_aCalId = aCalIdParts.join(":");
1607
1608 var outBoxUri = this.mOutBoxUrl.clone();
1609 var thisCalendar = this;
1610
1611 var organizer = this.mMailToUrl;
1612
1613 var dtstamp = now().getInTimezone(UTC()).icalString;
1614 var dtstart = aRangeStart.getInTimezone(UTC()).icalString;
1615 var dtend = aRangeEnd.getInTimezone(UTC()).icalString;
1616 var uuid = getUUID();
1617
1618 var fbQuery = "BEGIN:VCALENDAR\n";
1619 fbQuery += "VERSION:" + calGetProductVersion() + "\n";
1620 fbQuery += "PRODID:-" + calGetProductId() + "\n";
1621 fbQuery += "METHOD:REQUEST\n";
1622 fbQuery += "BEGIN:VFREEBUSY\n";
1623 fbQuery += "DTSTAMP:" + dtstamp + "\n";
1624 fbQuery += "ORGANIZER:" + organizer + "\n";
1625 fbQuery += "DTSTART:" + dtstart + "\n";
1626 fbQuery += "DTEND:" + dtend + "\n";
1627 fbQuery += "UID:" + uuid + "\n";
1628 var attendee = "ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL;CN=" + mailto_aCalId + "\n";
1629 var attendeeFolded = this.foldLine(attendee);
1630 fbQuery += attendeeFolded;
1631 fbQuery += "END:VFREEBUSY\n";
1632 fbQuery += "END:VCALENDAR\n";
1633 // RFC 2445 is specific about how lines end...
1634 fbQuery = fbQuery.replace(/\n/g, "\r\n");
1635
1636 var httpchannel = this.prepChannel(outBoxUri, fbQuery,
1637 "text/calendar; charset=utf-8");
1638 httpchannel.requestMethod = "POST";
1639 httpchannel.setRequestHeader("Originator", organizer, false);
1640 httpchannel.setRequestHeader("Recipient", mailto_aCalId, false);
1641
1642 var streamListener = {};
1643
1644 streamListener.onStreamComplete =
1645 function caldav_GFBI_oSC(aLoader, aContext, aStatus,
1646 aResultLength, aResult) {
1647 var resultConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
1648 .createInstance(Components
1649 .interfaces.nsIScriptableUnicodeConverter);
1650
1651 resultConverter.charset = "UTF-8";
1652 var str;
1653 try {
1654 str = resultConverter.convertFromByteArray(aResult, aResultLength);
1655 } catch(e) {
1656 LOG("Failed to parse freebusy response");
1657 }
1658
1659 if (aContext.responseStatus == 200) { // XXX = better error handling
1660 var periodsToReturn = [];
1661 var CalPeriod = new Components.
1662 Constructor("@mozilla.org/calendar/period;1",
1663 "calIPeriod");
1664 var CalDateTime = new Components.
1665 Constructor("@mozilla.org/calendar/datetime;1",
1666 "calIDateTime");
1667 var fbTypeMap = {};
1668 fbTypeMap["FREE"] = calIFreeBusyInterval.FREE;
1669 fbTypeMap["BUSY"] = calIFreeBusyInterval.BUSY;
1670 fbTypeMap["BUSY-UNAVAILABLE"] = calIFreeBusyInterval.BUSY_UNAVAILABLE;
1671 fbTypeMap["BUSY-TENTATIVE"] = calIFreeBusyInterval.BUSY_TENTATIVE;
1672 var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
1673 var D = new Namespace("D", "DAV:");
1674
1675 if (str.substr(0,6) == "<?xml ") {
1676 str = str.substring(str.indexOf('\n'));
1677 }
1678 str = str.replace(/\n\ /g, "");
1679 str = str.replace(/\r/g, "");
1680
1681 var response = new XML(str);
1682 var status = response..C::response..C::["request-status"];
1683 if (status.substr(0,1) != 2) {
1684 LOG("Got status " + status + " in response to freebusy query");
1685 return;
1686 }
1687 if (status.substr(0,3) != "2.0") {
1688 LOG("Got status " + status + " in response to freebusy query");
1689 }
1690 var caldata = response..C::response..C::["calendar-data"];
1691 var lines = caldata.split("\n");
1692 for (var i = 0; i < lines.length; i++) {
1693 if (lines[i].substr(0,8) == "FREEBUSY") {
1694 var descDat = lines[i].split(":");
1695 var fbName = descDat[0].split("=")[1];
1696 var fbType = fbTypeMap[fbName];
1697 var ranges = descDat[1].split(",");
1698 for (var j = 0; j < ranges.length; j++) {
1699 var parts = ranges[j].split("/");
1700 var begin = new CalDateTime();
1701 begin.icalString = parts[0];
1702 var end = new CalDateTime();
1703 end.icalString = parts[1];
1704 var period = new CalPeriod();
1705 period.start = begin;
1706 period.end = end;
1707 period.makeImmutable();
1708 var interval = {
1709 QueryInterface: function fbInterval_QueryInterface(iid) {
1710 ensureIID([calIFreeBusyInterval, nsISupports], iid);
1711 return this;
1712 },
1713 calId: aCalId,
1714 interval: period,
1715 freeBusyType: fbType
1716 };
1717 periodsToReturn.push(interval);
1718 }
1719 }
1720 }
1721 aListener.onResult(null, periodsToReturn);
1722 } else {
1723 LOG("Received status " + aContext.responseStatus + " from freebusy query");
1724 }
1725
1726 }
1727
1728 var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
1729 .createInstance(Components.interfaces
1730 .nsIStreamLoader);
1731
1732 if (isOnBranch) {
1733 streamLoader.init(httpchannel, streamListener, httpchannel);
1734 } else {
1735 streamLoader.init(streamListener);
1736 httpchannel.asyncOpen(streamLoader, httpchannel);
1737 }
1738
1739 },
1740
1741 /**
1742 * RFC 2445 line folding
1743 */
1744 foldLine: function caldav_foldLine(aString) {
1745 var parts = [];
1746 while (aString.length) {
1747 var part = aString.substr(0,72);
1748 parts.push(part);
1749 aString = aString.substr(72);
1750 }
1751 return parts.join("\n ");
1752 },
1753
1754 ensurePath: function caldav_ensurePath(aString) {
1755 if (aString.charAt(0) != "/") {
1756 var bogusUri = makeURL(aString);
1757 return bogusUri.path;
1758 }
1759 return aString;
1760 },
1761
1762 // stubs to keep callbacks we don't support yet from throwing errors
1763 // we don't care about
1764 // nsIProgressEventSink
1765 onProgress: function onProgress(aRequest, aContext, aProgress, aProgressMax) {},
1766 onStatus: function onStatus(aRequest, aContext, aStatus, aStatusArg) {},
1767 // nsIDocShellTreeItem
1768 findItemWithName: function findItemWithName(name, aRequestor, aOriginalRequestor) {}
1769 };
1770
1771 function WebDavResource(url) {
1772 this.mResourceURL = url;
1773 }
1774
1775 WebDavResource.prototype = {
1776 mResourceURL: {},
1777 get resourceURL() {
1778 return this.mResourceURL;} ,
1779 QueryInterface: function(iid) {
1780 if (iid.equals(CI.nsIWebDAVResource) ||
1781 iid.equals(CI.nsISupports)) {
1782 return this;
1783 }
1784
1785 throw Components.interfaces.NS_ERROR_NO_INTERFACE;
1786 }
1787 };
1788
1789 function WebDavListener() {
1790 }
1791
1792 WebDavListener.prototype = {
1793
1794 QueryInterface: function (aIID) {
1795 if (!aIID.equals(Components.interfaces.nsISupports) &&
1796 !aIID.equals(nsIWebDavOperationListener)) {
1797 throw Components.results.NS_ERROR_NO_INTERFACE;
1798 }
1799
1800 return this;
1801 },
1802
1803 onOperationComplete: function(aStatusCode, aResource, aOperation,
1804 aClosure) {
1805 // aClosure is the listener
1806 aClosure.onOperationComplete(this, aStatusCode, 0, null, null);
1807
1808 LOG("WebDavListener.onOperationComplete() called");
1809 return;
1810 },
1811
1812 onOperationDetail: function(aStatusCode, aResource, aOperation, aDetail,
1813 aClosure) {
1814 LOG("WebDavListener.onOperationDetail() called");
1815 return;
1816 }
1817 }
1818
1819 function calDavObserver(aCalendar) {
1820 this.mCalendar = aCalendar;
1821 }
1822
1823 calDavObserver.prototype = {
1824 mCalendar: null,
1825 mInBatch: false,
1826
1827 // calIObserver:
1828 onStartBatch: function() {
1829 this.mCalendar.observers.notify("onStartBatch");
1830 this.mInBatch = true;
1831 },
1832 onEndBatch: function() {
1833 this.mCalendar.observers.notify("onEndBatch");
1834 this.mInBatch = false;
1835 },
1836 onLoad: function(calendar) {
1837 this.mCalendar.observers.notify("onLoad", [calendar]);
1838 },
1839 onAddItem: function(aItem) {
1840 this.mCalendar.observers.notify("onAddItem", [aItem]);
1841 },
1842 onModifyItem: function(aNewItem, aOldItem) {
1843 this.mCalendar.observers.notify("onModifyItem", [aNewItem, aOldItem]);
1844 },
1845 onDeleteItem: function(aDeletedItem) {
1846 this.mCalendar.observers.notify("onDeleteItem", [aDeletedItem]);
1847 },
1848 onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
1849 this.mCalendar.observers.notify("onPropertyChanged", [aCalendar, aName, aValue, aOldValue]);
1850 },
1851 onPropertyDeleting: function(aCalendar, aName) {
1852 this.mCalendar.observers.notify("onPropertyDeleting", [aCalendar, aName]);
1853 },
1854
1855 // Unless an error number is in this array, we consider it very bad, set
1856 // the calendar to readOnly, and give up.
1857 acceptableErrorNums: [],
1858
1859 onError: function(aErrNo, aMessage) {
1860 var errorIsOk = false;
1861 for each (num in this.acceptableErrorNums) {
1862 if (num == aErrNo) {
1863 errorIsOk = true;
1864 break;
1865 }
1866 }
1867 if (!errorIsOk)
1868 this.mCalendar.readOnly = true;
1869 this.mCalendar.observers.notify("onError", [aErrNo, aMessage]);
1870 }
1871 };
1872
1873 var g_fbService = null;
1874 function getFreeBusyService() {
1875 if (!g_fbService) {
1876 g_fbService =
1877 Components.classes["@mozilla.org/calendar/freebusy-service;1"]
1878 .getService(Components.interfaces.calIFreeBusyService);
1879 }
1880 return g_fbService;
1881 };