summaryrefslogtreecommitdiff
path: root/calendar/providers/gdata/components/calGoogleCalendar.js
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2022-03-26 20:18:05 -0500
committerMatt A. Tobin <email@mattatobin.com>2022-03-26 20:18:05 -0500
commitc3dc8a1f81c2148a64bc99a194da4c10614e9b95 (patch)
tree6915845b08018db4ee37f09a7a8ea9b4c17ebb27 /calendar/providers/gdata/components/calGoogleCalendar.js
parentc0d30f29a0a1d418442c9dc05c83ac6ef2921d15 (diff)
downloadaura-central-c3dc8a1f81c2148a64bc99a194da4c10614e9b95.tar.gz
Manually re-add calendar
Diffstat (limited to 'calendar/providers/gdata/components/calGoogleCalendar.js')
-rw-r--r--calendar/providers/gdata/components/calGoogleCalendar.js822
1 files changed, 822 insertions, 0 deletions
diff --git a/calendar/providers/gdata/components/calGoogleCalendar.js b/calendar/providers/gdata/components/calGoogleCalendar.js
new file mode 100644
index 000000000..ccef338ec
--- /dev/null
+++ b/calendar/providers/gdata/components/calGoogleCalendar.js
@@ -0,0 +1,822 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gdata-provider/modules/shim/Loader.jsm").shimIt(this);
+Components.utils.import("resource://gdata-provider/modules/shim/Calendar.jsm");
+Components.utils.import("resource://gdata-provider/modules/shim/PromiseExtras.jsm");
+
+CuImport("resource://gre/modules/Preferences.jsm", this);
+CuImport("resource://gre/modules/Promise.jsm", this);
+CuImport("resource://gre/modules/PromiseUtils.jsm", this);
+CuImport("resource://gre/modules/Services.jsm", this);
+CuImport("resource://gre/modules/Task.jsm", this);
+CuImport("resource://gre/modules/XPCOMUtils.jsm", this);
+
+CuImport("resource://calendar/modules/calProviderUtils.jsm", this);
+CuImport("resource://calendar/modules/calUtils.jsm", this);
+
+CuImport("resource://gdata-provider/modules/gdataLogging.jsm", this);
+CuImport("resource://gdata-provider/modules/gdataRequest.jsm", this);
+CuImport("resource://gdata-provider/modules/gdataSession.jsm", this);
+CuImport("resource://gdata-provider/modules/gdataUtils.jsm", this);
+
+var cICL = Components.interfaces.calIChangeLog;
+var cIOL = Components.interfaces.calIOperationListener;
+
+var MIN_REFRESH_INTERVAL = 30;
+
+/**
+ * calGoogleCalendar
+ * This Implements a calICalendar Object adapted to the Google Calendar
+ * Provider.
+ *
+ * @class
+ * @constructor
+ */
+function calGoogleCalendar() {
+ this.initProviderBase();
+ this.mThrottle = Object.create(null);
+}
+
+var calGoogleCalendarClassID = Components.ID("{d1a6e988-4b4d-45a5-ba46-43e501ea96e3}");
+var calGoogleCalendarInterfaces = [
+ Components.interfaces.calICalendar,
+ Components.interfaces.calISchedulingSupport,
+ Components.interfaces.calIChangeLog
+];
+calGoogleCalendar.prototype = {
+ __proto__: cal.ProviderBase.prototype,
+
+ classID: calGoogleCalendarClassID,
+ QueryInterface: XPCOMUtils.generateQI(calGoogleCalendarInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classDescription: "Google Calendar Provider",
+ contractID: "@mozilla.org/calendar/calendar;1?type=gdata",
+ classID: calGoogleCalendarClassID,
+ interfaces: calGoogleCalendarInterfaces
+ }),
+
+ /* Used to reset the local cache between releases */
+ CACHE_DB_VERSION: 3,
+
+ /* Member Variables */
+ mCalendarName: null,
+ mThrottle: null,
+ mThrottleLimits: {
+ "calendarList": 3600 * 1000,
+ "events": 30 * 1000,
+ "tasks": 30 * 1000
+ },
+
+ /* Public Members */
+ session: null,
+
+ /**
+ * Make sure a session is available.
+ */
+ ensureSession: function() {
+ if (!this.session) {
+ // Now actually set up the session
+ let sessionMgr = getGoogleSessionManager();
+ this.session = sessionMgr.getSessionByCalendar(this, true);
+
+ // Aside from setting up the session, bump the refresh interval to
+ // a higher value if its below the minimal refresh interval to
+ // avoid exceeding quota.
+ let interval = this.getProperty("refreshInterval");
+ if (interval < MIN_REFRESH_INTERVAL && interval != 0) {
+ cal.LOG("[calGoogleCalendar] Sorry, auto-refresh intervals under " + MIN_REFRESH_INTERVAL + " minutes would cause the quota to be reached too fast.");
+ this.setProperty("refreshInterval", 2 * MIN_REFRESH_INTERVAL);
+ }
+ }
+ },
+
+ ensureWritable: function() {
+ // Check if calendar is readonly
+ if (this.readOnly) {
+ const cIE = Components.interfaces.calIErrors;
+ throw new Components.Exception("", cIE.CAL_IS_READONLY);
+ }
+ },
+
+ get isDefaultCalendar() { return this.mCalendarName ? !this.mCalendarName.endsWith("@group.calendar.google.com") : false; },
+
+ /*
+ * implement calICalendar
+ */
+ get type() { return "gdata"; },
+ get providerID() { return "{a62ef8ec-5fdc-40c2-873c-223b8a6925cc}"; },
+ get canRefresh() { return true; },
+
+ get id() { return this.mID; },
+ set id(val) {
+ let setter = this.__proto__.__proto__.__lookupSetter__("id");
+ val = setter.call(this, val);
+
+ if (this.id && this.uri) {
+ this.ensureSession();
+ }
+ return val;
+ },
+
+ get uri() { return this.mUri; },
+ set uri(aUri) {
+ const protocols = ["http", "https", "webcal", "webcals"];
+ this.mUri = aUri;
+ if (aUri && aUri.schemeIs("googleapi")) {
+ // new format: googleapi://session-id/?calendar=calhash@group.calendar.google.com&tasks=taskhash
+ let [fullUser, path] = aUri.path.substr(2).split("/", 2);
+ let parameters = new Map(path.substr(1).split("&").filter(Boolean)
+ .map(function(x) { return x.split("=", 2).map(decodeURIComponent); }));
+
+ if (parameters.size == 0) {
+ this.mCalendarName = fullUser;
+ this.mTasklistName = this.isDefaultCalendar ? "@default" : null;
+ } else {
+ this.mCalendarName = parameters.get("calendar");
+ this.mTasklistName = parameters.get("tasks");
+ }
+
+ // Users that installed 1.0 had an issue where secondary calendars
+ // were migrated to their own session. This code fixes that and
+ // should be removed once 1.0.1 has been out for a while.
+ let googleUser = Preferences.get("calendar.google.calPrefs." + fullUser + ".googleUser");
+ if (googleUser && googleUser != fullUser) {
+ let newUri = "googleapi://" + googleUser + "/" + path;
+ cal.LOG("[calGoogleCalendar] Migrating url format from " + aUri.spec + " to " + newUri);
+ this.setProperty("uri", newUri);
+ this.mUri = Services.io.newURI(newUri, null, null);
+ }
+
+ // Unit tests will use a local uri, if the magic parameter is passed.
+ let port = parameters.get("testport");
+ if (port) {
+ cal.LOG("[calGoogleCalendar] Redirecting request to test port " + port);
+ API_BASE.EVENTS = "http://localhost:" + port + "/calendar/v3/";
+ API_BASE.TASKS = "http://localhost:" + port + "/tasks/v1/";
+ }
+ } else if (aUri && protocols.some(function(scheme) { return aUri.schemeIs(scheme); })) {
+ // Parse google url, catch private cookies, public calendars,
+ // basic and full types, bogus ics file extensions, invalid hostnames
+ let re = new RegExp("/calendar/(feeds|ical)/" +
+ "([^/]+)/(public|private|free-busy)-?([^/]+)?/" +
+ "(full|basic)(.ics)?$");
+
+ let matches = aUri.path.match(re);
+ if (matches) {
+ this.mCalendarName = decodeURIComponent(matches[2]);
+
+ let googleUser = Preferences.get("calendar.google.calPrefs." + this.mCalendarName + ".googleUser");
+ let newUri = "googleapi://" + (googleUser || this.mCalendarName) + "/?calendar=" + matches[2];
+
+ // Use the default task list, but only if this is the primary account.
+ if (googleUser && googleUser == this.mCalendarName) {
+ this.mTasklistName = "@default";
+ newUri += "&tasks=%40default";
+ }
+
+ cal.LOG("[calGoogleCalendar] Migrating url format from " + aUri.spec +
+ " to " + newUri);
+ this.setProperty("uri", newUri);
+ this.mUri = Services.io.newURI(newUri, null, null);
+ }
+ }
+
+ if (this.id && this.uri) {
+ this.ensureSession();
+ }
+
+ return this.mUri;
+ },
+
+ createEventsURI: function (/* ...extraParts */) {
+ let extraParts = Array.slice(arguments);
+ let eventsURI = null;
+ if (this.mCalendarName) {
+ let encodedName = encodeURIComponent(this.mCalendarName);
+ let parts = ["calendars", encodedName].concat(Array.filter(extraParts, Boolean));
+ eventsURI = API_BASE.EVENTS + parts.join("/");
+ }
+ return eventsURI;
+ },
+
+ createUsersURI: function(/* ...extraParts */) {
+ let extraParts = Array.slice(arguments);
+ let parts = ["users", "me"].concat(extraParts).map(encodeURIComponent);
+ return API_BASE.EVENTS + parts.join("/");
+ },
+
+ createTasksURI: function(/* ...extraParts */) {
+ let extraParts = Array.slice(arguments);
+ let tasksURI = null;
+ if (this.mTasklistName) {
+ let encodedName = encodeURIComponent(this.mTasklistName);
+ let parts = ["lists", encodedName].concat(Array.filter(extraParts, Boolean));
+ tasksURI = API_BASE.TASKS + parts.join("/");
+ }
+ return tasksURI;
+ },
+
+ getUpdatedMin: function getUpdatedMin(aWhich) {
+ let updatedMin = null;
+ let lastUpdated = this.getProperty("lastUpdated." + aWhich);
+ if (lastUpdated) {
+ updatedMin = cal.createDateTime(lastUpdated);
+ let lastWeek = cal.now();
+ lastWeek.day -= 7;
+ if (updatedMin.compare(lastWeek) <= 0) {
+ cal.LOG("[calGoogleCalendar] Last updated time for " + aWhich +
+ " is very old, doing full sync");
+ this.resetLog();
+ updatedMin = null;
+ }
+ }
+ return updatedMin ? getCorrectedDate(updatedMin) : null;
+ },
+
+ checkThrottle: function(type) {
+ let shouldRequest = true;
+ let now = new Date().getTime();
+
+ if (type in this.mThrottle) {
+ let then = this.mThrottle[type];
+
+ if (now - then < this.mThrottleLimits[type]) {
+ shouldRequest = false;
+ }
+ }
+
+ if (shouldRequest) {
+ this.mThrottle[type] = now;
+ } else {
+ cal.LOG("[calGoogleCalendar] Skipping " + type + " request to reduce requests");
+ }
+
+ return shouldRequest;
+ },
+
+ getProperty: function(aName) {
+ switch (aName) {
+ case "googleCalendarName":
+ return this.mCalendarName;
+ case "isDefaultCalendar":
+ return this.isDefaultCalendar;
+
+ // Capabilities
+ case "cache.enabled":
+ case "cache.always":
+ return true;
+ case "capabilities.timezones.floating.supported":
+ case "capabilities.attachments.supported":
+ case "capabilities.priority.supported":
+ return false;
+ case "capabilities.privacy.values":
+ return ["DEFAULT", "PUBLIC", "PRIVATE"];
+ case "capabilities.alarms.maxCount":
+ return 5;
+ case "capabilities.alarms.actionValues":
+ let actionValues = ["DISPLAY", "EMAIL"];
+ if (Preferences.get("calendar.google.enableSMSReminders", false)) {
+ actionValues.push("SMS");
+ }
+ return actionValues;
+ case "capabilities.tasks.supported":
+ return !!this.mTasklistName;
+ case "capabilities.events.supported":
+ return !!this.mCalendarName;
+ case "readOnly":
+ // If this calendar displays events, make it readonly if we are
+ // not the owner or have write access.
+ let accessRole = this.getProperty("settings.accessRole");
+ let isReader = (accessRole == "freeBusyReader" || accessRole == "reader");
+ if (this.mCalendarName && isReader) {
+ return true;
+ }
+ // Otherwise fall through
+ break;
+ case "organizerId":
+ return "mailto:" + this.mCalendarName;
+ case "itip.transport":
+ if (!this.isDefaultCalendar ||
+ !Preferences.get("calendar.google.enableEmailInvitations", false)) {
+ // If we explicitly return null here, then these calendars
+ // will not be included in the list of calendars to accept
+ // invitations to and imip will effectively be disabled.
+ return null;
+ }
+ break;
+ case "imip.identity.disabled":
+ // Disabling this hides the picker for identities in the new
+ // calendar wizard and calendar properties dialog. This should
+ // be done for all secondary calendars as they cannot accept
+ // invitations and if email invitations are generally disabled.
+ if (!this.isDefaultCalendar ||
+ !Preferences.get("calendar.google.enableEmailInvitations", false)) {
+ return true;
+ }
+ break;
+ }
+
+ return this.__proto__.__proto__.getProperty.apply(this, arguments);
+ },
+
+ setProperty: function(aName, aValue) {
+ switch (aName) {
+ case "refreshInterval":
+ if (aValue < MIN_REFRESH_INTERVAL && aValue != 0) {
+ cal.LOG("[calGoogleCalendar] Sorry, auto-refresh intervals under " +
+ MIN_REFRESH_INTERVAL + " minutes would cause the quota " +
+ "to be reached too fast.");
+ this.superCalendar.setProperty("refreshInterval", 2 * MIN_REFRESH_INTERVAL);
+ return;
+ }
+ break;
+ }
+
+ return this.__proto__.__proto__.setProperty.apply(this, arguments);
+ },
+
+ addItemOrUseCache: calendarShim.addItemOrUseCache,
+ adoptItemOrUseCache: calendarShim.adoptItemOrUseCache,
+ modifyItemOrUseCache: calendarShim.modifyItemOrUseCache,
+ deleteItemOrUseCache: calendarShim.deleteItemOrUseCache,
+ notifyPureOperationComplete: calendarShim.notifyPureOperationComplete,
+
+ addItem: function(aItem, aListener) { return this.adoptItem(aItem.clone(), aListener); },
+ adoptItem: function(aItem, aListener) {
+ function stackContains(part, max) {
+ if (max === undefined) max = 8;
+ let stack = Components.stack.caller;
+ while (stack && --max) {
+ if (stack.filename && stack.filename.endsWith(part)) {
+ return true;
+ }
+ stack = stack.caller;
+ }
+ return false;
+ }
+
+ // Now this sucks...both invitations and the offline cache send over
+ // items with the id set, but we have no way to figure out which is
+ // happening just by inspecting the item. Adding offline items should
+ // not be an import, but invitations should.
+ let isImport = aItem.id && (aItem.id == "xpcshell-import" || stackContains("calItipUtils.jsm"));
+ let request = new calGoogleRequest();
+
+ Task.spawn(function() {
+ let itemData = ItemToJSON(aItem, this.offlineStorage, isImport);
+
+ // Add the calendar to the item, for later use.
+ aItem.calendar = this.superCalendar;
+
+ request.type = request.ADD;
+ request.calendar = this;
+ if (cal.isEvent(aItem)) {
+ if (isImport) {
+ cal.LOG("[calGoogleCalendar] Adding invitation event " + aItem.title);
+ request.uri = this.createEventsURI("events", "import");
+ } else {
+ cal.LOG("[calGoogleCalendar] Adding regular event " + aItem.title);
+ request.uri = this.createEventsURI("events");
+ }
+
+ if (Preferences.get("calendar.google.sendEventNotifications", false)) {
+ request.addQueryParameter("sendNotifications", "true");
+ }
+ } else if (cal.isToDo(aItem)) {
+ cal.LOG("[calGoogleCalendar] Adding task " + aItem.title);
+ request.uri = this.createTasksURI("tasks");
+ // Tasks sent with an id will cause a bad request
+ delete itemData.id;
+ }
+
+ if (!request.uri) {
+ throw Components.Exception("Item type not supported",
+ Components.results.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ request.setUploadData("application/json; charset=UTF-8",
+ JSON.stringify(itemData));
+ let data = yield this.session.asyncItemRequest(request);
+
+ // All we need to do now is parse the item and complete the
+ // operation. The cache layer will take care of adding the item
+ // to the storage.
+ let metaData = Object.create(null);
+ let item = JSONToItem(data, this, this.defaultReminders || [],
+ null, metaData);
+
+ // Make sure to update the etag and id
+ saveItemMetadata(this.offlineStorage, item.hashId, metaData);
+
+ if (aItem.id && item.id != aItem.id) {
+ // Looks like the id changed, probably because its an offline
+ // item. This really sucks for us now, because the cache will
+ // reset the wrong item. As a hack, delete the item with its
+ // original id and complete the adoptItem call with the new
+ // item. This will add the new item to the calendar.
+ let pcal = promisifyCalendar(this.offlineStorage);
+ yield pcal.deleteItem(aItem);
+ }
+ throw new Task.Result(item);
+ }.bind(this)).then(function(item) {
+ cal.LOG("[calGoogleCalendar] Adding " + item.title + " succeeded");
+ this.observers.notify("onAddItem", [item]);
+ this.notifyOperationComplete(aListener, Components.results.NS_OK,
+ cIOL.ADD, item.id, item);
+ }.bind(this), function(e) {
+ let code = e.result || Components.results.NS_ERROR_FAILURE;
+ cal.ERROR("[calGoogleCalendar] Adding Item " + aItem.title +
+ " failed:" + code + ": " + e.message);
+ this.notifyPureOperationComplete(aListener, code, cIOL.ADD, aItem.id, e.message);
+ }.bind(this));
+ return request;
+ },
+
+ modifyItem: function(aNewItem, aOldItem, aListener) {
+ cal.LOG("[calGoogleCalendar] Modifying item " + aNewItem.title + " (" +
+ (aNewItem.recurrenceId ? aNewItem.recurrenceId.icalString :
+ "master item") + ")");
+
+ // Set up the request
+ let request = new calGoogleRequest();
+ Task.spawn(function() {
+ request.type = request.MODIFY;
+ request.calendar = this;
+ if (cal.isEvent(aNewItem)) {
+ let googleId = getGoogleId(aNewItem, this.offlineStorage);
+ request.uri = this.createEventsURI("events", googleId);
+
+ // Updating invitations often causes a forbidden error becase
+ // some parts are not writable. Using PATCH ignores anything
+ // that isn't allowed.
+ if (cal.isInvitation(aNewItem)) {
+ request.type = request.PATCH;
+ }
+
+ if (Preferences.get("calendar.google.sendEventNotifications", false)) {
+ request.addQueryParameter("sendNotifications", "true");
+ }
+ } else if (cal.isToDo(aNewItem)) {
+ request.uri = this.createTasksURI("tasks", aNewItem.id);
+ }
+
+ if (!request.uri) {
+ throw Components.Exception("Item type not supported",
+ Components.results.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ request.setUploadData("application/json; charset=UTF-8",
+ JSON.stringify(ItemToJSON(aNewItem, this.offlineStorage)));
+
+ // Set up etag from storage so we don't overwrite any foreign changes
+ let refItem = aOldItem || aNewItem;
+ let meta = getItemMetadata(this.offlineStorage, refItem) ||
+ getItemMetadata(this.offlineStorage, refItem.parentItem);
+ if (meta && meta.etag) {
+ request.addRequestHeader("If-Match", meta.etag);
+ } else {
+ cal.ERROR("[calGoogleCalendar] Missing ETag for " + refItem.hashId);
+ }
+
+ let data;
+ try {
+ data = yield this.session.asyncItemRequest(request);
+ } catch (e) {
+ if (e.result == calGoogleRequest.CONFLICT_MODIFY ||
+ e.result == calGoogleRequest.CONFLICT_DELETED) {
+ data = yield checkResolveConflict(request, this, aNewItem);
+ } else {
+ throw e;
+ }
+ }
+
+ // All we need to do now is parse the item and complete the
+ // operation. The cache layer will take care of adding the item
+ // to the storage cache.
+ let metaData = Object.create(null);
+ let item = JSONToItem(data, this,
+ this.defaultReminders || [],
+ aNewItem.clone(), metaData);
+
+ // Make sure to update the etag. Do so before switching to the
+ // parent item, as google saves its own etags for changed
+ // instances.
+ migrateItemMetadata(this.offlineStorage, aOldItem, item, metaData);
+
+ if (item.recurrenceId) {
+ // If we only modified an exception item, then we need to
+ // set the parent item and modify the exception.
+ let modifiedItem = aNewItem.parentItem.clone();
+ if (item.status == "CANCELLED") {
+ // Canceled means the occurrence is an EXDATE.
+ modifiedItem.recurrenceInfo.removeOccurrenceAt(item.recurrenceId);
+ } else {
+ // Not canceled means the occurrence was modified.
+ modifiedItem.recurrenceInfo.modifyException(item, true);
+ }
+ item = modifiedItem;
+ }
+
+ throw new Task.Result(item);
+ }.bind(this)).then(function (item) {
+ cal.LOG("[calGoogleCalendar] Modifying " + aNewItem.title + " succeeded");
+ this.observers.notify("onModifyItem", [item, aOldItem]);
+ this.notifyOperationComplete(aListener, Components.results.NS_OK,
+ cIOL.MODIFY, item.id, item);
+
+ }.bind(this), function(e) {
+ let code = e.result || Components.results.NS_ERROR_FAILURE;
+ if (code != Components.interfaces.calIErrors.OPERATION_CANCELLED) {
+ cal.ERROR("[calGoogleCalendar] Modifying item " + aNewItem.title +
+ " failed:" + code + ": " + e.message);
+ }
+ this.notifyPureOperationComplete(aListener, code, cIOL.MODIFY, aNewItem.id, e.message);
+ }.bind(this));
+ return request;
+ },
+
+ deleteItem: function(aItem, aListener) {
+ cal.LOG("[calGoogleCalendar] Deleting item " + aItem.title + "(" + aItem.id + ")");
+
+ let request = new calGoogleRequest();
+ Task.spawn(function() {
+ request.type = request.DELETE;
+ request.calendar = this;
+ if (cal.isEvent(aItem)) {
+ request.uri = this.createEventsURI("events", getGoogleId(aItem, this.offlineStorage));
+ if (Preferences.get("calendar.google.sendEventNotifications", false)) {
+ request.addQueryParameter("sendNotifications", "true");
+ }
+ } else if (cal.isToDo(aItem)) {
+ request.uri = this.createTasksURI("tasks", aItem.id);
+ }
+
+ if (!request.uri) {
+ throw Components.Exception("Item type not supported",
+ Components.results.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ // Set up etag from storage so we don't overwrite any foreign changes
+ let meta = getItemMetadata(this.offlineStorage, aItem) ||
+ getItemMetadata(this.offlineStorage, aItem.parentItem);
+ if (meta && meta.etag) {
+ request.addRequestHeader("If-Match", meta.etag);
+ } else {
+ cal.ERROR("[calGoogleCalendar] Missing ETag for " + aItem.hashId);
+ }
+
+ try {
+ yield this.session.asyncItemRequest(request);
+ } catch (e) {
+ if (e.result == calGoogleRequest.CONFLICT_MODIFY ||
+ e.result == calGoogleRequest.CONFLICT_DELETED) {
+ yield checkResolveConflict(request, this, aItem);
+ } else {
+ throw e;
+ }
+ }
+
+ deleteItemMetadata(this.offlineStorage, aItem);
+
+ throw new Task.Result(aItem);
+ }.bind(this)).then(function (item) {
+ cal.LOG("[calGoogleCalendar] Deleting " + aItem.title + " succeeded");
+ this.observers.notify("onDeleteItem", [item]);
+ this.notifyOperationComplete(aListener, Components.results.NS_OK,
+ cIOL.DELETE, item.id, item);
+ }.bind(this), function(e) {
+ let code = e.result || Components.results.NS_ERROR_FAILURE;
+ if (code != Components.interfaces.calIErrors.OPERATION_CANCELLED) {
+ cal.ERROR("[calGoogleCalendar] Deleting item " + aItem.title +
+ " failed:" + code + ": " + e.message);
+ }
+ this.notifyPureOperationComplete(aListener, code, cIOL.DELETE, aItem.id, e.message);
+ }.bind(this));
+ return request;
+ },
+
+ getItem: function(aId, aListener) {
+ this.mOfflineStorage.getItem.apply(this.mOfflineStorage, arguments);
+ },
+
+ getItems: function(aFilter, aCount, aRangeStart, aRangeEnd, aListener) {
+ this.mOfflineStorage.getItems.apply(this.mOfflineStorage, arguments);
+ },
+
+ refresh: function() {
+ this.mObservers.notify("onLoad", [this]);
+ },
+
+ migrateStorageCache: function() {
+ let cacheVersion = this.getProperty("cache.version");
+ if (!cacheVersion || cacheVersion >= this.CACHE_DB_VERSION) {
+ // Either up to date or first run, make sure property set right.
+ this.setProperty("cache.version", this.CACHE_DB_VERSION);
+ return Promise.resolve(false);
+ }
+
+ let needsReset = false;
+ cal.LOG("[calGoogleCalendar] Migrating cache from " +
+ cacheVersion + " to " + this.CACHE_DB_VERSION + " for " + this.name);
+
+ if (cacheVersion < 2) {
+ // The initial version 1.0 had some issues that required resetting
+ // the cache.
+ needsReset = true;
+ }
+
+ if (cacheVersion < 3) {
+ // There was an issue with ids from the birthday calendar, we need
+ // to reset just this calendar. See bug 1169062.
+ let birthdayCalendar = "#contacts@group.v.calendar.google.com";
+ if (this.mCalendarName && this.mCalendarName == birthdayCalendar) {
+ needsReset = true;
+ }
+ }
+
+ // Migration all done. Reset if requested.
+ if (needsReset) {
+ return this.resetSync().then(function() {
+ this.setProperty("cache.version", this.CACHE_DB_VERSION);
+ return needsReset;
+ }.bind(this));
+ } else {
+ this.setProperty("cache.version", this.CACHE_DB_VERSION);
+ return Promise.resolve(needsReset);
+ }
+ },
+
+ /**
+ * Implement calIChangeLog
+ */
+ get offlineStorage() { return this.mOfflineStorage; },
+ set offlineStorage(val) {
+ this.mOfflineStorage = val;
+ this.migrateStorageCache();
+ return val;
+ },
+
+ resetLog: function() {
+ this.resetSync().then(function() {
+ this.mObservers.notify("onLoad", [this]);
+ }.bind(this));
+ },
+
+ resetSync: function() {
+ let deferred = PromiseUtils.defer();
+ cal.LOG("[calGoogleCalendar] Resetting last updated counter for " + this.name);
+ this.setProperty("syncToken.events", "");
+ this.setProperty("lastUpdated.tasks", "");
+ this.mThrottle = Object.create(null);
+ this.mOfflineStorage.QueryInterface(Components.interfaces.calICalendarProvider)
+ .deleteCalendar(this.mOfflineStorage, {
+ onDeleteCalendar: function(aCalendar, aStatus, aDetail) {
+ if (Components.isSuccessCode(aStatus)) {
+ deferred.resolve();
+ } else {
+ deferred.reject(aDetail);
+ }
+ }
+ });
+ return deferred.promise;
+ },
+
+ replayChangesOn: function(aListener) {
+ // Figure out if the user is idle, no need to synchronize if so.
+ let idleTime = Components.classes["@mozilla.org/widget/idleservice;1"]
+ .getService(Components.interfaces.nsIIdleService)
+ .idleTime;
+ let maxIdleTime = Preferences.get("calendar.google.idleTime", 300) * 1000;
+
+ if (maxIdleTime != 0 && idleTime > maxIdleTime) {
+ cal.LOG("[calGoogleCalendar] Skipping refresh since user is idle");
+ aListener.onResult({ status: Components.results.NS_OK }, null);
+ return Promise.resolve();
+ }
+
+ // Now that we've determined we are not idle we can continue with the sync.
+ let maxResults = Preferences.get("calendar.google.maxResultsPerRequest", null);
+
+ // We are going to be making potentially lots of changes to the offline
+ // storage, start a batch operation.
+ this.mOfflineStorage.startBatch();
+
+ // Update the calendar settings
+ let calendarPromise = Promise.resolve();
+ if (this.mCalendarName && this.checkThrottle("calendarList")) {
+ let calendarRequest = new calGoogleRequest();
+ calendarRequest.calendar = this;
+ calendarRequest.type = calendarRequest.GET;
+ calendarRequest.uri = this.createUsersURI("calendarList", this.mCalendarName)
+ calendarPromise = this.session.asyncItemRequest(calendarRequest).then(function(aData) {
+ if (aData.defaultReminders) {
+ this.defaultReminders = aData.defaultReminders.map(function(x) { return JSONToAlarm(x, true); });
+ } else {
+ this.defaultReminders = [];
+ }
+
+ for each (let k in ["accessRole", "backgroundColor", "description",
+ "foregroundColor", "location", "primary",
+ "summary", "summaryOverride", "timeZone"]) {
+ this.setProperty("settings." + k, aData[k]);
+ }
+ this.setProperty("settings.defaultReminders", JSON.stringify(aData.defaultReminders));
+ }.bind(this));
+ }
+
+ // Set up a request for the events
+ let eventsRequest = new calGoogleRequest();
+ let eventsPromise = Promise.resolve();
+ eventsRequest.calendar = this;
+ eventsRequest.type = eventsRequest.GET;
+ eventsRequest.uri = this.createEventsURI("events");
+ eventsRequest.addQueryParameter("maxResults", maxResults);
+ let syncToken = this.getProperty("syncToken.events");
+ if (syncToken) {
+ eventsRequest.addQueryParameter("showDeleted", "true");
+ eventsRequest.addQueryParameter("syncToken", syncToken);
+ }
+ if (eventsRequest.uri && this.checkThrottle("events")) {
+ let saver = new ItemSaver(this);
+ eventsPromise = this.session.asyncPaginatedRequest(eventsRequest, null, function(aData) {
+ // On each request...
+ return saver.parseItemStream(aData);
+ }.bind(this), function(aData) {
+ // On last request...
+ return saver.complete().then(function() {
+ if (aData.nextSyncToken) {
+ cal.LOG("[calGoogleCalendar] New sync token for " +
+ this.name + "(events) is now: " + aData.nextSyncToken);
+ this.setProperty("syncToken.events", aData.nextSyncToken);
+ }
+ }.bind(this));
+ }.bind(this));
+ }
+
+ // Set up a request for tasks
+ let tasksRequest = new calGoogleRequest();
+ let tasksPromise = Promise.resolve();
+ tasksRequest.calendar = this;
+ tasksRequest.type = tasksRequest.GET;
+ tasksRequest.uri = this.createTasksURI("tasks");
+ tasksRequest.addQueryParameter("maxResults", maxResults);
+ let lastUpdated = this.getUpdatedMin("tasks");
+ if (lastUpdated) {
+ tasksRequest.addQueryParameter("updatedMin", cal.toRFC3339(lastUpdated));
+ tasksRequest.addQueryParameter("showDeleted", "true");
+ }
+ if (tasksRequest.uri && this.checkThrottle("tasks")) {
+ let saver = new ItemSaver(this);
+ let lastUpdated = null;
+ tasksPromise = this.session.asyncPaginatedRequest(tasksRequest, function(aData) {
+ // On the first request...
+ lastUpdated = tasksRequest.requestDate.icalString;
+ }.bind(this), function(aData) {
+ // On each request...
+ return saver.parseItemStream(aData);
+ }.bind(this), function(aData) {
+ // On last request...
+ return saver.complete().then(function() {
+ cal.LOG("[calGoogleCalendar] Last sync date for " + this.name +
+ "(tasks) is now: " + tasksRequest.requestDate.toString());
+ this.setProperty("lastUpdated.tasks", lastUpdated);
+ }.bind(this));
+ }.bind(this));
+ }
+
+ return PromiseAll([calendarPromise, eventsPromise, tasksPromise]).then(function() {
+ this.mOfflineStorage.endBatch();
+ aListener.onResult({ status: Components.results.NS_OK }, null);
+ }.bind(this), function(e) {
+ this.mOfflineStorage.endBatch();
+ let code = e.result || Components.results.NS_ERROR_FAILURE;
+ if (code == calGoogleRequest.RESOURCE_GONE) {
+ cal.LOG("[calGoogleCalendar] Server did not accept " +
+ "incremental update, resetting calendar and " +
+ "starting over.");
+ this.resetSync().then(function() {
+ this.replayChangesOn(aListener);
+ }.bind(this), function(e) {
+ cal.ERROR("[calGoogleCalendar] Error resetting calendar:\n" +
+ stringException(e));
+ aListener.onResult({ status: e.result }, e.message);
+ }.bind(this));
+ } else {
+ cal.LOG("[calGoogleCalendar] Error syncing:\n" + code + ":" +
+ stringException(e));
+ aListener.onResult({ status: code }, e.message);
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Implement calISchedulingSupport. Most is taken care of by the base
+ * provider, but we want to advertise that we will always take care of
+ * notifications.
+ */
+ canNotify: function(aMethod, aItem) { return true; }
+};
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([calGoogleCalendar]);