/* 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/. */ var EXPORTED_SYMBOLS = ["calCachedCalendar"]; var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); var calICalendar = Ci.calICalendar; var cICL = Ci.calIChangeLog; var cIOL = Ci.calIOperationListener; var gNoOpListener = { QueryInterface: ChromeUtils.generateQI(["calIOperationListener"]), onGetResult(calendar, status, itemType, detail, items) {}, onOperationComplete(calendar, status, opType, id, detail) {}, }; /** * Returns true if the exception passed is one that should cause the cache * layer to retry the operation. This is usually a network error or other * temporary error. * * @param result The result code to check. * @returns True, if the result code means server unavailability. */ function isUnavailableCode(result) { // Stolen from nserror.h const NS_ERROR_MODULE_NETWORK = 6; function NS_ERROR_GET_MODULE(code) { return ((code >> 16) - 0x45) & 0x1fff; } if (NS_ERROR_GET_MODULE(result) == NS_ERROR_MODULE_NETWORK && !Components.isSuccessCode(result)) { // This is a network error, which most likely means we should // retry it some time. return true; } // Other potential errors we want to retry with switch (result) { case Cr.NS_ERROR_NOT_AVAILABLE: return true; default: return false; } } function calCachedCalendarObserverHelper(home, isCachedObserver) { this.home = home; this.isCachedObserver = isCachedObserver; } calCachedCalendarObserverHelper.prototype = { QueryInterface: ChromeUtils.generateQI(["calIObserver"]), isCachedObserver: false, onStartBatch() { this.home.mObservers.notify("onStartBatch", [this.home]); }, onEndBatch() { this.home.mObservers.notify("onEndBatch", [this.home]); }, async onLoad(calendar) { if (this.isCachedObserver) { this.home.mObservers.notify("onLoad", [this.home]); } else { // start sync action after uncached calendar has been loaded. // xxx todo, think about: // although onAddItem et al have been called, we need to fire // an additional onLoad completing the refresh call (->composite) let home = this.home; await home.synchronize(); home.mObservers.notify("onLoad", [home]); } }, onAddItem(aItem) { if (this.isCachedObserver) { this.home.mObservers.notify("onAddItem", arguments); } }, onModifyItem(aNewItem, aOldItem) { if (this.isCachedObserver) { this.home.mObservers.notify("onModifyItem", arguments); } }, onDeleteItem(aItem) { if (this.isCachedObserver) { this.home.mObservers.notify("onDeleteItem", arguments); } }, onError(aCalendar, aErrNo, aMessage) { this.home.mObservers.notify("onError", arguments); }, onPropertyChanged(aCalendar, aName, aValue, aOldValue) { if (!this.isCachedObserver) { this.home.mObservers.notify("onPropertyChanged", [this.home, aName, aValue, aOldValue]); } }, onPropertyDeleting(aCalendar, aName) { if (!this.isCachedObserver) { this.home.mObservers.notify("onPropertyDeleting", [this.home, aName]); } }, }; function calCachedCalendar(uncachedCalendar) { this.wrappedJSObject = this; this.mSyncQueue = []; this.mObservers = new cal.data.ObserverSet(Ci.calIObserver); uncachedCalendar.superCalendar = this; uncachedCalendar.addObserver(new calCachedCalendarObserverHelper(this, false)); this.mUncachedCalendar = uncachedCalendar; this.setupCachedCalendar(); if (this.supportsChangeLog) { uncachedCalendar.offlineStorage = this.mCachedCalendar; } this.offlineCachedItems = {}; this.offlineCachedItemFlags = {}; } calCachedCalendar.prototype = { /* eslint-disable mozilla/use-chromeutils-generateqi */ QueryInterface(aIID) { if (aIID.equals(Ci.calISchedulingSupport) && this.mUncachedCalendar.QueryInterface(aIID)) { // check whether uncached calendar supports it: return this; } else if (aIID.equals(Ci.calICalendar) || aIID.equals(Ci.nsISupports)) { return this; } throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); }, /* eslint-enable mozilla/use-chromeutils-generateqi */ mCachedCalendar: null, mCachedObserver: null, mUncachedCalendar: null, mObservers: null, mSuperCalendar: null, offlineCachedItems: null, offlineCachedItemFlags: null, onCalendarUnregistering() { if (this.mCachedCalendar) { let self = this; this.mCachedCalendar.removeObserver(this.mCachedObserver); // TODO put changes into a different calendar and delete // afterwards. let listener = { onDeleteCalendar(aCalendar, aStatus, aDetail) { self.mCachedCalendar = null; }, }; this.mCachedCalendar .QueryInterface(Ci.calICalendarProvider) .deleteCalendar(this.mCachedCalendar, listener); } }, setupCachedCalendar() { try { if (this.mCachedCalendar) { // this is actually a resetupCachedCalendar: // Although this doesn't really follow the spec, we know the // storage calendar's deleteCalendar method is synchronous. // TODO put changes into a different calendar and delete // afterwards. this.mCachedCalendar .QueryInterface(Ci.calICalendarProvider) .deleteCalendar(this.mCachedCalendar, null); if (this.supportsChangeLog) { // start with full sync: this.mUncachedCalendar.resetLog(); } } else { let calType = Services.prefs.getStringPref("calendar.cache.type", "storage"); // While technically, the above deleteCalendar should delete the // whole calendar, this is nothing more than deleting all events // todos and properties. Therefore the initialization can be // skipped. let cachedCalendar = Cc["@mozilla.org/calendar/calendar;1?type=" + calType].createInstance( Ci.calICalendar ); switch (calType) { case "memory": { if (this.supportsChangeLog) { // start with full sync: this.mUncachedCalendar.resetLog(); } break; } case "storage": { let file = cal.provider.getCalendarDirectory(); file.append("cache.sqlite"); cachedCalendar.uri = Services.io.newFileURI(file); cachedCalendar.id = this.id; break; } default: { throw new Error("unsupported cache calendar type: " + calType); } } cachedCalendar.transientProperties = true; // Forward the disabled property to the storage calendar so that it // stops interacting with the file system. Other properties have no // useful effect on the storage calendar, so don't forward them. cachedCalendar.setProperty("disabled", this.getProperty("disabled")); cachedCalendar.setProperty("relaxedMode", true); cachedCalendar.superCalendar = this; if (!this.mCachedObserver) { this.mCachedObserver = new calCachedCalendarObserverHelper(this, true); } cachedCalendar.addObserver(this.mCachedObserver); this.mCachedCalendar = cachedCalendar; } } catch (exc) { console.error(exc); } }, async getOfflineAddedItems() { this.offlineCachedItems = {}; for await (let items of cal.iterate.streamValues( this.mCachedCalendar.getItems( calICalendar.ITEM_FILTER_ALL_ITEMS | calICalendar.ITEM_FILTER_OFFLINE_CREATED, 0, null, null ) )) { for (let item of items) { this.offlineCachedItems[item.hashId] = item; this.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_CREATED_RECORD; } } }, async getOfflineModifiedItems() { for await (let items of cal.iterate.streamValues( this.mCachedCalendar.getItems( calICalendar.ITEM_FILTER_OFFLINE_MODIFIED | calICalendar.ITEM_FILTER_ALL_ITEMS, 0, null, null ) )) { for (let item of items) { this.offlineCachedItems[item.hashId] = item; this.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_MODIFIED_RECORD; } } }, async getOfflineDeletedItems() { for await (let items of cal.iterate.streamValues( this.mCachedCalendar.getItems( calICalendar.ITEM_FILTER_OFFLINE_DELETED | calICalendar.ITEM_FILTER_ALL_ITEMS, 0, null, null ) )) { for (let item of items) { this.offlineCachedItems[item.hashId] = item; this.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_DELETED_RECORD; } } }, mPendingSync: null, async synchronize() { if (!this.mPendingSync) { this.mPendingSync = this._doSynchronize().catch(console.error); } return this.mPendingSync; }, async _doSynchronize() { let clearPending = () => { this.mPendingSync = null; }; if (this.getProperty("disabled")) { clearPending(); return; } if (this.offline) { clearPending(); return; } if (this.supportsChangeLog) { await new Promise((resolve, reject) => { let spec = this.uri.spec; cal.LOG("[calCachedCalendar] Doing changelog based sync for calendar " + spec); let opListener = { onResult(operation, result) { if (!operation || !operation.isPending) { let status = operation ? operation.status : Cr.NS_OK; clearPending(); if (!Components.isSuccessCode(status)) { reject( "[calCachedCalendar] replay action failed: " + (operation && operation.id ? operation.id : "") + ", uri=" + spec + ", result=" + result + ", operation=" + operation ); return; } cal.LOG("[calCachedCalendar] replayChangesOn finished."); resolve(); } }, }; this.mUncachedCalendar.replayChangesOn(opListener); }); return; } cal.LOG("[calCachedCalendar] Doing full sync for calendar " + this.uri.spec); await this.getOfflineAddedItems(); await this.getOfflineModifiedItems(); await this.getOfflineDeletedItems(); // TODO instead of deleting the calendar and creating a new // one, maybe we want to do a "real" sync between the // existing local calendar and the remote calendar. this.setupCachedCalendar(); let modifiedTimes = {}; try { for await (let items of cal.iterate.streamValues( this.mUncachedCalendar.getItems(Ci.calICalendar.ITEM_FILTER_ALL_ITEMS, 0, null, null) )) { for (let item of items) { // Adding items recd from the Memory Calendar // These may be different than what the cache has modifiedTimes[item.id] = item.lastModifiedTime; this.mCachedCalendar.addItem(item); } } } catch (e) { await this.playbackOfflineItems(); this.mCachedObserver.onLoad(this.mCachedCalendar); clearPending(); throw e; // Do not swallow this error. } await new Promise((resolve, reject) => { cal.iterate.forEach( this.offlineCachedItems, item => { switch (this.offlineCachedItemFlags[item.hashId]) { case cICL.OFFLINE_FLAG_CREATED_RECORD: // Created items are not present on the server, so its safe to adopt them this.adoptOfflineItem(item.clone()); break; case cICL.OFFLINE_FLAG_MODIFIED_RECORD: // Two Cases Here: if (item.id in modifiedTimes) { // The item is still on the server, we just retrieved it in the listener above. if (item.lastModifiedTime.compare(modifiedTimes[item.id]) < 0) { // The item on the server has been modified, ask to overwrite cal.WARN( "[calCachedCalendar] Item '" + item.title + "' at the server seems to be modified recently." ); this.promptOverwrite("modify", item, null); } else { // Our item is newer, just modify the item this.modifyOfflineItem(item, null); } } else { // The item has been deleted from the server, ask if it should be added again cal.WARN( "[calCachedCalendar] Item '" + item.title + "' has been deleted from the server" ); if (cal.provider.promptOverwrite("modify", item, null)) { this.adoptOfflineItem(item.clone()); } } break; case cICL.OFFLINE_FLAG_DELETED_RECORD: if (item.id in modifiedTimes) { // The item seems to exist on the server... if (item.lastModifiedTime.compare(modifiedTimes[item.id]) < 0) { // ...and has been modified on the server. Ask to overwrite cal.WARN( "[calCachedCalendar] Item '" + item.title + "' at the server seems to be modified recently." ); this.promptOverwrite("delete", item, null); } else { // ...and has not been modified. Delete it now. this.deleteOfflineItem(item); } } else { // Item has already been deleted from the server, no need to change anything. } break; } }, async () => { this.offlineCachedItems = {}; this.offlineCachedItemFlags = {}; await this.playbackOfflineItems(); clearPending(); resolve(); } ); }); }, onOfflineStatusChanged(aNewState) { if (aNewState) { // Going offline: (XXX get items before going offline?) => we may ask the user to stay online a bit longer } else if (!this.getProperty("disabled") && this.getProperty("refreshInterval") != "0") { // Going online (start replaying changes to the remote calendar). // Don't do this if the calendar is disabled or set to manual updates only. this.refresh(); } }, // aOldItem is already in the cache async promptOverwrite(aMethod, aItem, aOldItem) { let overwrite = cal.provider.promptOverwrite(aMethod, aItem); if (overwrite) { if (aMethod == "modify") { await this.modifyOfflineItem(aItem, aOldItem); } else { await this.deleteOfflineItem(aItem); } } }, /* * Asynchronously performs playback operations of items added, modified, or deleted offline * * @param aPlaybackType (optional) The starting operation type. This function will be * called recursively through playback operations in the order of * add, modify, delete. By default playback will start with the add * operation. Valid values for this parameter are defined as * OFFLINE_FLAG_XXX constants in the calIChangeLog interface. */ async playbackOfflineItems(aPlaybackType) { let self = this; let storage = this.mCachedCalendar.QueryInterface(Ci.calIOfflineStorage); let itemQueue = []; let debugOp; let nextCallback; let uncachedOp; let filter; aPlaybackType = aPlaybackType || cICL.OFFLINE_FLAG_CREATED_RECORD; switch (aPlaybackType) { case cICL.OFFLINE_FLAG_CREATED_RECORD: debugOp = "add"; nextCallback = this.playbackOfflineItems.bind(this, cICL.OFFLINE_FLAG_MODIFIED_RECORD); uncachedOp = item => this.mUncachedCalendar.addItem(item); filter = calICalendar.ITEM_FILTER_OFFLINE_CREATED; break; case cICL.OFFLINE_FLAG_MODIFIED_RECORD: debugOp = "modify"; nextCallback = this.playbackOfflineItems.bind(this, cICL.OFFLINE_FLAG_DELETED_RECORD); uncachedOp = item => this.mUncachedCalendar.modifyItem(item, item); filter = calICalendar.ITEM_FILTER_OFFLINE_MODIFIED; break; case cICL.OFFLINE_FLAG_DELETED_RECORD: debugOp = "delete"; uncachedOp = item => this.mUncachedCalendar.deleteItem(item); filter = calICalendar.ITEM_FILTER_OFFLINE_DELETED; break; default: cal.ERROR("[calCachedCalendar] Invalid playback type: " + aPlaybackType); return; } async function popItemQueue() { if (!itemQueue || itemQueue.length == 0) { // no items left in the queue, move on to the next operation if (nextCallback) { await nextCallback(); } } else { // perform operation on the next offline item in the queue let item = itemQueue.pop(); let error = null; try { await uncachedOp(item); } catch (e) { error = e; cal.ERROR( "[calCachedCalendar] Could not perform playback operation " + debugOp + " for item " + (item.title || " (none) ") + ": " + e ); } if (!error) { if (aPlaybackType == cICL.OFFLINE_FLAG_DELETED_RECORD) { self.mCachedCalendar.deleteItem(item); } else { storage.resetItemOfflineFlag(item); } } else { // If the playback action could not be performed, then there // is no need for further action. The item still has the // offline flag, so it will be taken care of next time. cal.WARN( "[calCachedCalendar] Unable to perform playback action " + debugOp + " to the server, will try again next time (" + item.id + "," + error + ")" ); } // move on to the next item in the queue await popItemQueue(); } } itemQueue = itemQueue.concat( await this.mCachedCalendar.getItemsAsArray( calICalendar.ITEM_FILTER_ALL_ITEMS | filter, 0, null, null ) ); if (this.offline) { cal.LOG("[calCachedCalendar] back to offline mode, reconciliation aborted"); } else { cal.LOG( "[calCachedCalendar] Performing playback operation " + debugOp + " on " + itemQueue.length + " items to " + self.name ); // start the first operation await popItemQueue(); } }, get superCalendar() { return (this.mSuperCalendar && this.mSuperCalendar.superCalendar) || this; }, set superCalendar(val) { this.mSuperCalendar = val; }, get offline() { return Services.io.offline; }, get supportsChangeLog() { return cal.wrapInstance(this.mUncachedCalendar, Ci.calIChangeLog) != null; }, get canRefresh() { // enable triggering sync using the reload button return true; }, get supportsScheduling() { return this.mUncachedCalendar.supportsScheduling; }, getSchedulingSupport() { return this.mUncachedCalendar.getSchedulingSupport(); }, getProperty(aName) { switch (aName) { case "cache.enabled": if (this.mUncachedCalendar.getProperty("cache.always")) { return true; } break; } return this.mUncachedCalendar.getProperty(aName); }, setProperty(aName, aValue) { if (aName == "disabled") { // Forward the disabled property to the storage calendar so that it // stops interacting with the file system. Other properties have no // useful effect on the storage calendar, so don't forward them. this.mCachedCalendar.setProperty(aName, aValue); } this.mUncachedCalendar.setProperty(aName, aValue); }, async refresh() { if (this.offline) { this.downstreamRefresh(); } else if (this.supportsChangeLog) { /* we first ensure that any remaining offline items are reconciled with the calendar server */ await this.playbackOfflineItems(); await this.downstreamRefresh(); } else { this.downstreamRefresh(); } }, async downstreamRefresh() { if (this.mUncachedCalendar.canRefresh && !this.offline) { this.mUncachedCalendar.refresh(); // will trigger synchronize once the calendar is loaded return; } await this.synchronize(); // fire completing onLoad for this refresh call this.mCachedObserver.onLoad(this.mCachedCalendar); }, addObserver(aObserver) { this.mObservers.add(aObserver); }, removeObserver(aObserver) { this.mObservers.delete(aObserver); }, async addItem(item) { return this.adoptItem(item.clone()); }, async adoptItem(item) { return new Promise((resolve, reject) => { this.doAdoptItem(item, (calendar, status, opType, id, detail) => { if (!Components.isSuccessCode(status)) { return reject(new Components.Exception(detail, status)); } return resolve(detail); }); }); }, /** * The function form of calIOperationListener.onOperationComplete used where * the whole interface is not needed. * * @callback OnOperationCompleteHandler * * @param {calICalendar} calendar * @param {number} status * @param {number} operationType * @param {string} id * @param {calIItem|Error} detail */ /** * Keeps track of pending callbacks injected into the uncached calendar during * adopt or modify operations. This is done to ensure we remove the correct * callback when multiple operations occur at once. * * @type {OnOperationComplateHandler[]} */ _injectedCallbacks: [], /** * Executes the actual addition of the item using either the cached or uncached * calendar depending on offline state. A separate method is used here to * preserve the order of the "onAddItem" event. * * @param {calIItem} item * @param {OnOperationCompleteHandler} handler */ doAdoptItem(item, listener) { // Forwarding add/modify/delete to the cached calendar using the calIObserver // callbacks would be advantageous, because the uncached provider could implement // a true push mechanism firing without being triggered from within the program. // But this would mean the uncached provider fires on the passed // calIOperationListener, e.g. *before* it fires on calIObservers // (because that order is undefined). Firing onOperationComplete before onAddItem et al // would result in this facade firing onOperationComplete even though the modification // hasn't yet been performed on the cached calendar (which happens in onAddItem et al). // Result is that we currently stick to firing onOperationComplete if the cached calendar // has performed the modification, see below: let onSuccess = item => listener(item.calendar, Cr.NS_OK, cIOL.ADD, item.id, item); let onError = e => listener(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e); if (this.offline) { // If we are offline, don't even try to add the item this.adoptOfflineItem(item).then(onSuccess, onError); } else { // Otherwise ask the provider to add the item now. // Expected to be called in the context of the uncached calendar's adoptItem() // so this adoptItem() call returns first. This is a needed hack to keep the // cached calendar's "onAddItem" event firing before the endBatch() call of // the uncached calendar. let adoptItemCallback = async (calendar, status, opType, id, detail) => { if (isUnavailableCode(status)) { // The item couldn't be added to the (remote) location, // this is like being offline. Add the item to the cached // calendar instead. cal.LOG( "[calCachedCalendar] Calendar " + calendar.name + " is unavailable, adding item offline" ); await this.adoptOfflineItem(item).then(onSuccess, onError); } else if (Components.isSuccessCode(status)) { // On success, add the item to the cache. await this.mCachedCalendar.addItem(detail).then(onSuccess, onError); } else { // Either an error occurred or this is a successful add // to a cached calendar. Forward the call to the listener listener(this, status, opType, id, detail); } this.mUncachedCalendar.wrappedJSObject._cachedAdoptItemCallback = null; this._injectedCallbacks = this._injectedCallbacks.filter(cb => cb != adoptItemCallback); }; // Store the callback so we can remove the correct one later. this._injectedCallbacks.push(adoptItemCallback); this.mUncachedCalendar.wrappedJSObject._cachedAdoptItemCallback = adoptItemCallback; this.mUncachedCalendar.adoptItem(item).catch(e => { adoptItemCallback(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e); }); } }, /** * Adds an item to the cached (storage) calendar. * * @param {calIItem} item * @returns {calIItem} */ async adoptOfflineItem(item) { let adoptedItem = await this.mCachedCalendar.adoptItem(item); await this.mCachedCalendar.QueryInterface(Ci.calIOfflineStorage).addOfflineItem(adoptedItem); return adoptedItem; }, async modifyItem(newItem, oldItem) { return new Promise((resolve, reject) => { this.doModifyItem(newItem, oldItem, (calendar, status, opType, id, detail) => { if (!Components.isSuccessCode(status)) { return reject(new Components.Exception(detail, status)); } return resolve(detail); }); }); }, /** * Executes the actual modification of the item using either the cached or * uncached calendar depending on offline state. A separate method is used here * to preserve the order of the "onModifyItem" event. * * @param {calIItem} newItem * @param {calIItem} oldItem * @param {OnOperationCompleteHandler} handler */ doModifyItem(newItem, oldItem, listener) { let onSuccess = item => listener(item.calendar, Cr.NS_OK, cIOL.MODIFY, item.id, item); let onError = e => listener(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e); // Forwarding add/modify/delete to the cached calendar using the calIObserver // callbacks would be advantageous, because the uncached provider could implement // a true push mechanism firing without being triggered from within the program. // But this would mean the uncached provider fires on the passed // calIOperationListener, e.g. *before* it fires on calIObservers // (because that order is undefined). Firing onOperationComplete before onAddItem et al // would result in this facade firing onOperationComplete even though the modification // hasn't yet been performed on the cached calendar (which happens in onAddItem et al). // Result is that we currently stick to firing onOperationComplete if the cached calendar // has performed the modification, see below: */ // Expected to be called in the context of the uncached calendar's modifyItem() // so this modifyItem() call returns first. This is a needed hack to keep the // cached calendar's "onModifyItem" event firing before the endBatch() call of // the uncached calendar. let modifyItemCallback = async (calendar, status, opType, id, detail) => { // Returned Promise only available through wrappedJSObject. if (isUnavailableCode(status)) { // The item couldn't be modified at the (remote) location, // this is like being offline. Add the item to the cache // instead. cal.LOG( "[calCachedCalendar] Calendar " + calendar.name + " is unavailable, modifying item offline" ); await this.modifyOfflineItem(newItem, oldItem).then(onSuccess, onError); } else if (Components.isSuccessCode(status)) { // On success, modify the item in the cache await this.mCachedCalendar.modifyItem(detail, oldItem).then(onSuccess, onError); } else { // This happens on error, forward the error through the listener listener(this, status, opType, id, detail); } this._injectedCallbacks = this._injectedCallbacks.filter(cb => cb != modifyItemCallback); }; // First of all, we should find out if the item to modify is // already an offline item or not. if (this.offline) { // If we are offline, don't even try to modify the item this.modifyOfflineItem(newItem, oldItem).then(onSuccess, onError); } else { // Otherwise, get the item flags and further process the item. this.mCachedCalendar.getItemOfflineFlag(oldItem).then(offline_flag => { if ( offline_flag == cICL.OFFLINE_FLAG_CREATED_RECORD || offline_flag == cICL.OFFLINE_FLAG_MODIFIED_RECORD ) { // The item is already offline, just modify it in the cache this.modifyOfflineItem(newItem, oldItem).then(onSuccess, onError); } else { // Not an offline item, attempt to modify using provider // This is a needed hack to keep the cached calendar's "onModifyItem" event // firing before the endBatch() call of the uncached calendar. It is called // in mUncachedCalendar's modifyItem() method. this.mUncachedCalendar.wrappedJSObject._cachedModifyItemCallback = modifyItemCallback; // Store the callback so we can remove the correct one later. this._injectedCallbacks.push(modifyItemCallback); this.mUncachedCalendar.modifyItem(newItem, oldItem).catch(e => { modifyItemCallback(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e); }); } }); } }, /** * Modifies an item in the cached calendar. * * @param {calIItem} newItem * @param {calIItem} oldItem * @returns {calIItem} */ async modifyOfflineItem(newItem, oldItem) { let modifiedItem = await this.mCachedCalendar.modifyItem(newItem, oldItem); await this.mCachedCalendar .QueryInterface(Ci.calIOfflineStorage) .modifyOfflineItem(modifiedItem); return modifiedItem; }, async deleteItem(item) { // First of all, we should find out if the item to delete is // already an offline item or not. if (this.offline) { // If we are offline, don't even try to delete the item await this.deleteOfflineItem(item); } else { // Otherwise, get the item flags, the listener will further // process the item. let offline_flag = await this.mCachedCalendar.getItemOfflineFlag(item); if ( offline_flag == cICL.OFFLINE_FLAG_CREATED_RECORD || offline_flag == cICL.OFFLINE_FLAG_MODIFIED_RECORD ) { // The item is already offline, just mark it deleted it in // the cache await this.deleteOfflineItem(item); } else { try { // Not an offline item, attempt to delete using provider await this.mUncachedCalendar.deleteItem(item); // On success, delete the item from the cache await this.mCachedCalendar.deleteItem(item); // Also, remove any meta data associated with the item try { this.mCachedCalendar.QueryInterface(Ci.calISyncWriteCalendar).deleteMetaData(item.id); } catch (e) { cal.LOG("[calCachedCalendar] Offline storage doesn't support metadata"); } } catch (e) { if (isUnavailableCode(e.result)) { // The item couldn't be deleted at the (remote) location, // this is like being offline. Mark the item deleted in the // cache instead. cal.LOG( "[calCachedCalendar] Calendar " + item.calendar.name + " is unavailable, deleting item offline" ); await this.deleteOfflineItem(item); } } } } }, async deleteOfflineItem(item) { /* We do not delete the item from the cache, as we will need it when reconciling the cache content and the server content. */ return this.mCachedCalendar.QueryInterface(Ci.calIOfflineStorage).deleteOfflineItem(item); }, }; (function () { function defineForwards(proto, targetName, functions, getters, gettersAndSetters) { function defineForwardGetter(attr) { proto.__defineGetter__(attr, function () { return this[targetName][attr]; }); } function defineForwardGetterAndSetter(attr) { defineForwardGetter(attr); proto.__defineSetter__(attr, function (value) { return (this[targetName][attr] = value); }); } function defineForwardFunction(funcName) { proto[funcName] = function (...args) { let obj = this[targetName]; return obj[funcName](...args); }; } functions.forEach(defineForwardFunction); getters.forEach(defineForwardGetter); gettersAndSetters.forEach(defineForwardGetterAndSetter); } defineForwards( calCachedCalendar.prototype, "mUncachedCalendar", ["deleteProperty", "isInvitation", "getInvitedAttendee", "canNotify"], ["providerID", "type", "aclManager", "aclEntry"], ["id", "name", "uri", "readOnly"] ); defineForwards( calCachedCalendar.prototype, "mCachedCalendar", ["getItem", "getItems", "getItemsAsArray", "startBatch", "endBatch"], [], [] ); })();