diff options
Diffstat (limited to 'comm/calendar/providers/storage/CalStorageCalendar.jsm')
-rw-r--r-- | comm/calendar/providers/storage/CalStorageCalendar.jsm | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/comm/calendar/providers/storage/CalStorageCalendar.jsm b/comm/calendar/providers/storage/CalStorageCalendar.jsm new file mode 100644 index 0000000000..5d330986b6 --- /dev/null +++ b/comm/calendar/providers/storage/CalStorageCalendar.jsm @@ -0,0 +1,563 @@ +/* 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/. */ + +const EXPORTED_SYMBOLS = ["CalStorageCalendar"]; + +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +const { CalReadableStreamFactory } = ChromeUtils.import( + "resource:///modules/CalReadableStreamFactory.jsm" +); +const { CalStorageDatabase } = ChromeUtils.import( + "resource:///modules/calendar/CalStorageDatabase.jsm" +); +const { CalStorageModelFactory } = ChromeUtils.import( + "resource:///modules/calendar/CalStorageModelFactory.jsm" +); +const { CalStorageStatements } = ChromeUtils.import( + "resource:///modules/calendar/CalStorageStatements.jsm" +); +const { upgradeDB } = ChromeUtils.import("resource:///modules/calendar/calStorageUpgrade.jsm"); + +const kCalICalendar = Ci.calICalendar; +const cICL = Ci.calIChangeLog; + +function CalStorageCalendar() { + this.initProviderBase(); +} +var calStorageCalendarClassID = Components.ID("{b3eaa1c4-5dfe-4c0a-b62a-b3a514218461}"); +var calStorageCalendarInterfaces = [ + "calICalendar", + "calICalendarProvider", + "calIOfflineStorage", + "calISchedulingSupport", + "calISyncWriteCalendar", +]; +CalStorageCalendar.prototype = { + __proto__: cal.provider.BaseClass.prototype, + classID: calStorageCalendarClassID, + QueryInterface: cal.generateQI(calStorageCalendarInterfaces), + classInfo: cal.generateCI({ + classID: calStorageCalendarClassID, + contractID: "@mozilla.org/calendar/calendar;1?type=storage", + classDescription: "Calendar Storage Provider", + interfaces: calStorageCalendarInterfaces, + }), + + // + // private members + // + mStorageDb: null, + mItemModel: null, + mOfflineModel: null, + mMetaModel: null, + + // + // calICalendarProvider interface + // + + get displayName() { + return cal.l10n.getCalString("storageName"); + }, + + get shortName() { + return "SQLite"; + }, + + async deleteCalendar(aCalendar, listener) { + await this.mItemModel.deleteCalendar(); + try { + if (listener) { + listener.onDeleteCalendar(aCalendar, Cr.NS_OK, null); + } + } catch (ex) { + this.mStorageDb.logError("error calling listener.onDeleteCalendar", ex); + } + }, + + detectCalendars() { + throw Components.Exception( + "calStorageCalendar does not implement detectCalendars", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + }, + + mRelaxedMode: undefined, + get relaxedMode() { + if (this.mRelaxedMode === undefined) { + this.mRelaxedMode = this.getProperty("relaxedMode"); + } + return this.mRelaxedMode; + }, + + // + // calICalendar interface + // + + getProperty(aName) { + switch (aName) { + case "cache.supported": + return false; + case "requiresNetwork": + return false; + case "capabilities.priority.supported": + return true; + case "capabilities.removeModes": + return ["delete"]; + } + return this.__proto__.__proto__.getProperty.apply(this, arguments); + }, + + get supportsScheduling() { + return true; + }, + + getSchedulingSupport() { + return this; + }, + + // readonly attribute AUTF8String type; + get type() { + return "storage"; + }, + + // attribute AUTF8String id; + get id() { + return this.__proto__.__proto__.__lookupGetter__("id").call(this); + }, + set id(val) { + this.__proto__.__proto__.__lookupSetter__("id").call(this, val); + + if (!this.mStorageDb && this.uri && this.id) { + // Prepare the database as soon as we have an id and an uri. + this.prepareInitDB(); + } + }, + + // attribute nsIURI uri; + get uri() { + return this.__proto__.__proto__.__lookupGetter__("uri").call(this); + }, + set uri(aUri) { + // We can only load once + if (this.uri) { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + + this.__proto__.__proto__.__lookupSetter__("uri").call(this, aUri); + + if (!this.mStorageDb && this.uri && this.id) { + // Prepare the database as soon as we have an id and an uri. + this.prepareInitDB(); + } + }, + + // attribute mozIStorageAsyncConnection db; + get db() { + return this.mStorageDb.db; + }, + + /** + * Initialize the Database. This should generally only be called from the + * uri or id setter and requires those two attributes to be set. It may also + * be called again when the schema version of the database is newer than + * the version expected by this version of Thunderbird. + */ + prepareInitDB() { + this.mStorageDb = CalStorageDatabase.connect(this.uri, this.id); + upgradeDB(this); + }, + + afterUpgradeDB() { + this.initDB(); + Services.obs.addObserver(this, "profile-change-teardown"); + }, + + observe(aSubject, aTopic, aData) { + if (aTopic == "profile-change-teardown") { + Services.obs.removeObserver(this, "profile-change-teardown"); + // Finalize the storage statements, but don't close the database. + // CalStorageDatabase.jsm will take care of that while blocking profile-before-change. + this.mStatements?.finalize(); + } + }, + + refresh() { + // no-op + }, + + // Promise<calIItemBase> addItem(in calIItemBase aItem); + async addItem(aItem) { + let newItem = aItem.clone(); + return this.adoptItem(newItem); + }, + + // Promise<calIItemBase> adoptItem(in calIItemBase aItem); + async adoptItem(aItem) { + let onError = async (message, exception) => { + this.notifyOperationComplete( + null, + exception, + Ci.calIOperationListener.ADD, + aItem.id, + message + ); + return Promise.reject(new Components.Exception(message, exception)); + }; + + if (this.readOnly) { + return onError("Calendar is readonly", Ci.calIErrors.CAL_IS_READONLY); + } + + if (aItem.id == null) { + // is this an error? Or should we generate an IID? + aItem.id = cal.getUUID(); + } else { + let olditem = await this.mItemModel.getItemById(aItem.id); + if (olditem) { + if (this.relaxedMode) { + // we possibly want to interact with the user before deleting + await this.mItemModel.deleteItemById(aItem.id, true); + } else { + return onError("ID already exists for addItem", Ci.calIErrors.DUPLICATE_ID); + } + } + } + + let parentItem = aItem.parentItem; + if (parentItem != aItem) { + parentItem = parentItem.clone(); + parentItem.recurrenceInfo.modifyException(aItem, true); + } + parentItem.calendar = this.superCalendar; + parentItem.makeImmutable(); + + await this.mItemModel.addItem(parentItem); + + // notify observers + this.observers.notify("onAddItem", [aItem]); + return aItem; + }, + + // Promise<calIItemBase> modifyItem(in calIItemBase aNewItem, in calIItemBase aOldItem) + async modifyItem(aNewItem, aOldItem) { + // HACK Just modifying the item would clear the offline flag, we need to + // retrieve the flag and pass it to the real modify function. + let offlineFlag = await this.getItemOfflineFlag(aOldItem); + let oldOfflineFlag = offlineFlag; + + let reportError = (errStr, errId = Cr.NS_ERROR_FAILURE) => { + this.notifyOperationComplete( + null, + errId, + Ci.calIOperationListener.MODIFY, + aNewItem.id, + errStr + ); + return Promise.reject(new Components.Exception(errStr, errId)); + }; + + if (this.readOnly) { + return reportError("Calendar is readonly", Ci.calIErrors.CAL_IS_READONLY); + } + if (!aNewItem) { + return reportError("A modified version of the item is required", Cr.NS_ERROR_INVALID_ARG); + } + if (aNewItem.id == null) { + // this is definitely an error + return reportError("ID for modifyItem item is null"); + } + + let modifiedItem = aNewItem.parentItem.clone(); + if (this.getProperty("capabilities.propagate-sequence")) { + // Ensure the exception, its parent and the other exceptions have the + // same sequence number, to make sure we can send our changes to the + // server if the event has been updated via the blue bar + let newSequence = aNewItem.getProperty("SEQUENCE"); + this._propagateSequence(modifiedItem, newSequence); + } + + // Ensure that we're looking at the base item if we were given an + // occurrence. Later we can optimize this. + if (aNewItem.parentItem != aNewItem) { + modifiedItem.recurrenceInfo.modifyException(aNewItem, false); + } + + // If no old item was passed, then we should overwrite in any case. + // Pick up the old item from the database and use this as an old item + // later on. + if (!aOldItem) { + aOldItem = await this.mItemModel.getItemById(aNewItem.id); + } + + if (this.relaxedMode) { + // We've already filled in the old item above, if this doesn't exist + // then just take the current item as its old version + if (!aOldItem) { + aOldItem = aNewItem; + } + aOldItem = aOldItem.parentItem; + } else { + let storedOldItem = null; + if (aOldItem) { + storedOldItem = await this.mItemModel.getItemById(aOldItem.id); + } + if (!aOldItem || !storedOldItem) { + // no old item found? should be using addItem, then. + return reportError("ID does not already exist for modifyItem"); + } + aOldItem = aOldItem.parentItem; + + if (aOldItem.generation != storedOldItem.generation) { + return reportError("generation too old for for modifyItem"); + } + + // xxx todo: this only modified master item's generation properties + // I start asking myself why we need a separate X-MOZ-GENERATION. + // Just for the sake of checking inconsistencies of modifyItem calls? + if (aOldItem.generation == modifiedItem.generation) { + // has been cloned and modified + // Only take care of incrementing the generation if relaxed mode is + // off. Users of relaxed mode need to take care of this themselves. + modifiedItem.generation += 1; + } + } + + modifiedItem.makeImmutable(); + await this.mItemModel.updateItem(modifiedItem, aOldItem); + await this.mOfflineModel.setOfflineJournalFlag(aNewItem, oldOfflineFlag); + + this.notifyOperationComplete( + null, + Cr.NS_OK, + Ci.calIOperationListener.MODIFY, + modifiedItem.id, + modifiedItem + ); + + // notify observers + this.observers.notify("onModifyItem", [modifiedItem, aOldItem]); + return modifiedItem; + }, + + // Promise<void> deleteItem(in calIItemBase item) + async deleteItem(item) { + let onError = async (message, exception) => { + this.notifyOperationComplete( + null, + exception, + Ci.calIOperationListener.DELETE, + item.id, + message + ); + return Promise.reject(new Components.Exception(message, exception)); + }; + + if (this.readOnly) { + return onError("Calendar is readonly", Ci.calIErrors.CAL_IS_READONLY); + } + + if (item.parentItem != item) { + item.parentItem.recurrenceInfo.removeExceptionFor(item.recurrenceId); + // xxx todo: would we want to support this case? Removing an occurrence currently results + // in a modifyItem(parent) + return null; + } + + if (item.id == null) { + return onError("ID is null for deleteItem", Cr.NS_ERROR_FAILURE); + } + + await this.mItemModel.deleteItemById(item.id); + + this.notifyOperationComplete(null, Cr.NS_OK, Ci.calIOperationListener.DELETE, item.id, item); + + // notify observers + this.observers.notify("onDeleteItem", [item]); + return null; + }, + + // Promise<calIItemBase|null> getItem(in string id); + async getItem(aId) { + return this.mItemModel.getItemById(aId); + }, + + // ReadableStream<calIItemBase> getItems(in unsigned long itemFilter, + // in unsigned long count, + // in calIDateTime rangeStart, + // in calIDateTime rangeEnd); + getItems(itemFilter, count, rangeStart, rangeEnd) { + let query = { + rangeStart, + rangeEnd, + filters: { + wantUnrespondedInvitations: + (itemFilter & kCalICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION) != 0 && + this.superCalendar.supportsScheduling, + wantEvents: (itemFilter & kCalICalendar.ITEM_FILTER_TYPE_EVENT) != 0, + wantTodos: (itemFilter & kCalICalendar.ITEM_FILTER_TYPE_TODO) != 0, + asOccurrences: (itemFilter & kCalICalendar.ITEM_FILTER_CLASS_OCCURRENCES) != 0, + wantOfflineDeletedItems: (itemFilter & kCalICalendar.ITEM_FILTER_OFFLINE_DELETED) != 0, + wantOfflineCreatedItems: (itemFilter & kCalICalendar.ITEM_FILTER_OFFLINE_CREATED) != 0, + wantOfflineModifiedItems: (itemFilter & kCalICalendar.ITEM_FILTER_OFFLINE_MODIFIED) != 0, + itemCompletedFilter: (itemFilter & kCalICalendar.ITEM_FILTER_COMPLETED_YES) != 0, + itemNotCompletedFilter: (itemFilter & kCalICalendar.ITEM_FILTER_COMPLETED_NO) != 0, + }, + count, + }; + + if ((!query.filters.wantEvents && !query.filters.wantTodos) || this.getProperty("disabled")) { + // nothing to do + return CalReadableStreamFactory.createEmptyReadableStream(); + } + + return this.mItemModel.getItems(query); + }, + + async getItemOfflineFlag(aItem) { + // It is possible that aItem can be null, flag provided should be null in this case + return aItem ? this.mOfflineModel.getItemOfflineFlag(aItem) : null; + }, + + // + // calIOfflineStorage interface + // + async addOfflineItem(aItem) { + let newOfflineJournalFlag = cICL.OFFLINE_FLAG_CREATED_RECORD; + await this.mOfflineModel.setOfflineJournalFlag(aItem, newOfflineJournalFlag); + }, + + async modifyOfflineItem(aItem) { + let oldOfflineJournalFlag = await this.getItemOfflineFlag(aItem); + let newOfflineJournalFlag = cICL.OFFLINE_FLAG_MODIFIED_RECORD; + if ( + oldOfflineJournalFlag == cICL.OFFLINE_FLAG_CREATED_RECORD || + oldOfflineJournalFlag == cICL.OFFLINE_FLAG_DELETED_RECORD + ) { + // Do nothing since a flag of "created" or "deleted" exists + } else { + await this.mOfflineModel.setOfflineJournalFlag(aItem, newOfflineJournalFlag); + } + this.notifyOperationComplete(null, Cr.NS_OK, Ci.calIOperationListener.MODIFY, aItem.id, aItem); + }, + + async deleteOfflineItem(aItem) { + let oldOfflineJournalFlag = await this.getItemOfflineFlag(aItem); + if (oldOfflineJournalFlag) { + // Delete item if flag is set + if (oldOfflineJournalFlag == cICL.OFFLINE_FLAG_CREATED_RECORD) { + await this.mItemModel.deleteItemById(aItem.id); + } else if (oldOfflineJournalFlag == cICL.OFFLINE_FLAG_MODIFIED_RECORD) { + await this.mOfflineModel.setOfflineJournalFlag(aItem, cICL.OFFLINE_FLAG_DELETED_RECORD); + } + } else { + await this.mOfflineModel.setOfflineJournalFlag(aItem, cICL.OFFLINE_FLAG_DELETED_RECORD); + } + + // notify observers + this.observers.notify("onDeleteItem", [aItem]); + }, + + async resetItemOfflineFlag(aItem) { + await this.mOfflineModel.setOfflineJournalFlag(aItem, null); + }, + + // + // database handling + // + + // database initialization + // assumes this.mStorageDb is valid + + initDB() { + cal.ASSERT(this.mStorageDb, "Database has not been opened!", true); + + try { + this.mStorageDb.executeSimpleSQL("PRAGMA journal_mode=WAL"); + this.mStorageDb.executeSimpleSQL("PRAGMA cache_size=-10240"); // 10 MiB + this.mStatements = new CalStorageStatements(this.mStorageDb); + this.mItemModel = CalStorageModelFactory.createInstance( + "cached-item", + this.mStorageDb, + this.mStatements, + this + ); + this.mOfflineModel = CalStorageModelFactory.createInstance( + "offline", + this.mStorageDb, + this.mStatements, + this + ); + this.mMetaModel = CalStorageModelFactory.createInstance( + "metadata", + this.mStorageDb, + this.mStatements, + this + ); + } catch (e) { + this.mStorageDb.logError("Error initializing statements.", e); + } + }, + + async shutdownDB() { + try { + this.mStatements.finalize(); + if (this.mStorageDb) { + await this.mStorageDb.close(); + this.mStorageDb = null; + } + } catch (e) { + cal.ERROR("Error closing storage database: " + e); + } + }, + + // + // calISyncWriteCalendar interface + // + + setMetaData(id, value) { + this.mMetaModel.deleteMetaDataById(id); + this.mMetaModel.addMetaData(id, value); + }, + + deleteMetaData(id) { + this.mMetaModel.deleteMetaDataById(id); + }, + + getMetaData(id) { + return this.mMetaModel.getMetaData(id); + }, + + getAllMetaDataIds() { + return this.mMetaModel.getAllMetaData("item_id"); + }, + + getAllMetaDataValues() { + return this.mMetaModel.getAllMetaData("value"); + }, + + /** + * propagate the given sequence in exceptions. It may be needed by some calendar implementations + */ + _propagateSequence(aItem, newSequence) { + if (newSequence) { + aItem.setProperty("SEQUENCE", newSequence); + } else { + aItem.deleteProperty("SEQUENCE"); + } + let rec = aItem.recurrenceInfo; + if (rec) { + let exceptions = rec.getExceptionIds(); + if (exceptions.length > 0) { + for (let exid of exceptions) { + let ex = rec.getExceptionFor(exid); + if (newSequence) { + ex.setProperty("SEQUENCE", newSequence); + } else { + ex.deleteProperty("SEQUENCE"); + } + } + } + } + }, +}; |