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 Google Calendar Provider code.
15 *
16 * The Initial Developer of the Original Code is
17 * Philipp Kewisch <mozilla@kewis.ch>
18 * Portions created by the Initial Developer are Copyright (C) 2006
19 * the Initial Developer. All Rights Reserved.
20 *
21 * Contributor(s):
22 * Joey Minta <jminta@gmail.com>
23 *
24 * Alternatively, the contents of this file may be used under the terms of
25 * either the GNU General Public License Version 2 or later (the "GPL"), or
26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27 * in which case the provisions of the GPL or the LGPL are applicable instead
28 * of those above. If you wish to allow use of your version of this file only
29 * under the terms of either the GPL or the LGPL, and not to allow others to
30 * use your version of this file under the terms of the MPL, indicate your
31 * decision by deleting the provisions above and replace them with the notice
32 * and other provisions required by the GPL or the LGPL. If you do not delete
33 * the provisions above, a recipient may use your version of this file under
34 * the terms of any one of the MPL, the GPL or the LGPL.
35 *
36 * ***** END LICENSE BLOCK ***** */
37
38 /**
39 * calGoogleCalendar
40 * This Implements a calICalendar Object adapted to the Google Calendar
41 * Provider.
42 *
43 * @class
44 * @constructor
45 */
46 function calGoogleCalendar() {
47 this.initProviderBase();
48 }
49
50 calGoogleCalendar.prototype = {
51 __proto__: calProviderBase.prototype,
52
53 QueryInterface: function cGS_QueryInterface(aIID) {
54 return doQueryInterface(this,
55 calGoogleCalendar.prototype,
56 aIID,
57 null,
58 g_classInfo["calGoogleCalendar"]);
59 },
60
61 /* Member Variables */
62 mSession: null,
63 mFullUri: null,
64 mCalendarName: null,
65
66 /*
67 * Google Calendar Provider attributes
68 */
69
70 /**
71 * readonly attribute googleCalendarName
72 * Google's Calendar name. This represents the <calendar name> in
73 * http[s]://www.google.com/calendar/feeds/<calendar name>/private/full
74 */
75 get googleCalendarName cGC_getGoogleCalendarName() {
76 return this.mCalendarName;
77 },
78
79 /**
80 * attribute session
81 * An calGoogleSession Object that handles the session requests.
82 */
83 get session cGC_getSession() {
84 return this.mSession;
85 },
86 set session cGC_setSession(v) {
87 return this.mSession = v;
88 },
89
90 /**
91 * findSession
92 * Populates the Session Object based on the preferences or the result of a
93 * login prompt.
94 *
95 * @param aIgnoreExistingSession If set, find the session regardless of
96 * whether the session has been previously set
97 * or not
98 */
99 findSession: function cGC_findSession(aIgnoreExistingSession) {
100 if (this.mSession && !aIgnoreExistingSession) {
101 return;
102 }
103
104 // We need to find out which Google account fits to this calendar.
105 var googleUser = getCalendarPref(this, "googleUser");
106 if (googleUser) {
107 this.mSession = getSessionByUsername(googleUser);
108 } else {
109 // We have no user, therefore we need to ask the user. Show a
110 // user/password prompt and set the session based on those
111 // values.
112
113 var username = { value: this.mCalendarName };
114 var password = { value: null };
115 var savePassword = { value: false };
116
117 if (getCalendarCredentials(this.mCalendarName,
118 username,
119 password,
120 savePassword)) {
121 this.mSession = getSessionByUsername(username.value);
122 this.mSession.googlePassword = password.value;
123 this.mSession.savePassword = savePassword.value;
124 setCalendarPref(this,
125 "googleUser",
126 "CHAR",
127 this.mSession.googleUser);
128 }
129 }
130 },
131
132 /*
133 * implement calICalendar
134 */
135 get type cGC_getType() {
136 return "gdata";
137 },
138
139 get sendItipInvitations cGC_getSendItipInvitations() {
140 return false;
141 },
142
143 get uri cGC_getUri() {
144 return this.mUri;
145 },
146
147 get fullUri cGC_getFullUri() {
148 return this.mFullUri;
149 },
150 set uri cGC_setUri(aUri) {
151 // Parse google url, catch private cookies, public calendars,
152 // basic and full types, bogus ics file extensions, invalid hostnames
153 var re = new RegExp("/calendar/(feeds|ical)/" +
154 "([^/]+)/(public|private)-?([^/]+)?/" +
155 "(full|basic)(.ics)?$");
156
157 var matches = aUri.path.match(re);
158
159 if (!matches) {
160 throw new Components.Exception(aUri, Components.results.NS_ERROR_MALFORMED_URI);
161 }
162
163 // Set internal Calendar Name
164 this.mCalendarName = decodeURIComponent(matches[2]);
165
166 // Set normalized url. We need private visibility and full projection
167 this.mFullUri = aUri.clone();
168 this.mFullUri.path = "/calendar/feeds/" + matches[2] + "/private/full";
169
170 // Remember the uri as it was passed, in case the calendar manager
171 // relies on it.
172 this.mUri = aUri;
173
174 this.findSession(true);
175 return this.mUri;
176 },
177
178 getProperty: function cGC_getProperty(aName) {
179 switch (aName) {
180 // Capabilities
181 case "capabilities.attachments.supported":
182 case "capabilities.priority.supported":
183 case "capabilities.tasks.supported":
184 return false;
185 case "capabilities.privacy.values":
186 return ["DEFAULT", "PUBLIC", "PRIVATE"];
187 }
188
189 return this.__proto__.__proto__.getProperty.apply(this, arguments);
190 },
191
192 get canRefresh cGC_getCanRefresh() {
193 return true;
194 },
195
196 adoptItem: function cGC_adoptItem(aItem, aListener) {
197 LOG("Adding item " + aItem.title);
198
199 try {
200 // Check if calendar is readonly
201 if (this.readOnly) {
202 throw new Components.Exception("",
203 Components.interfaces.calIErrors.CAL_IS_READONLY);
204 }
205
206 // Make sure the item is an event
207 aItem = aItem.QueryInterface(Components.interfaces.calIEvent);
208
209 // Check if we have a session. If not, then the user has canceled
210 // the login prompt.
211 if (!this.mSession) {
212 this.findSession();
213 }
214
215 // Add the calendar to the item, for later use.
216 aItem.calendar = this.superCalendar;
217
218 // When adding items, the google user is the organizer.
219 var organizer = createAttendee();
220 organizer.isOrganizer = true;
221 organizer.commonName = this.mSession.googleFullName;
222 organizer.id = "mailto:" + this.mSession.googleUser;
223 aItem.organizer = organizer;
224
225 this.mSession.addItem(this,
226 aItem,
227 this.addItem_response,
228 aListener);
229 } catch (e) {
230 LOG("adoptItem failed before request " + aItem.title + "\n:" + e);
231 if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) {
232 // The calendar is readonly, make sure this is set and
233 // notify the user. This can come from above or from
234 // mSession.addItem which checks for the editURI
235 this.readOnly = true;
236 this.mObservers.notify("onError", [e.result, e.message]);
237 }
238
239 if (aListener != null) {
240 aListener.onOperationComplete(this.superCalendar,
241 e.result,
242 Components.interfaces.calIOperationListener.ADD,
243 null,
244 e.message);
245 }
246 }
247 },
248
249 addItem: function cGC_addItem(aItem, aListener) {
250 // Google assigns an ID to every event added. Any id set here or in
251 // adoptItem will be overridden.
252 return this.adoptItem( aItem.clone(), aListener );
253 },
254
255 modifyItem: function cGC_modifyItem(aNewItem, aOldItem, aListener) {
256 LOG("Modifying item " + aOldItem.title);
257
258 try {
259 if (this.readOnly) {
260 throw new Components.Exception("",
261 Components.interfaces.calIErrors.CAL_IS_READONLY);
262 }
263
264 // Check if we have a session. If not, then the user has canceled
265 // the login prompt.
266 if (!this.mSession) {
267 this.findSession();
268 }
269
270 // Check if enough fields have changed to warrant sending the event
271 // to google. This saves network traffic.
272 if (relevantFieldsMatch(aOldItem, aNewItem)) {
273 LOG("Not requesting item modification for " + aOldItem.id +
274 "(" + aOldItem.title + "), relevant fields match");
275
276 if (aListener != null) {
277 aListener.onOperationComplete(this.superCalendar,
278 Components.results.NS_OK,
279 Components.interfaces.calIOperationListener.MODIFY,
280 aNewItem.id,
281 aNewItem);
282 }
283 this.mObservers.notify("onModifyItem", [aNewItem, aOldItem]);
284 return;
285 }
286
287 // We need the old item in the response so the observer can be
288 // called correctly.
289 var extradata = { olditem: aOldItem,
290 newitem: aNewItem,
291 listener: aListener };
292
293 this.mSession.modifyItem(this,
294 aOldItem,
295 aNewItem,
296 this.modifyItem_response,
297 extradata);
298 } catch (e) {
299 LOG("modifyItem failed before request " +
300 aNewItem.title + "(" + aNewItem.id + "):\n" + e);
301
302 if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) {
303 // The calendar is readonly, make sure this is set and
304 // notify the user. This can come from above or from
305 // mSession.modifyItem which checks for the editURI
306 this.readOnly = true;
307 this.mObservers.notify("onError", [e.result, e.message]);
308 }
309
310 if (aListener != null) {
311 aListener.onOperationComplete(this.superCalendar,
312 e.result,
313 Components.interfaces.calIOperationListener.MODIFY,
314 null,
315 e.message);
316 }
317 }
318 },
319
320 deleteItem: function cGC_deleteItem(aItem, aListener) {
321 LOG("Deleting item " + aItem.title + "(" + aItem.id + ")");
322
323 try {
324 if (this.readOnly) {
325 throw new Components.Exception("",
326 Components.interfaces.calIErrors.CAL_IS_READONLY);
327 }
328
329 // Check if we have a session. If not, then the user has canceled
330 // the login prompt.
331 if (!this.mSession) {
332 this.findSession();
333 }
334
335 // We need the item in the response, since google dosen't return any
336 // item XML data on delete, and we need to call the observers.
337 var extradata = { listener: aListener, item: aItem };
338
339 this.mSession.deleteItem(this,
340 aItem,
341 this.deleteItem_response,
342 extradata);
343 } catch (e) {
344 LOG("deleteItem failed before request for " +
345 aItem.title + "(" + aItem.id + "):\n" + e);
346
347 if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) {
348 // The calendar is readonly, make sure this is set and
349 // notify the user. This can come from above or from
350 // mSession.deleteItem which checks for the editURI
351 this.readOnly = true;
352 this.mObservers.notify("onError", [e.result, e.message]);
353 }
354
355 if (aListener != null) {
356 aListener.onOperationComplete(this.superCalendar,
357 e.result,
358 Components.interfaces.calIOperationListener.DELETE,
359 null,
360 e.message);
361 }
362 }
363 },
364
365 getItem: function cGC_getItem(aId, aListener) {
366 // This function needs a test case using mechanisms in bug 365212
367 LOG("Getting item with id " + aId);
368 try {
369
370 // Check if we have a session. If not, then the user has canceled
371 // the login prompt.
372 if (!this.mSession) {
373 this.findSession();
374 }
375
376 var extradata = {
377 listener: aListener,
378 id: aId
379 };
380
381 this.mSession.getItem(this,
382 aId,
383 this.getItem_response,
384 extradata);
385 } catch (e) {
386 LOG("getItem failed before request " + aId + "):\n" + e);
387
388
389 if (aListener != null) {
390 aListener.onOperationComplete(this.superCalendar,
391 e.result,
392 Components.interfaces.calIOperationListener.GET,
393 null,
394 e.message);
395 }
396 }
397 },
398
399 getItems: function cGC_getItems(aItemFilter,
400 aCount,
401 aRangeStart,
402 aRangeEnd,
403 aListener) {
404 try {
405 // Check if we have a session. If not, then the user has canceled
406 // the login prompt.
407 if (!this.mSession) {
408 this.findSession();
409 }
410
411 // item base type
412 var wantEvents = ((aItemFilter &
413 Components.interfaces.calICalendar.ITEM_FILTER_TYPE_EVENT) != 0);
414 var wantTodos = ((aItemFilter &
415 Components.interfaces.calICalendar.ITEM_FILTER_TYPE_TODO) != 0);
416
417 // check if events are wanted
418 if (!wantEvents && !wantTodos) {
419 // Nothing to do. The onOperationComplete in the catch block
420 // below will catch this.
421 throw new Components.Exception("", Components.results.NS_OK);
422 } else if (wantTodos && !wantEvents) {
423 throw new Components.Exception("", Components.results.NS_ERROR_NOT_IMPLEMENTED);
424 }
425 // return occurrences?
426 var itemReturnOccurrences = ((aItemFilter &
427 Components.interfaces.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) != 0);
428
429 var extradata = {
430 itemfilter: aItemFilter,
431 rangeStart: aRangeStart,
432 rangeEnd: aRangeEnd,
433 listener: aListener
434 };
435
436 this.mSession.getItems(this,
437 aCount,
438 aRangeStart,
439 aRangeEnd,
440 itemReturnOccurrences,
441 this.getItems_response,
442 extradata);
443 } catch (e) {
444 if (aListener != null) {
445 aListener.onOperationComplete(this.superCalendar,
446 e.result,
447 Components.interfaces.calIOperationListener.GET,
448 null,
449 e.message);
450 }
451 }
452 },
453
454 refresh: function cGC_refresh() {
455 this.mObservers.notify("onLoad", [this]);
456 },
457
458 /*
459 * Google Calendar Provider Response functions
460 */
461
462 /**
463 * addItem_response
464 * Response callback, called by the session object when an item was added
465 *
466 * @param aRequest The request object that initiated the request
467 * @param aStatus The response code. This is a Components.results.* code
468 * @param aResult In case of an error, this is the error string, otherwise
469 * an XML representation of the added item.
470 */
471 addItem_response: function cGC_addItem_response(aRequest,
472 aStatus,
473 aResult) {
474 var item = this.general_response(Components.interfaces.calIOperationListener.ADD,
475 aResult,
476 aStatus,
477 aRequest.extraData);
478 // Notify Observers
479 if (item) {
480 this.mObservers.notify("onAddItem", [item]);
481 }
482 },
483
484 /**
485 * modifyItem_response
486 * Response callback, called by the session object when an item was modified
487 *
488 * @param aRequest The request object that initiated the request
489 * @param aStatus The response code. This is a Components.results.* Code
490 * @param aResult In case of an error, this is the error string, otherwise
491 * an XML representation of the modified item
492 */
493 modifyItem_response: function cGC_modifyItem_response(aRequest,
494 aStatus,
495 aResult) {
496 var item = this.general_response(Components.interfaces.calIOperationListener.MODIFY,
497 aResult,
498 aStatus,
499 aRequest.extraData.listener,
500 aRequest.extraData.newitem);
501 // Notify Observers
502 if (item) {
503 var oldItem = aRequest.extraData.olditem;
504 if (item.parentItem != item) {
505 item.parentItem.recurrenceInfo.modifyException(item);
506 item = item.parentItem;
507 oldItem = oldItem.parentItem;
508 }
509 this.mObservers.notify("onModifyItem", [item, oldItem]);
510 }
511 },
512
513 /**
514 * deleteItem_response
515 * Response callback, called by the session object when an Item was deleted
516 *
517 * @param aRequest The request object that initiated the request
518 * @param aStatus The response code. This is a Components.results.* Code
519 * @param aResult In case of an error, this is the error string, otherwise
520 * this may be empty.
521 */
522 deleteItem_response: function cGC_deleteItem_response(aRequest,
523 aStatus,
524 aResult) {
525 // The reason we are not using general_response here is because deleted
526 // items are not returned as xml from google. We need to pass the item
527 // we saved with the request.
528
529 try {
530 // Check if the call succeeded
531 if (aStatus != Components.results.NS_OK) {
532 throw new Components.Exception(aResult, aStatus);
533 }
534
535 // All operations need to call onOperationComplete
536 if (aRequest.extraData.listener) {
537 LOG("Deleting item " + aRequest.extraData.item.id +
538 " successful");
539
540 aRequest.extraData.listener.onOperationComplete(this.superCalendar,
541 Components.results.NS_OK,
542 Components.interfaces.calIOperationListener.DELETE,
543 aRequest.extraData.item.id,
544 aRequest.extraData.item);
545 }
546
547 // Notify Observers
548 this.mObservers.notify("onDeleteItem", [aRequest.extraData.item]);
549 } catch (e) {
550 LOG("Deleting item " + aRequest.extraData.item.id + " failed");
551 // Operation failed
552 if (aRequest.extraData.listener) {
553 aRequest.extraData.listener.onOperationComplete(this.superCalendar,
554 e.result,
555 Components.interfaces.calIOperationListener.DELETE,
556 null,
557 e.message);
558 }
559 }
560 },
561
562 /**
563 * getItem_response
564 * Response callback, called by the session object when a single Item was
565 * downloaded.
566 *
567 * @param aRequest The request object that initiated the request
568 * @param aStatus The response code. This is a Components.results.* Code
569 * @param aResult In case of an error, this is the error string, otherwise
570 * an XML representation of the requested item
571 */
572 getItem_response: function cGC_getItem_response(aRequest,
573 aStatus,
574 aResult) {
575 // XXX Due to google issue 399, we need to parse a full feed here.
576 try {
577 if (!Components.isSuccessCode(aStatus)) {
578 throw new Components.Exception(aResult, aStatus);
579 }
580
581 // Prepare Namespaces
582 var gCal = new Namespace("gCal",
583 "http://schemas.google.com/gCal/2005");
584 var gd = new Namespace("gd", "http://schemas.google.com/g/2005");
585 var atom = new Namespace("", "http://www.w3.org/2005/Atom");
586 default xml namespace = atom;
587
588 // A feed was passed back, parse it. Due to bug 336551 we need to
589 // filter out the <?xml...?> part.
590 var xml = new XML(aResult.substring(38));
591 var timezoneString = xml.gCal::timezone.@value.toString() || "UTC";
592 var timezone = gdataTimezoneProvider.getTimezone(timezoneString);
593
594 // This line is needed, otherwise the for each () block will never
595 // be entered. It may seem strange, but if you don't believe me, try
596 // it!
597 xml.link.(@rel);
598
599 // We might be able to get the full name through this feed's author
600 // tags. We need to make sure we have a session for that.
601 if (!this.mSession) {
602 this.findSession();
603 }
604
605 // Get the item entry by id
606 var itemEntry = xml.entry.(id.substring(id.lastIndexOf('/') + 1) == aRequest.extraData.id);
607 var item = XMLEntryToItem(itemEntry, timezone, this);
608
609 if (item.recurrenceInfo) {
610 // If this item is recurring, get all exceptions for this item.
611 for each (var entry in xml.entry.gd::originalEvent.(@id == aRequest.extraData.id)) {
612 var excItem = XMLEntryToItem(entry.parent(), timezone, this);
613
614 // Google uses the status field to reflect negative
615 // exceptions.
616 if (excItem.status == "CANCELED") {
617 item.recurrenceInfo.removeOccurrenceAt(excItem.recurrenceId);
618 } else {
619 excItem.calendar = this;
620 excItem.parentItem = item;
621 item.recurrenceInfo.modifyException(excItem);
622 }
623 }
624 }
625 // We are done, notify the listener of our result and that we are
626 // done.
627 aRequest.extraData.listener.onGetResult(this.superCalendar,
628 Components.results.NS_OK,
629 Components.interfaces.calIEvent,
630 null,
631 1,
632 [item]);
633 aRequest.extraData.listener.onOperationComplete(this.superCalendar,
634 Components.results.NS_OK,
635 Components.interfaces.calIOperationListener.GET,
636 null,
637 null);
638 } catch (e) {
639 LOG("Error getting item " + aRequest.id + ":\n" + e);
640 aRequest.extraData.listener.onOperationComplete(this.superCalendar,
641 e.result,
642 Components.interfaces.calIOperationListener.GET,
643 null,
644 e.message);
645 }
646 },
647
648 /**
649 * getItems_response
650 * Response callback, called by the session object when an Item feed was
651 * downloaded.
652 *
653 * @param aRequest The request object that initiated the request
654 * @param aStatus The response code. This is a Components.results.* Code
655 * @param aResult In case of an error, this is the error string, otherwise
656 * an XML feed with the requested items.
657 */
658 getItems_response: function cGC_getItems_response(aRequest,
659 aStatus,
660 aResult) {
661 LOG("Recieved response for " + aRequest.uri);
662 try {
663 // Check if the call succeeded
664 if (aStatus != Components.results.NS_OK) {
665 throw new Components.Exception(aResult, aStatus);
666 }
667
668 // Prepare Namespaces
669 var gCal = new Namespace("gCal",
670 "http://schemas.google.com/gCal/2005");
671 var gd = new Namespace("gd", "http://schemas.google.com/g/2005");
672 var atom = new Namespace("", "http://www.w3.org/2005/Atom");
673 default xml namespace = atom;
674
675 // A feed was passed back, parse it. Due to bug 336551 we need to
676 // filter out the <?xml...?> part.
677 var xml = new XML(aResult.substring(38));
678 var timezoneString = xml.gCal::timezone.@value.toString() || "UTC";
679 var timezone = gdataTimezoneProvider.getTimezone(timezoneString);
680
681 // This line is needed, otherwise the for each () block will never
682 // be entered. It may seem strange, but if you don't believe me, try
683 // it!
684 xml.link.(@rel);
685
686 // We might be able to get the full name through this feed's author
687 // tags. We need to make sure we have a session for that.
688 if (!this.mSession) {
689 this.findSession();
690 }
691
692 if (xml.author.email == this.mSession.googleUser) {
693 this.mSession.googleFullName = xml.author.name.toString();
694 }
695
696 // If this is a synchronization run (i.e updated-min was passed to
697 // google, then we also have a calendar to replay the changes on.
698 var destinationCal = aRequest.extraData.destination;
699
700 // Parse all <entry> tags
701 for each (var entry in xml.entry) {
702 if (entry.gd::originalEvent.toString()) {
703 // This is an exception. It will be parsed later so skip it
704 // for now.
705 // XXX this may be possible to filter via e4x, I just
706 // haven't found out how
707 continue;
708 }
709 LOG("Parsing entry:\n" + entry + "\n");
710
711 var item = XMLEntryToItem(entry, timezone, this.superCalendar);
712 if (item.status == "CANCELED") {
713 if (destinationCal) {
714 // When synchronizing, a "CANCELED" item is a deleted
715 // event. Delete it from the destination calendar.
716 destinationCal.deleteItem(item, null);
717 }
718 continue;
719 }
720
721 var expandedItems;
722 item.calendar = this.superCalendar;
723 if (item.recurrenceInfo) {
724 // This is a recurring item. It may have exceptions. Go
725 // through all items that have this event as an original
726 // event.
727 for each (var oid in xml.entry.gd::originalEvent.(@id == item.id)) {
728
729 // Parse the exception and modify the current item
730 var excItem = XMLEntryToItem(oid.parent(),
731 timezone,
732 this);
733 if (excItem) {
734 // Google uses the status field to reflect negative
735 // exceptions.
736 if (excItem.status == "CANCELED") {
737 item.recurrenceInfo.removeOccurrenceAt(excItem.recurrenceId);
738 } else {
739 excItem.calendar = this;
740 excItem.parentItem = item;
741 item.recurrenceInfo.modifyException(excItem);
742 }
743 }
744 }
745 }
746
747 item.makeImmutable();
748 LOGitem(item);
749 if (aRequest.extraData.itemfilter &
750 Components.interfaces.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) {
751 var start = aRequest.extraData.rangeStart;
752 var end = aRequest.extraData.rangeEnd;
753 expandedItems = item.getOccurrencesBetween(start, end, {});
754
755 LOG("Expanded item " + item.title + " to " +
756 expandedItems.length + " items");
757 } else {
758 expandedItems = [item];
759 }
760
761 if (destinationCal) {
762 // When synchronizing, instead of reporting to a listener,
763 // we must just modify the item on the destination calendar.
764 // Since relaxed mode is set on the destination calendar, we
765 // can just call modifyItem, which will also handle
766 // additions correctly.
767 for each (var item in expandedItems) {
768 destinationCal.modifyItem(item, null, null);
769 }
770 } else if (aRequest.extraData.listener) {
771 // Otherwise, this in an uncached getItems call, notify the
772 // listener that we got a result, but only if we actually
773 // have a listener
774 aRequest.extraData.listener.onGetResult(this.superCalendar,
775 Components.results.NS_OK,
776 Components.interfaces.calIEvent,
777 null,
778 expandedItems.length,
779 expandedItems);
780 }
781 }
782
783 // Operation Completed successfully.
784 if (aRequest.extraData.listener instanceof Components.interfaces.calIOperationListener) {
785 aRequest.extraData.listener.onOperationComplete(this.superCalendar,
786 Components.results.NS_OK,
787 Components.interfaces.calIOperationListener.GET,
788 null,
789 null);
790 } else if (aRequest.extraData.listener instanceof Components.interfaces.calIGenericOperationListener) {
791 // The listener for synchronization is a
792 // calIGenericOperationListener. Call accordingly.
793 aRequest.extraData.listener.onResult(aRequest, null);
794
795 // Set the last updated timestamp to now.
796 LOG("Last sync date for " + this.name + " is now: " + aRequest.requestDate.toString());
797
798 this.setProperty("google.lastUpdated",
799 aRequest.requestDate.icalString);
800 }
801 } catch (e) {
802 LOG("Error getting items:\n" + e);
803 // Operation failed
804 if (aRequest.extraData.listener instanceof Components.interfaces.calIOperationListener) {
805 aRequest.extraData.listener.onOperationComplete(this.superCalendar,
806 e.result,
807 Components.interfaces.calIOperationListener.GET,
808 null,
809 e.message);
810 } else if (aRequest.extraData.listener instanceof Components.interfaces.calIGenericOperationListener) {
811 aRequest.extraData.listener.onResult({ status: e.result},
812 e.message);
813 }
814 }
815 },
816
817 /**
818 * general_response
819 * Handles common actions for multiple response types. This does not notify
820 * observers.
821 *
822 * @param aOperation The operation type (Components.interfaces.calIOperationListener.*)
823 * @param aItemString The string represenation of the item
824 * @param aStatus The response code. This is a Components.results.*
825 * error code
826 * @param aListener The listener to be called on completion
827 * (an instance of calIOperationListener)
828 * @param aReferenceItem The item to apply the information from the xml
829 * to. If null, a new item will be used.
830 *
831 * @return The Item as a calIEvent
832 */
833 general_response: function cGC_general_response(aOperation,
834 aItemString,
835 aStatus,
836 aListener,
837 aReferenceItem) {
838
839 try {
840 // Check if the call succeeded, if not then aItemString is an error
841 // message
842
843 if (aStatus != Components.results.NS_OK) {
844 throw new Components.Exception(aItemString, aStatus);
845 }
846
847 // An Item was passed back, parse it. Due to bug 336551 we need to
848 // filter out the <?xml...?> part.
849 var xml = new XML(aItemString.substring(38));
850
851 // Get the local timezone from the preferences
852 var timezone = calendarDefaultTimezone();
853
854 // Parse the Item with the given timezone
855 var item = XMLEntryToItem(xml,
856 timezone,
857 this.superCalendar,
858 aReferenceItem);
859
860 LOGitem(item);
861 item.calendar = this.superCalendar;
862
863 // GET operations need to call onGetResult
864 if (aOperation == Components.interfaces.calIOperationListener.GET) {
865 aListener.onGetResult(this.superCalendar,
866 Components.results.NS_OK,
867 Components.interfaces.calIEvent,
868 null,
869 1,
870 [item]);
871 }
872
873 // All operations need to call onOperationComplete
874 if (aListener) {
875 aListener.onOperationComplete(this.superCalendar,
876 Components.results.NS_OK,
877 aOperation,
878 (item ? item.id : null),
879 item);
880 }
881 return item;
882 } catch (e) {
883 LOG("General response failed: " + e);
884
885 if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) {
886 // The calendar is readonly, make sure this is set and
887 // notify the user.
888 this.readOnly = true;
889 this.mObservers.notify("onError", [e.result, e.message]);
890 }
891
892 // Operation failed
893 if (aListener) {
894 aListener.onOperationComplete(this.superCalendar,
895 e.result,
896 aOperation,
897 null,
898 e.message);
899 }
900 }
901 // Returning null to avoid js strict warning.
902 return null;
903 },
904
905 /**
906 * Implement calIChangeLog
907 */
908 resetLog: function cGC_resetLog() {
909 this.deleteProperty("google.lastUpdated");
910 },
911
912 replayChangesOn: function cGC_replayChangesOn(aDestination, aListener) {
913 var extraData = {
914 destination: aDestination,
915 listener: aListener
916 };
917
918 var lastUpdate = this.getProperty("google.lastUpdated");
919 var lastUpdateDateTime;
920 if (lastUpdate) {
921 // Set up the last sync stamp
922 lastUpdateDateTime = createDateTime();
923 lastUpdateDateTime.icalString = lastUpdate;
924
925 // Set up last week
926 var lastWeek = getCorrectedDate(now().getInTimezone(UTC()));
927 lastWeek.day -= 7;
928 if (lastWeek.compare(lastUpdateDateTime) >= 0) {
929 // The last sync was longer than a week ago. Google requires a full
930 // sync in that case. This call also takes care of calling
931 // resetLog().
932 this.superCalendar.wrappedJSObject.setupCachedCalendar();
933 lastUpdateDateTime = null;
934 }
935 LOG("The calendar " + this.name + " was last modified: " + lastUpdateDateTime);
936
937 }
938
939 // Calling getItems with the aLastModified parameter causes a
940 // synchronization to occur. If the property was wiped out above, then
941 // having destination in the extraData will still add items to the
942 // cached calendar.
943 return this.mSession.getItems(this,
944 null,
945 null,
946 null,
947 false,
948 this.getItems_response,
949 extraData,
950 lastUpdateDateTime);
951 }
952 };