summaryrefslogtreecommitdiffstats
path: root/comm/calendar/providers/storage
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/providers/storage
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/calendar/providers/storage')
-rw-r--r--comm/calendar/providers/storage/CalStorageCachedItemModel.jsm219
-rw-r--r--comm/calendar/providers/storage/CalStorageCalendar.jsm563
-rw-r--r--comm/calendar/providers/storage/CalStorageDatabase.jsm333
-rw-r--r--comm/calendar/providers/storage/CalStorageItemModel.jsm1374
-rw-r--r--comm/calendar/providers/storage/CalStorageMetaDataModel.jsm94
-rw-r--r--comm/calendar/providers/storage/CalStorageModelBase.jsm65
-rw-r--r--comm/calendar/providers/storage/CalStorageModelFactory.jsm52
-rw-r--r--comm/calendar/providers/storage/CalStorageOfflineModel.jsm54
-rw-r--r--comm/calendar/providers/storage/CalStorageStatements.jsm751
-rw-r--r--comm/calendar/providers/storage/calStorageHelpers.jsm121
-rw-r--r--comm/calendar/providers/storage/calStorageUpgrade.jsm1889
-rw-r--r--comm/calendar/providers/storage/components.conf14
-rw-r--r--comm/calendar/providers/storage/moz.build28
13 files changed, 5557 insertions, 0 deletions
diff --git a/comm/calendar/providers/storage/CalStorageCachedItemModel.jsm b/comm/calendar/providers/storage/CalStorageCachedItemModel.jsm
new file mode 100644
index 0000000000..80a367f2af
--- /dev/null
+++ b/comm/calendar/providers/storage/CalStorageCachedItemModel.jsm
@@ -0,0 +1,219 @@
+/* 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 = ["CalStorageCachedItemModel"];
+
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+const { CalReadableStreamFactory } = ChromeUtils.import(
+ "resource:///modules/CalReadableStreamFactory.jsm"
+);
+const { CalStorageItemModel } = ChromeUtils.import(
+ "resource:///modules/calendar/CalStorageItemModel.jsm"
+);
+
+/**
+ * CalStorageCachedItemModel extends CalStorageItemModel to add caching support
+ * for items. Most of the methods here are overridden from the parent class to
+ * either add or retrieve items from the cache.
+ */
+class CalStorageCachedItemModel extends CalStorageItemModel {
+ /**
+ * Cache for all items.
+ *
+ * @type {Map<string, calIItemBase>}
+ */
+ itemCache = new Map();
+
+ /**
+ * Cache for recurring events.
+ *
+ * @type {Map<string, calIEvent>}
+ */
+ #recurringEventsCache = new Map();
+
+ /**
+ * Cache for recurring events offline flags.
+ *
+ * @type {Map<string, number>}
+ */
+ #recurringEventsOfflineFlagCache = new Map();
+
+ /**
+ * Cache for recurring todos.
+ *
+ * @type {Map<string, calITodo>}
+ */
+ #recurringTodosCache = new Map();
+
+ /**
+ * Cache for recurring todo offline flags.
+ *
+ * @type {Map<string, number>}
+ */
+ #recurringTodosOfflineCache = new Map();
+
+ /**
+ * Promise that resolves when the caches have been built up.
+ *
+ * @type {Promise<void>}
+ */
+ #recurringCachePromise = null;
+
+ /**
+ * Build up recurring event and todo cache with its offline flags.
+ */
+ async #ensureRecurringItemCaches() {
+ if (!this.#recurringCachePromise) {
+ this.#recurringCachePromise = this.#buildRecurringItemCaches();
+ }
+ return this.#recurringCachePromise;
+ }
+
+ async #buildRecurringItemCaches() {
+ // Retrieve items and flags for recurring events and todos before combining
+ // storing them in the item cache. Items need to be expunged from the
+ // existing item cache to avoid get(Event|Todo)FromRow providing stale
+ // values.
+ let expunge = id => this.itemCache.delete(id);
+ let [events, eventFlags] = await this.getRecurringEventAndFlagMaps(expunge);
+ let [todos, todoFlags] = await this.getRecurringTodoAndFlagMaps(expunge);
+ let itemsMap = await this.getAdditionalDataForItemMap(new Map([...events, ...todos]));
+
+ this.itemCache = new Map([...this.itemCache, ...itemsMap]);
+ this.#recurringEventsCache = new Map([...this.#recurringEventsCache, ...events]);
+ this.#recurringEventsOfflineFlagCache = new Map([
+ ...this.#recurringEventsOfflineFlagCache,
+ ...eventFlags,
+ ]);
+ this.#recurringTodosCache = new Map([...this.#recurringTodosCache, ...todos]);
+ this.#recurringTodosOfflineCache = new Map([...this.#recurringTodosOfflineCache, ...todoFlags]);
+ }
+
+ /**
+ * Overridden here to build the recurring item caches when needed.
+ *
+ * @param {CalStorageQuery} query
+ *
+ * @returns {ReadableStream<calIItemBase>
+ */
+ getItems(query) {
+ let self = this;
+ let getStream = () => super.getItems(query);
+ return CalReadableStreamFactory.createReadableStream({
+ async start(controller) {
+ // HACK because recurring offline events/todos objects don't have offline_journal information
+ // Hence we need to update the offline flags caches.
+ // It can be an expensive operation but is only used in Online Reconciliation mode
+ if (
+ (query.filters.wantOfflineCreatedItems ||
+ query.filters.wantOfflineDeletedItems ||
+ query.filters.wantOfflineModifiedItems) &&
+ self.mRecItemCachePromise
+ ) {
+ // If there's an existing Promise and it's not complete, wait for it - something else is
+ // already waiting and we don't want to break that by throwing away the caches. If it IS
+ // complete, we'll continue immediately.
+ let recItemCachePromise = self.mRecItemCachePromise;
+ await recItemCachePromise;
+ await new Promise(resolve => ChromeUtils.idleDispatch(resolve));
+ // Check in case someone else already threw away the caches.
+ if (self.mRecItemCachePromise == recItemCachePromise) {
+ self.mRecItemCachePromise = null;
+ }
+ }
+ await self.#ensureRecurringItemCaches();
+
+ for await (let value of cal.iterate.streamValues(getStream())) {
+ controller.enqueue(value);
+ }
+ controller.close();
+ },
+ });
+ }
+
+ /**
+ * Overridden here to provide the events from the cache.
+ *
+ * @returns {[Map<string, calIEvent>, Map<string, number>]}
+ */
+ async getFullRecurringEventAndFlagMaps() {
+ return [this.#recurringEventsCache, this.#recurringEventsOfflineFlagCache];
+ }
+
+ /**
+ * Overridden here to provide the todos from the cache.
+ *
+ * @returns {[Map<string, calITodo>, Map<string, number>]}
+ */
+ async getFullRecurringTodoAndFlagMaps() {
+ return [this.#recurringTodosCache, this.#recurringTodosOfflineCache];
+ }
+
+ async getEventFromRow(row, getAdditionalData = true) {
+ let item = this.itemCache.get(row.getResultByName("id"));
+ if (item) {
+ return item;
+ }
+
+ item = await super.getEventFromRow(row, getAdditionalData);
+ if (getAdditionalData) {
+ this.#cacheItem(item);
+ }
+ return item;
+ }
+
+ async getTodoFromRow(row, getAdditionalData = true) {
+ let item = this.itemCache.get(row.getResultByName("id"));
+ if (item) {
+ return item;
+ }
+
+ item = await super.getTodoFromRow(row, getAdditionalData);
+ if (getAdditionalData) {
+ this.#cacheItem(item);
+ }
+ return item;
+ }
+
+ async addItem(item) {
+ await super.addItem(item);
+ this.#cacheItem(item);
+ }
+
+ async getItemById(id) {
+ await this.#ensureRecurringItemCaches();
+ let item = this.itemCache.get(id);
+ if (item) {
+ return item;
+ }
+ return super.getItemById(id);
+ }
+
+ async deleteItemById(id, keepMeta) {
+ await super.deleteItemById(id, keepMeta);
+ this.itemCache.delete(id);
+ this.#recurringEventsCache.delete(id);
+ this.#recurringTodosCache.delete(id);
+ }
+
+ /**
+ * Adds an item to the relevant caches.
+ *
+ * @param {calIItemBase} item
+ */
+ #cacheItem(item) {
+ if (item.recurrenceId) {
+ // Do not cache recurring item instances. See bug 1686466.
+ return;
+ }
+ this.itemCache.set(item.id, item);
+ if (item.recurrenceInfo) {
+ if (item.isEvent()) {
+ this.#recurringEventsCache.set(item.id, item);
+ } else {
+ this.#recurringTodosCache.set(item.id, item);
+ }
+ }
+ }
+}
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");
+ }
+ }
+ }
+ }
+ },
+};
diff --git a/comm/calendar/providers/storage/CalStorageDatabase.jsm b/comm/calendar/providers/storage/CalStorageDatabase.jsm
new file mode 100644
index 0000000000..b4ba1dc2b9
--- /dev/null
+++ b/comm/calendar/providers/storage/CalStorageDatabase.jsm
@@ -0,0 +1,333 @@
+/* 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 = ["CalStorageDatabase"];
+
+const { AsyncShutdown } = ChromeUtils.importESModule(
+ "resource://gre/modules/AsyncShutdown.sys.mjs"
+);
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+let connections = new Map();
+
+/**
+ * Checks for an existing SQLite connection to `file`, or creates a new one.
+ * Calls to `openConnectionTo` and `closeConnection` are counted so we know
+ * if a connection is no longer used.
+ *
+ * @param {nsIFile} file
+ * @returns {mozIStorageConnection}
+ */
+function openConnectionTo(file) {
+ let data = connections.get(file.path);
+
+ if (data) {
+ data.useCount++;
+ return data.connection;
+ }
+
+ let connection = Services.storage.openDatabase(file);
+ connections.set(file.path, { connection, useCount: 1 });
+ return connection;
+}
+
+/**
+ * Closes an SQLite connection if it is no longer in use.
+ *
+ * @param {mozIStorageConnection} connection
+ * @returns {Promise} - resolves when the connection is closed, or immediately
+ * if the database is still in use.
+ */
+function closeConnection(connection, forceClosed) {
+ let file = connection.databaseFile;
+ let data = connections.get(file.path);
+
+ if (forceClosed || !data || --data.useCount == 0) {
+ return new Promise(resolve => {
+ connection.asyncClose({
+ complete() {
+ resolve();
+ },
+ });
+ connections.delete(file.path);
+ });
+ }
+
+ return Promise.resolve();
+}
+
+// Clean up all open databases at shutdown. All storage statements must be closed by now,
+// which CalStorageCalendar does during profile-change-teardown.
+AsyncShutdown.profileBeforeChange.addBlocker("Calendar: closing databases", async () => {
+ let promises = [];
+ for (let data of connections.values()) {
+ promises.push(closeConnection(data.connection, true));
+ }
+ await Promise.allSettled(promises);
+});
+
+/**
+ * CalStorageDatabase is a mozIStorageAsyncConnection wrapper used by the
+ * storage calendar.
+ */
+class CalStorageDatabase {
+ /**
+ * @type {mozIStorageAsyncConnection}
+ */
+ db = null;
+
+ /**
+ * @type {string}
+ */
+ calendarId = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ lastStatement = null;
+
+ /**
+ * @param {mozIStorageAsyncConnection} db
+ * @param {string} calendarId
+ */
+ constructor(db, calendarId) {
+ this.db = db;
+ this.calendarId = calendarId;
+ }
+
+ /**
+ * Initializes a CalStorageDatabase using the provided nsIURI and calendar
+ * id.
+ *
+ * @param {nsIURI} uri
+ * @param {string} calendarId
+ *
+ * @returns {CalStorageDatabase}
+ */
+ static connect(uri, calendarId) {
+ if (uri.schemeIs("file")) {
+ let fileURL = uri.QueryInterface(Ci.nsIFileURL);
+
+ if (!fileURL) {
+ throw new Components.Exception("Invalid file", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+ // open the database
+ return new CalStorageDatabase(openConnectionTo(fileURL.file), calendarId);
+ } else if (uri.schemeIs("moz-storage-calendar")) {
+ // New style uri, no need for migration here
+ let localDB = cal.provider.getCalendarDirectory();
+ localDB.append("local.sqlite");
+
+ if (!localDB.exists()) {
+ // This can happen with a database upgrade and the "too new schema" situation.
+ localDB.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o700);
+ }
+
+ return new CalStorageDatabase(openConnectionTo(localDB), calendarId);
+ }
+ throw new Components.Exception("Invalid Scheme " + uri.spec);
+ }
+
+ /**
+ * Calls the same method on the underlying database connection.
+ *
+ * @param {string} sql
+ *
+ * @returns {mozIStorageAsyncStatement}
+ */
+ createAsyncStatement(sql) {
+ return this.db.createAsyncStatement(sql);
+ }
+
+ /**
+ * Calls the same method on the underlying database connection.
+ *
+ * @param {string} sql
+ *
+ * @returns {mozIStorageStatement}
+ */
+ createStatement(sql) {
+ return this.db.createStatement(sql);
+ }
+
+ /**
+ * Calls the same method on the underlying database connection.
+ *
+ * @param {string} sql
+ *
+ * @returns
+ */
+ executeSimpleSQL(sql) {
+ return this.db.executeSimpleSQL(sql);
+ }
+
+ /**
+ * Takes care of necessary preparations for most of our statements.
+ *
+ * @param {mozIStorageAsyncStatement} aStmt
+ */
+ prepareStatement(aStmt) {
+ try {
+ aStmt.params.cal_id = this.calendarId;
+ this.lastStatement = aStmt;
+ } catch (e) {
+ this.logError("prepareStatement exception", e);
+ }
+ return aStmt;
+ }
+
+ /**
+ * Executes a statement using an item as a parameter.
+ *
+ * @param {mozIStorageStatement} stmt - The statement to execute.
+ * @param {string} idParam - The name of the parameter referring to the item id.
+ * @param {string} id - The id of the item.
+ */
+ executeSyncItemStatement(aStmt, aIdParam, aId) {
+ try {
+ aStmt.params.cal_id = this.calendarId;
+ aStmt.params[aIdParam] = aId;
+ aStmt.executeStep();
+ } catch (e) {
+ this.logError("executeSyncItemStatement exception", e);
+ throw e;
+ } finally {
+ aStmt.reset();
+ }
+ }
+
+ prepareAsyncStatement(aStmts, aStmt) {
+ if (!aStmts.has(aStmt)) {
+ aStmts.set(aStmt, aStmt.newBindingParamsArray());
+ }
+ return aStmts.get(aStmt);
+ }
+
+ prepareAsyncParams(aArray) {
+ let params = aArray.newBindingParams();
+ params.bindByName("cal_id", this.calendarId);
+ return params;
+ }
+
+ /**
+ * Executes one or more SQL statemets.
+ *
+ * @param {mozIStorageAsyncStatement|mozIStorageAsyncStatement[]} aStmts
+ * @param {Function} aCallback
+ */
+ executeAsync(aStmts, aCallback) {
+ if (!Array.isArray(aStmts)) {
+ aStmts = [aStmts];
+ }
+
+ let self = this;
+ return new Promise((resolve, reject) => {
+ this.db.executeAsync(aStmts, {
+ resultPromises: [],
+
+ handleResult(aResultSet) {
+ this.resultPromises.push(this.handleResultInner(aResultSet));
+ },
+ async handleResultInner(aResultSet) {
+ let row = aResultSet.getNextRow();
+ while (row) {
+ try {
+ await aCallback(row);
+ } catch (ex) {
+ this.handleError(ex);
+ }
+ if (this.finishCalled) {
+ self.logError(
+ "Async query completed before all rows consumed. This should never happen.",
+ null
+ );
+ }
+ row = aResultSet.getNextRow();
+ }
+ },
+ handleError(aError) {
+ cal.WARN(aError);
+ },
+ async handleCompletion(aReason) {
+ await Promise.all(this.resultPromises);
+
+ switch (aReason) {
+ case Ci.mozIStorageStatementCallback.REASON_FINISHED:
+ this.finishCalled = true;
+ resolve();
+ break;
+ case Ci.mozIStorageStatementCallback.REASON_CANCELLED:
+ reject(Components.Exception("async statement was cancelled", Cr.NS_ERROR_ABORT));
+ break;
+ default:
+ reject(Components.Exception("error executing async statement", Cr.NS_ERROR_FAILURE));
+ break;
+ }
+ },
+ });
+ });
+ }
+
+ prepareItemStatement(aStmts, aStmt, aIdParam, aId) {
+ aStmt.params.cal_id = this.calendarId;
+ aStmt.params[aIdParam] = aId;
+ aStmts.push(aStmt);
+ }
+
+ /**
+ * Internal logging function that should be called on any database error,
+ * it will log as much info as possible about the database context and
+ * last statement so the problem can be investigated more easily.
+ *
+ * @param message Error message to log.
+ * @param exception Exception that caused the error.
+ */
+ logError(message, exception) {
+ let logMessage = "Message: " + message;
+ if (this.db) {
+ if (this.db.connectionReady) {
+ logMessage += "\nConnection Ready: " + this.db.connectionReady;
+ }
+ if (this.db.lastError) {
+ logMessage += "\nLast DB Error Number: " + this.db.lastError;
+ }
+ if (this.db.lastErrorString) {
+ logMessage += "\nLast DB Error Message: " + this.db.lastErrorString;
+ }
+ if (this.db.databaseFile) {
+ logMessage += "\nDatabase File: " + this.db.databaseFile.path;
+ }
+ if (this.db.lastInsertRowId) {
+ logMessage += "\nLast Insert Row Id: " + this.db.lastInsertRowId;
+ }
+ if (this.db.transactionInProgress) {
+ logMessage += "\nTransaction In Progress: " + this.db.transactionInProgress;
+ }
+ }
+
+ if (this.lastStatement) {
+ logMessage += "\nLast DB Statement: " + this.lastStatement;
+ // Async statements do not allow enumeration of parameters.
+ if (this.lastStatement instanceof Ci.mozIStorageStatement && this.lastStatement.params) {
+ for (let param in this.lastStatement.params) {
+ logMessage +=
+ "\nLast Statement param [" + param + "]: " + this.lastStatement.params[param];
+ }
+ }
+ }
+
+ if (exception) {
+ logMessage += "\nException: " + exception;
+ }
+ cal.ERROR("[calStorageCalendar] " + logMessage + "\n" + cal.STACK(10));
+ }
+
+ /**
+ * Close the underlying db connection.
+ */
+ close() {
+ closeConnection(this.db);
+ this.db = null;
+ }
+}
diff --git a/comm/calendar/providers/storage/CalStorageItemModel.jsm b/comm/calendar/providers/storage/CalStorageItemModel.jsm
new file mode 100644
index 0000000000..a25e5bbd46
--- /dev/null
+++ b/comm/calendar/providers/storage/CalStorageItemModel.jsm
@@ -0,0 +1,1374 @@
+/* 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 = ["CalStorageItemModel"];
+
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+const { CAL_ITEM_FLAG, newDateTime } = ChromeUtils.import(
+ "resource:///modules/calendar/calStorageHelpers.jsm"
+);
+const { CalReadableStreamFactory } = ChromeUtils.import(
+ "resource:///modules/CalReadableStreamFactory.jsm"
+);
+const { CalStorageModelBase } = ChromeUtils.import(
+ "resource:///modules/calendar/CalStorageModelBase.jsm"
+);
+
+const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ CalAlarm: "resource:///modules/CalAlarm.jsm",
+ CalAttachment: "resource:///modules/CalAttachment.jsm",
+ CalAttendee: "resource:///modules/CalAttendee.jsm",
+ CalEvent: "resource:///modules/CalEvent.jsm",
+ CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm",
+ CalRelation: "resource:///modules/CalRelation.jsm",
+ CalTodo: "resource:///modules/CalTodo.jsm",
+});
+
+const cICL = Ci.calIChangeLog;
+const USECS_PER_SECOND = 1000000;
+const DEFAULT_START_TIME = -9223372036854776000;
+
+// endTime needs to be the max value a PRTime can be
+const DEFAULT_END_TIME = 9223372036854776000;
+
+// Calls to get items from the database await this Promise. In normal operation
+// the Promise resolves after most application start-up operations, so that we
+// don't start hitting the database during start-up. Fox XPCShell tests, normal
+// start-up doesn't happen, so we just resolve the Promise instantly.
+let startupPromise;
+if (Services.appinfo.name == "xpcshell") {
+ startupPromise = Promise.resolve();
+} else {
+ const { MailGlue } = ChromeUtils.import("resource:///modules/MailGlue.jsm");
+ startupPromise = MailGlue.afterStartUp;
+}
+
+/**
+ * CalStorageItemModel provides methods for manipulating item data.
+ */
+class CalStorageItemModel extends CalStorageModelBase {
+ /**
+ * calCachedCalendar modifies the superCalendar property so this is made
+ * lazy.
+ *
+ * @type {calISchedulingSupport}
+ */
+ get #schedulingSupport() {
+ return (
+ (this.calendar.superCalendar.supportsScheduling &&
+ this.calendar.superCalendar.getSchedulingSupport()) ||
+ null
+ );
+ }
+
+ /**
+ * Update the item passed.
+ *
+ * @param {calIItemBase} item - The newest version of the item.
+ * @param {calIItemBase} oldItem - The previous version of the item.
+ */
+ async updateItem(item, olditem) {
+ cal.ASSERT(!item.recurrenceId, "no parent item passed!", true);
+ await this.deleteItemById(olditem.id, true);
+ await this.addItem(item);
+ }
+
+ /**
+ * Object containing the parameters for executing a DB query.
+ *
+ * @typedef {object} CalStorageQuery
+ * @property {CalStorageQueryFilter} filter
+ * @property {calIDateTime} rangeStart
+ * @property {calIDateTime?} rangeEnd
+ * @property {number} count
+ */
+
+ /**
+ * Object indicating types and state of items to return.
+ *
+ * @typedef {object} CalStorageQueryFilter
+ * @property {boolean} wantUnrespondedInvitations
+ * @property {boolean} wantEvents
+ * @property {boolean} wantTodos
+ * @property {boolean} asOccurrences
+ * @property {boolean} wantOfflineDeletedItems
+ * @property {boolean} wantOfflineCreatedItems
+ * @property {boolean} wantOfflineModifiedItems
+ * @property {boolean} itemCompletedFilter
+ * @property {boolean} itemNotCompletedFilter
+ */
+
+ /**
+ * Retrieves one or more items from the database based on the query provided.
+ * See the definition of CalStorageQuery for valid query parameters.
+ *
+ * @param {CalStorageQuery} query
+ *
+ * @returns {ReadableStream<calIItemBase>}
+ */
+ getItems(query) {
+ let { filters, count } = query;
+ let self = this;
+ return CalReadableStreamFactory.createBoundedReadableStream(
+ count,
+ CalReadableStreamFactory.defaultQueueSize,
+ {
+ async start(controller) {
+ if (filters) {
+ if (filters.wantEvents) {
+ for await (let value of cal.iterate.streamValues(self.#getEvents(query))) {
+ controller.enqueue(value);
+ }
+ }
+
+ count = count && count - controller.count;
+ if (filters.wantTodos && (!count || count > 0)) {
+ for await (let value of cal.iterate.streamValues(
+ self.#getTodos({ ...query, count })
+ )) {
+ controller.enqueue(value);
+ }
+ }
+ controller.close();
+ }
+ },
+ }
+ );
+ }
+
+ /**
+ * Queries the database for calIEvent records providing them in a streaming
+ * fashion.
+ *
+ * @param {CalStorageQuery} query
+ *
+ * @returns {ReadableStream<calIEvent>}
+ */
+ #getEvents(query) {
+ let { filters, rangeStart, rangeEnd } = query;
+ let startTime = DEFAULT_START_TIME;
+ let endTime = DEFAULT_END_TIME;
+
+ if (rangeStart) {
+ startTime = rangeStart.nativeTime;
+ }
+ if (rangeEnd) {
+ endTime = rangeEnd.nativeTime;
+ }
+
+ let params; // stmt params
+ let requestedOfflineJournal = null;
+
+ if (filters.wantOfflineDeletedItems) {
+ requestedOfflineJournal = cICL.OFFLINE_FLAG_DELETED_RECORD;
+ } else if (filters.wantOfflineCreatedItems) {
+ requestedOfflineJournal = cICL.OFFLINE_FLAG_CREATED_RECORD;
+ } else if (filters.wantOfflineModifiedItems) {
+ requestedOfflineJournal = cICL.OFFLINE_FLAG_MODIFIED_RECORD;
+ }
+ let self = this;
+ return CalReadableStreamFactory.createBoundedReadableStream(
+ query.count,
+ CalReadableStreamFactory.defaultQueueSize,
+ {
+ async start(controller) {
+ await startupPromise;
+ // first get non-recurring events that happen to fall within the range
+ try {
+ self.db.prepareStatement(self.statements.mSelectNonRecurringEventsByRange);
+ params = self.statements.mSelectNonRecurringEventsByRange.params;
+ params.range_start = startTime;
+ params.range_end = endTime;
+ params.start_offset = rangeStart ? rangeStart.timezoneOffset * USECS_PER_SECOND : 0;
+ params.end_offset = rangeEnd ? rangeEnd.timezoneOffset * USECS_PER_SECOND : 0;
+ params.offline_journal = requestedOfflineJournal;
+
+ await self.db.executeAsync(
+ self.statements.mSelectNonRecurringEventsByRange,
+ async row => {
+ let event = self.#expandOccurrences(
+ await self.getEventFromRow(row),
+ startTime,
+ rangeStart,
+ rangeEnd,
+ filters
+ );
+ controller.enqueue(event);
+ }
+ );
+ } catch (e) {
+ self.db.logError("Error selecting non recurring events by range!\n", e);
+ }
+
+ if (!controller.maxTotalItemsReached) {
+ // Process the recurring events
+ let [recEvents, recEventFlags] = await self.getFullRecurringEventAndFlagMaps();
+ for (let [id, evitem] of recEvents.entries()) {
+ let cachedJournalFlag = recEventFlags.get(id);
+ // No need to return flagged unless asked i.e. requestedOfflineJournal == cachedJournalFlag
+ // Return created and modified offline records if requestedOfflineJournal is null alongwith events that have no flag
+ if (
+ (requestedOfflineJournal == null &&
+ cachedJournalFlag != cICL.OFFLINE_FLAG_DELETED_RECORD) ||
+ (requestedOfflineJournal != null && cachedJournalFlag == requestedOfflineJournal)
+ ) {
+ controller.enqueue(
+ self.#expandOccurrences(evitem, startTime, rangeStart, rangeEnd, filters)
+ );
+ if (controller.maxTotalItemsReached) {
+ break;
+ }
+ }
+ }
+ }
+ controller.close();
+ },
+ }
+ );
+ }
+
+ /**
+ * Queries the database for calITodo records providing them in a streaming
+ * fashion.
+ *
+ * @param {CalStorageQuery} query
+ *
+ * @returns {ReadableStream<calITodo>}
+ */
+ #getTodos(query) {
+ let { filters, rangeStart, rangeEnd } = query;
+ let startTime = DEFAULT_START_TIME;
+ let endTime = DEFAULT_END_TIME;
+
+ if (rangeStart) {
+ startTime = rangeStart.nativeTime;
+ }
+ if (rangeEnd) {
+ endTime = rangeEnd.nativeTime;
+ }
+
+ let params; // stmt params
+ let requestedOfflineJournal = null;
+
+ if (filters.wantOfflineCreatedItems) {
+ requestedOfflineJournal = cICL.OFFLINE_FLAG_CREATED_RECORD;
+ } else if (filters.wantOfflineDeletedItems) {
+ requestedOfflineJournal = cICL.OFFLINE_FLAG_DELETED_RECORD;
+ } else if (filters.wantOfflineModifiedItems) {
+ requestedOfflineJournal = cICL.OFFLINE_FLAG_MODIFIED_RECORD;
+ }
+
+ let checkCompleted = item =>
+ item.isCompleted ? filters.itemCompletedFilter : filters.itemNotCompletedFilter;
+
+ let self = this;
+ return CalReadableStreamFactory.createBoundedReadableStream(
+ query.count,
+ CalReadableStreamFactory.defaultQueueSize,
+ {
+ async start(controller) {
+ await startupPromise;
+ // first get non-recurring todos that happen to fall within the range
+ try {
+ self.db.prepareStatement(self.statements.mSelectNonRecurringTodosByRange);
+ params = self.statements.mSelectNonRecurringTodosByRange.params;
+ params.range_start = startTime;
+ params.range_end = endTime;
+ params.start_offset = rangeStart ? rangeStart.timezoneOffset * USECS_PER_SECOND : 0;
+ params.end_offset = rangeEnd ? rangeEnd.timezoneOffset * USECS_PER_SECOND : 0;
+ params.offline_journal = requestedOfflineJournal;
+
+ await self.db.executeAsync(
+ self.statements.mSelectNonRecurringTodosByRange,
+ async row => {
+ let todo = self.#expandOccurrences(
+ await self.getTodoFromRow(row),
+ startTime,
+ rangeStart,
+ rangeEnd,
+ filters,
+ checkCompleted
+ );
+ controller.enqueue(todo);
+ }
+ );
+ } catch (e) {
+ self.db.logError("Error selecting non recurring todos by range", e);
+ }
+
+ if (!controller.maxTotalItemsReached) {
+ // Note: Reading the code, completed *occurrences* seems to be broken, because
+ // only the parent item has been filtered; I fixed that.
+ // Moreover item.todo_complete etc seems to be a leftover...
+
+ // process the recurring todos
+ let [recTodos, recTodoFlags] = await self.getFullRecurringTodoAndFlagMaps();
+ for (let [id, todoitem] of recTodos) {
+ let cachedJournalFlag = recTodoFlags.get(id);
+ if (
+ (requestedOfflineJournal == null &&
+ (cachedJournalFlag == cICL.OFFLINE_FLAG_MODIFIED_RECORD ||
+ cachedJournalFlag == cICL.OFFLINE_FLAG_CREATED_RECORD ||
+ cachedJournalFlag == null)) ||
+ (requestedOfflineJournal != null && cachedJournalFlag == requestedOfflineJournal)
+ ) {
+ controller.enqueue(
+ self.#expandOccurrences(
+ todoitem,
+ startTime,
+ rangeStart,
+ rangeEnd,
+ filters,
+ checkCompleted
+ )
+ );
+ if (controller.maxTotalItemsReached) {
+ break;
+ }
+ }
+ }
+ }
+ controller.close();
+ },
+ }
+ );
+ }
+
+ #checkUnrespondedInvitation(item) {
+ let att = this.#schedulingSupport.getInvitedAttendee(item);
+ return att && att.participationStatus == "NEEDS-ACTION";
+ }
+
+ #expandOccurrences(item, startTime, rangeStart, rangeEnd, filters, optionalFilterFunc) {
+ if (item.recurrenceInfo && item.recurrenceInfo.recurrenceEndDate < startTime) {
+ return [];
+ }
+
+ let expandedItems = [];
+ if (item.recurrenceInfo && filters.asOccurrences) {
+ // If the item is recurring, get all occurrences that fall in
+ // the range. If the item doesn't fall into the range at all,
+ // this expands to 0 items.
+ expandedItems = item.recurrenceInfo.getOccurrences(rangeStart, rangeEnd, 0);
+ if (filters.wantUnrespondedInvitations) {
+ expandedItems = expandedItems.filter(item => this.#checkUnrespondedInvitation(item));
+ }
+ } else if (
+ (!filters.wantUnrespondedInvitations || this.#checkUnrespondedInvitation(item)) &&
+ cal.item.checkIfInRange(item, rangeStart, rangeEnd)
+ ) {
+ // If no occurrences are wanted, check only the parent item.
+ // This will be changed with bug 416975.
+ expandedItems = [item];
+ }
+
+ if (expandedItems.length) {
+ if (optionalFilterFunc) {
+ expandedItems = expandedItems.filter(optionalFilterFunc);
+ }
+ }
+ return expandedItems;
+ }
+
+ /**
+ * Read in the common ItemBase attributes from aDBRow, and stick
+ * them on item.
+ *
+ * @param {mozIStorageRow} row
+ * @param {calIItemBase} item
+ */
+ #getItemBaseFromRow(row, item) {
+ item.calendar = this.calendar.superCalendar;
+ item.id = row.getResultByName("id");
+ if (row.getResultByName("title")) {
+ item.title = row.getResultByName("title");
+ }
+ if (row.getResultByName("priority")) {
+ item.priority = row.getResultByName("priority");
+ }
+ if (row.getResultByName("privacy")) {
+ item.privacy = row.getResultByName("privacy");
+ }
+ if (row.getResultByName("ical_status")) {
+ item.status = row.getResultByName("ical_status");
+ }
+
+ if (row.getResultByName("alarm_last_ack")) {
+ // alarm acks are always in utc
+ item.alarmLastAck = newDateTime(row.getResultByName("alarm_last_ack"), "UTC");
+ }
+
+ if (row.getResultByName("recurrence_id")) {
+ item.recurrenceId = newDateTime(
+ row.getResultByName("recurrence_id"),
+ row.getResultByName("recurrence_id_tz")
+ );
+ if ((row.getResultByName("flags") & CAL_ITEM_FLAG.RECURRENCE_ID_ALLDAY) != 0) {
+ item.recurrenceId.isDate = true;
+ }
+ }
+
+ if (row.getResultByName("time_created")) {
+ item.setProperty("CREATED", newDateTime(row.getResultByName("time_created"), "UTC"));
+ }
+
+ // This must be done last because the setting of any other property
+ // after this would overwrite it again.
+ if (row.getResultByName("last_modified")) {
+ item.setProperty("LAST-MODIFIED", newDateTime(row.getResultByName("last_modified"), "UTC"));
+ }
+ }
+
+ /**
+ * @callback OnItemRowCallback
+ * @param {string} id - The id of the item fetched from the row.
+ */
+
+ /**
+ * Provides all recurring events along with offline flag values for each event.
+ *
+ * @param {OnItemRowCallback} [callback] - If provided, will be called on each row
+ * fetched.
+ * @returns {Promise<[Map<string, calIEvent>, Map<string, number>]>}
+ */
+ async getRecurringEventAndFlagMaps(callback) {
+ await startupPromise;
+ let events = new Map();
+ let flags = new Map();
+ this.db.prepareStatement(this.statements.mSelectEventsWithRecurrence);
+ await this.db.executeAsync(this.statements.mSelectEventsWithRecurrence, async row => {
+ let item_id = row.getResultByName("id");
+ if (callback) {
+ callback(item_id);
+ }
+ let item = await this.getEventFromRow(row, false);
+ events.set(item_id, item);
+ flags.set(item_id, row.getResultByName("offline_journal") || null);
+ });
+ return [events, flags];
+ }
+
+ /**
+ * Provides all recurring events with additional data populated along with
+ * offline flags values for each event.
+ *
+ * @returns {Promise<[Map<string, calIEvent>, Map<string, number>]>}
+ */
+ async getFullRecurringEventAndFlagMaps() {
+ let [events, flags] = await this.getRecurringEventAndFlagMaps();
+ return [await this.getAdditionalDataForItemMap(events), flags];
+ }
+
+ /**
+ * Provides all recurring todos along with offline flag values for each event.
+ *
+ * @param {OnItemRowCallback} [callback] - If provided, will be called on each row
+ * fetched.
+ *
+ * @returns {Promise<[Map<string, calITodo>, Map<string, number>]>}
+ */
+ async getRecurringTodoAndFlagMaps(callback) {
+ await startupPromise;
+ let todos = new Map();
+ let flags = new Map();
+ this.db.prepareStatement(this.statements.mSelectTodosWithRecurrence);
+ await this.db.executeAsync(this.statements.mSelectTodosWithRecurrence, async row => {
+ let item_id = row.getResultByName("id");
+ if (callback) {
+ callback(item_id);
+ }
+ let item = await this.getTodoFromRow(row, false);
+ todos.set(item_id, item);
+ flags.set(item_id, row.getResultByName("offline_journal") || null);
+ });
+ return [todos, flags];
+ }
+
+ /**
+ * Provides all recurring todos with additional data populated along with
+ * offline flags values for each todo.
+ *
+ * @returns {Promise<[Map<string, calITodo>, Map<string, number>]>}
+ */
+ async getFullRecurringTodoAndFlagMaps() {
+ let [todos, flags] = await this.getRecurringTodoAndFlagMaps();
+ return [await this.getAdditionalDataForItemMap(todos), flags];
+ }
+
+ /**
+ * The `icalString` database fields could be stored with or without lines
+ * folded, but if this raw data is passed to ical.js it misinterprets the
+ * white-space as significant. Strip it out as the data is fetched.
+ *
+ * @param {mozIStorageRow} row
+ * @returns {string}
+ */
+ #unfoldIcalString(row) {
+ return row.getResultByName("icalString").replaceAll("\r\n ", "");
+ }
+
+ /**
+ * Populates additional data for a Map of items. This method is overridden in
+ * CalStorageCachedItemModel to allow the todos to be loaded from the cache.
+ *
+ * @param {Map<string, calIItem>} itemMap
+ *
+ * @returns {Promise<Map<string, calIItem>>} The original Map with items modified.
+ */
+ async getAdditionalDataForItemMap(itemsMap) {
+ await startupPromise;
+ //NOTE: There seems to be a bug in the SQLite subsystem that causes callers
+ //awaiting on this method to continue prematurely. This can cause unexpected
+ //behaviour. After investigating, it appears triggering the bug is related
+ //to the number of queries executed here.
+ this.db.prepareStatement(this.statements.mSelectAllAttendees);
+ await this.db.executeAsync(this.statements.mSelectAllAttendees, row => {
+ let item = itemsMap.get(row.getResultByName("item_id"));
+ if (!item) {
+ return;
+ }
+
+ let attendee = new lazy.CalAttendee(this.#unfoldIcalString(row));
+ if (attendee && attendee.id) {
+ if (attendee.isOrganizer) {
+ item.organizer = attendee;
+ } else {
+ item.addAttendee(attendee);
+ }
+ } else {
+ cal.WARN(
+ "[calStorageCalendar] Skipping invalid attendee for item '" +
+ item.title +
+ "' (" +
+ item.id +
+ ")."
+ );
+ }
+ });
+
+ this.db.prepareStatement(this.statements.mSelectAllProperties);
+ await this.db.executeAsync(this.statements.mSelectAllProperties, row => {
+ let item = itemsMap.get(row.getResultByName("item_id"));
+ if (!item) {
+ return;
+ }
+
+ let name = row.getResultByName("key");
+ switch (name) {
+ case "DURATION":
+ // for events DTEND/DUE is enforced by calEvent/calTodo, so suppress DURATION:
+ break;
+ case "CATEGORIES": {
+ let cats = cal.category.stringToArray(row.getResultByName("value"));
+ item.setCategories(cats);
+ break;
+ }
+ default:
+ let value = row.getResultByName("value");
+ item.setProperty(name, value);
+ break;
+ }
+ });
+
+ this.db.prepareStatement(this.statements.mSelectAllParameters);
+ await this.db.executeAsync(this.statements.mSelectAllParameters, row => {
+ let item = itemsMap.get(row.getResultByName("item_id"));
+ if (!item) {
+ return;
+ }
+
+ let prop = row.getResultByName("key1");
+ let param = row.getResultByName("key2");
+ let value = row.getResultByName("value");
+ item.setPropertyParameter(prop, param, value);
+ });
+
+ this.db.prepareStatement(this.statements.mSelectAllRecurrences);
+ await this.db.executeAsync(this.statements.mSelectAllRecurrences, row => {
+ let item = itemsMap.get(row.getResultByName("item_id"));
+ if (!item) {
+ return;
+ }
+
+ let recInfo = item.recurrenceInfo;
+ if (!recInfo) {
+ recInfo = new lazy.CalRecurrenceInfo(item);
+ item.recurrenceInfo = recInfo;
+ }
+
+ let ritem = this.#getRecurrenceItemFromRow(row);
+ recInfo.appendRecurrenceItem(ritem);
+ });
+
+ this.db.prepareStatement(this.statements.mSelectAllEventExceptions);
+ await this.db.executeAsync(this.statements.mSelectAllEventExceptions, async row => {
+ let item = itemsMap.get(row.getResultByName("id"));
+ if (!item) {
+ return;
+ }
+
+ let rec = item.recurrenceInfo;
+ let exc = await this.getEventFromRow(row);
+ rec.modifyException(exc, true);
+ });
+
+ this.db.prepareStatement(this.statements.mSelectAllTodoExceptions);
+ await this.db.executeAsync(this.statements.mSelectAllTodoExceptions, async row => {
+ let item = itemsMap.get(row.getResultByName("id"));
+ if (!item) {
+ return;
+ }
+
+ let rec = item.recurrenceInfo;
+ let exc = await this.getTodoFromRow(row);
+ rec.modifyException(exc, true);
+ });
+
+ this.db.prepareStatement(this.statements.mSelectAllAttachments);
+ await this.db.executeAsync(this.statements.mSelectAllAttachments, row => {
+ let item = itemsMap.get(row.getResultByName("item_id"));
+ if (item) {
+ item.addAttachment(new lazy.CalAttachment(this.#unfoldIcalString(row)));
+ }
+ });
+
+ this.db.prepareStatement(this.statements.mSelectAllRelations);
+ await this.db.executeAsync(this.statements.mSelectAllRelations, row => {
+ let item = itemsMap.get(row.getResultByName("item_id"));
+ if (item) {
+ item.addRelation(new lazy.CalRelation(this.#unfoldIcalString(row)));
+ }
+ });
+
+ this.db.prepareStatement(this.statements.mSelectAllAlarms);
+ await this.db.executeAsync(this.statements.mSelectAllAlarms, row => {
+ let item = itemsMap.get(row.getResultByName("item_id"));
+ if (item) {
+ item.addAlarm(new lazy.CalAlarm(this.#unfoldIcalString(row)));
+ }
+ });
+
+ for (let item of itemsMap.values()) {
+ this.#fixGoogleCalendarDescriptionIfNeeded(item);
+ item.makeImmutable();
+ }
+ return itemsMap;
+ }
+
+ /**
+ * For items that were cached or stored in previous versions,
+ * put Google's HTML description in the right place.
+ *
+ * @param {calIItemBase} item
+ */
+ #fixGoogleCalendarDescriptionIfNeeded(item) {
+ if (item.id && item.id.endsWith("@google.com")) {
+ let description = item.getProperty("DESCRIPTION");
+ if (description) {
+ let altrep = item.getPropertyParameter("DESCRIPTION", "ALTREP");
+ if (!altrep) {
+ cal.view.fixGoogleCalendarDescription(item);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param {mozIStorageRow} row
+ * @param {boolean} getAdditionalData
+ */
+ async getEventFromRow(row, getAdditionalData = true) {
+ let item = new lazy.CalEvent();
+ let flags = row.getResultByName("flags");
+
+ if (row.getResultByName("event_start")) {
+ item.startDate = newDateTime(
+ row.getResultByName("event_start"),
+ row.getResultByName("event_start_tz")
+ );
+ }
+ if (row.getResultByName("event_end")) {
+ item.endDate = newDateTime(
+ row.getResultByName("event_end"),
+ row.getResultByName("event_end_tz")
+ );
+ }
+ if (row.getResultByName("event_stamp")) {
+ item.setProperty("DTSTAMP", newDateTime(row.getResultByName("event_stamp"), "UTC"));
+ }
+ if (flags & CAL_ITEM_FLAG.EVENT_ALLDAY) {
+ item.startDate.isDate = true;
+ item.endDate.isDate = true;
+ }
+
+ // This must be done last to keep the modification time intact.
+ this.#getItemBaseFromRow(row, item);
+ if (getAdditionalData) {
+ await this.#getAdditionalDataForItem(item, row.getResultByName("flags"));
+ item.makeImmutable();
+ }
+ return item;
+ }
+
+ /**
+ * @param {mozIStorageRow} row
+ * @param {boolean} getAdditionalData
+ */
+ async getTodoFromRow(row, getAdditionalData = true) {
+ let item = new lazy.CalTodo();
+ let flags = row.getResultByName("flags");
+
+ if (row.getResultByName("todo_entry")) {
+ item.entryDate = newDateTime(
+ row.getResultByName("todo_entry"),
+ row.getResultByName("todo_entry_tz")
+ );
+ }
+ if (row.getResultByName("todo_due")) {
+ item.dueDate = newDateTime(
+ row.getResultByName("todo_due"),
+ row.getResultByName("todo_due_tz")
+ );
+ }
+ if (row.getResultByName("todo_stamp")) {
+ item.setProperty("DTSTAMP", newDateTime(row.getResultByName("todo_stamp"), "UTC"));
+ }
+ if (row.getResultByName("todo_completed")) {
+ item.completedDate = newDateTime(
+ row.getResultByName("todo_completed"),
+ row.getResultByName("todo_completed_tz")
+ );
+ }
+ if (row.getResultByName("todo_complete")) {
+ item.percentComplete = row.getResultByName("todo_complete");
+ }
+ if (flags & CAL_ITEM_FLAG.EVENT_ALLDAY) {
+ if (item.entryDate) {
+ item.entryDate.isDate = true;
+ }
+ if (item.dueDate) {
+ item.dueDate.isDate = true;
+ }
+ }
+
+ // This must be done last to keep the modification time intact.
+ this.#getItemBaseFromRow(row, item);
+ if (getAdditionalData) {
+ await this.#getAdditionalDataForItem(item, row.getResultByName("flags"));
+ item.makeImmutable();
+ }
+ return item;
+ }
+
+ /**
+ * After we get the base item, we need to check if we need to pull in
+ * any extra data from other tables. We do that here.
+ */
+ async #getAdditionalDataForItem(item, flags) {
+ // This is needed to keep the modification time intact.
+ let savedLastModifiedTime = item.lastModifiedTime;
+
+ if (flags & CAL_ITEM_FLAG.HAS_ATTENDEES) {
+ let selectItem = null;
+ if (item.recurrenceId == null) {
+ selectItem = this.statements.mSelectAttendeesForItem;
+ } else {
+ selectItem = this.statements.mSelectAttendeesForItemWithRecurrenceId;
+ this.#setDateParamHelper(selectItem, "recurrence_id", item.recurrenceId);
+ }
+
+ try {
+ this.db.prepareStatement(selectItem);
+ selectItem.params.item_id = item.id;
+ await this.db.executeAsync(selectItem, row => {
+ let attendee = new lazy.CalAttendee(this.#unfoldIcalString(row));
+ if (attendee && attendee.id) {
+ if (attendee.isOrganizer) {
+ item.organizer = attendee;
+ } else {
+ item.addAttendee(attendee);
+ }
+ } else {
+ cal.WARN(
+ `[calStorageCalendar] Skipping invalid attendee for item '${item.title}' (${item.id}).`
+ );
+ }
+ });
+ } catch (e) {
+ this.db.logError(`Error getting attendees for item '${item.title}' (${item.id})!`, e);
+ }
+ }
+
+ if (flags & CAL_ITEM_FLAG.HAS_PROPERTIES) {
+ let selectItem = null;
+ let selectParam = null;
+ if (item.recurrenceId == null) {
+ selectItem = this.statements.mSelectPropertiesForItem;
+ selectParam = this.statements.mSelectParametersForItem;
+ } else {
+ selectItem = this.statements.mSelectPropertiesForItemWithRecurrenceId;
+ this.#setDateParamHelper(selectItem, "recurrence_id", item.recurrenceId);
+ selectParam = this.statements.mSelectParametersForItemWithRecurrenceId;
+ this.#setDateParamHelper(selectParam, "recurrence_id", item.recurrenceId);
+ }
+
+ try {
+ this.db.prepareStatement(selectItem);
+ selectItem.params.item_id = item.id;
+ await this.db.executeAsync(selectItem, row => {
+ let name = row.getResultByName("key");
+ switch (name) {
+ case "DURATION":
+ // for events DTEND/DUE is enforced by calEvent/calTodo, so suppress DURATION:
+ break;
+ case "CATEGORIES": {
+ let cats = cal.category.stringToArray(row.getResultByName("value"));
+ item.setCategories(cats);
+ break;
+ }
+ default:
+ let value = row.getResultByName("value");
+ item.setProperty(name, value);
+ break;
+ }
+ });
+
+ this.db.prepareStatement(selectParam);
+ selectParam.params.item_id = item.id;
+ await this.db.executeAsync(selectParam, row => {
+ let prop = row.getResultByName("key1");
+ let param = row.getResultByName("key2");
+ let value = row.getResultByName("value");
+ item.setPropertyParameter(prop, param, value);
+ });
+ } catch (e) {
+ this.db.logError(
+ "Error getting extra properties for item '" + item.title + "' (" + item.id + ")!",
+ e
+ );
+ }
+ }
+
+ if (flags & CAL_ITEM_FLAG.HAS_RECURRENCE) {
+ if (item.recurrenceId) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ let recInfo = new lazy.CalRecurrenceInfo(item);
+ item.recurrenceInfo = recInfo;
+
+ try {
+ this.db.prepareStatement(this.statements.mSelectRecurrenceForItem);
+ this.statements.mSelectRecurrenceForItem.params.item_id = item.id;
+ await this.db.executeAsync(this.statements.mSelectRecurrenceForItem, row => {
+ let ritem = this.#getRecurrenceItemFromRow(row);
+ recInfo.appendRecurrenceItem(ritem);
+ });
+ } catch (e) {
+ this.db.logError(
+ "Error getting recurrence for item '" + item.title + "' (" + item.id + ")!",
+ e
+ );
+ }
+ }
+
+ if (flags & CAL_ITEM_FLAG.HAS_EXCEPTIONS) {
+ // it's safe that we don't run into this branch again for exceptions
+ // (getAdditionalDataForItem->get[Event|Todo]FromRow->getAdditionalDataForItem):
+ // every excepton has a recurrenceId and isn't flagged as CAL_ITEM_FLAG.HAS_EXCEPTIONS
+ if (item.recurrenceId) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ let rec = item.recurrenceInfo;
+
+ if (item.isEvent()) {
+ this.statements.mSelectEventExceptions.params.id = item.id;
+ this.db.prepareStatement(this.statements.mSelectEventExceptions);
+ try {
+ await this.db.executeAsync(this.statements.mSelectEventExceptions, async row => {
+ let exc = await this.getEventFromRow(row, false);
+ rec.modifyException(exc, true);
+ });
+ } catch (e) {
+ this.db.logError(
+ "Error getting exceptions for event '" + item.title + "' (" + item.id + ")!",
+ e
+ );
+ }
+ } else if (item.isTodo()) {
+ this.statements.mSelectTodoExceptions.params.id = item.id;
+ this.db.prepareStatement(this.statements.mSelectTodoExceptions);
+ try {
+ await this.db.executeAsync(this.statements.mSelectTodoExceptions, async row => {
+ let exc = await this.getTodoFromRow(row, false);
+ rec.modifyException(exc, true);
+ });
+ } catch (e) {
+ this.db.logError(
+ "Error getting exceptions for task '" + item.title + "' (" + item.id + ")!",
+ e
+ );
+ }
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+ }
+
+ if (flags & CAL_ITEM_FLAG.HAS_ATTACHMENTS) {
+ let selectAttachment = this.statements.mSelectAttachmentsForItem;
+ if (item.recurrenceId != null) {
+ selectAttachment = this.statements.mSelectAttachmentsForItemWithRecurrenceId;
+ this.#setDateParamHelper(selectAttachment, "recurrence_id", item.recurrenceId);
+ }
+ try {
+ this.db.prepareStatement(selectAttachment);
+ selectAttachment.params.item_id = item.id;
+ await this.db.executeAsync(selectAttachment, row => {
+ item.addAttachment(new lazy.CalAttachment(this.#unfoldIcalString(row)));
+ });
+ } catch (e) {
+ this.db.logError(
+ "Error getting attachments for item '" + item.title + "' (" + item.id + ")!",
+ e
+ );
+ }
+ }
+
+ if (flags & CAL_ITEM_FLAG.HAS_RELATIONS) {
+ let selectRelation = this.statements.mSelectRelationsForItem;
+ if (item.recurrenceId != null) {
+ selectRelation = this.statements.mSelectRelationsForItemWithRecurrenceId;
+ this.#setDateParamHelper(selectRelation, "recurrence_id", item.recurrenceId);
+ }
+ try {
+ this.db.prepareStatement(selectRelation);
+ selectRelation.params.item_id = item.id;
+ await this.db.executeAsync(selectRelation, row => {
+ item.addRelation(new lazy.CalRelation(this.#unfoldIcalString(row)));
+ });
+ } catch (e) {
+ this.db.logError(
+ "Error getting relations for item '" + item.title + "' (" + item.id + ")!",
+ e
+ );
+ }
+ }
+
+ if (flags & CAL_ITEM_FLAG.HAS_ALARMS) {
+ let selectAlarm = this.statements.mSelectAlarmsForItem;
+ if (item.recurrenceId != null) {
+ selectAlarm = this.statements.mSelectAlarmsForItemWithRecurrenceId;
+ this.#setDateParamHelper(selectAlarm, "recurrence_id", item.recurrenceId);
+ }
+ try {
+ selectAlarm.params.item_id = item.id;
+ this.db.prepareStatement(selectAlarm);
+ await this.db.executeAsync(selectAlarm, row => {
+ item.addAlarm(new lazy.CalAlarm(this.#unfoldIcalString(row)));
+ });
+ } catch (e) {
+ this.db.logError(
+ "Error getting alarms for item '" + item.title + "' (" + item.id + ")!",
+ e
+ );
+ }
+ }
+
+ this.#fixGoogleCalendarDescriptionIfNeeded(item);
+ // Restore the saved modification time
+ item.setProperty("LAST-MODIFIED", savedLastModifiedTime);
+ }
+
+ #getRecurrenceItemFromRow(row) {
+ let ritem;
+ let prop = cal.icsService.createIcalPropertyFromString(this.#unfoldIcalString(row));
+ switch (prop.propertyName) {
+ case "RDATE":
+ case "EXDATE":
+ ritem = cal.createRecurrenceDate();
+ break;
+ case "RRULE":
+ case "EXRULE":
+ ritem = cal.createRecurrenceRule();
+ break;
+ default:
+ throw new Error("Unknown recurrence item: " + prop.propertyName);
+ }
+
+ ritem.icalProperty = prop;
+ return ritem;
+ }
+
+ /**
+ * Get an item from db given its id.
+ *
+ * @param {string} aID
+ */
+ async getItemById(aID) {
+ let item = null;
+ try {
+ // try events first
+ this.db.prepareStatement(this.statements.mSelectEvent);
+ this.statements.mSelectEvent.params.id = aID;
+ await this.db.executeAsync(this.statements.mSelectEvent, async row => {
+ item = await this.getEventFromRow(row);
+ });
+ } catch (e) {
+ this.db.logError("Error selecting item by id " + aID + "!", e);
+ }
+
+ // try todo if event fails
+ if (!item) {
+ try {
+ this.db.prepareStatement(this.statements.mSelectTodo);
+ this.statements.mSelectTodo.params.id = aID;
+ await this.db.executeAsync(this.statements.mSelectTodo, async row => {
+ item = await this.getTodoFromRow(row);
+ });
+ } catch (e) {
+ this.db.logError("Error selecting item by id " + aID + "!", e);
+ }
+ }
+ return item;
+ }
+
+ #setDateParamHelper(params, entryname, cdt) {
+ if (cdt) {
+ params.bindByName(entryname, cdt.nativeTime);
+ let timezone = cdt.timezone;
+ let ownTz = cal.timezoneService.getTimezone(timezone.tzid);
+ if (ownTz) {
+ // if we know that TZID, we use it
+ params.bindByName(entryname + "_tz", ownTz.tzid);
+ } else if (timezone.icalComponent) {
+ // foreign one
+ params.bindByName(entryname + "_tz", timezone.icalComponent.serializeToICS());
+ } else {
+ // timezone component missing
+ params.bindByName(entryname + "_tz", "floating");
+ }
+ } else {
+ params.bindByName(entryname, null);
+ params.bindByName(entryname + "_tz", null);
+ }
+ }
+
+ /**
+ * Adds an item to the database, the item should have an id that is not
+ * already in use.
+ *
+ * @param {calIItemBase} item
+ */
+ async addItem(item) {
+ let stmts = new Map();
+ this.#prepareItem(stmts, item);
+ for (let [stmt, array] of stmts) {
+ stmt.bindParameters(array);
+ }
+ await this.db.executeAsync([...stmts.keys()]);
+ }
+
+ // The prepare* functions prepare the database bits
+ // to write the given item type. They're to return
+ // any bits they want or'd into flags, which will be
+ // prepared for writing by #prepareEvent/#prepareTodo.
+ //
+ #prepareItem(stmts, item) {
+ let flags = 0;
+
+ flags |= this.#prepareAttendees(stmts, item);
+ flags |= this.#prepareRecurrence(stmts, item);
+ flags |= this.#prepareProperties(stmts, item);
+ flags |= this.#prepareAttachments(stmts, item);
+ flags |= this.#prepareRelations(stmts, item);
+ flags |= this.#prepareAlarms(stmts, item);
+
+ if (item.isEvent()) {
+ this.#prepareEvent(stmts, item, flags);
+ } else if (item.isTodo()) {
+ this.#prepareTodo(stmts, item, flags);
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+ }
+
+ #prepareEvent(stmts, item, flags) {
+ let array = this.db.prepareAsyncStatement(stmts, this.statements.mInsertEvent);
+ let params = this.db.prepareAsyncParams(array);
+
+ this.#setupItemBaseParams(item, params);
+
+ this.#setDateParamHelper(params, "event_start", item.startDate);
+ this.#setDateParamHelper(params, "event_end", item.endDate);
+ let dtstamp = item.stampTime;
+ params.bindByName("event_stamp", dtstamp && dtstamp.nativeTime);
+
+ if (item.startDate.isDate) {
+ flags |= CAL_ITEM_FLAG.EVENT_ALLDAY;
+ }
+
+ params.bindByName("flags", flags);
+
+ array.addParams(params);
+ }
+
+ #prepareTodo(stmts, item, flags) {
+ let array = this.db.prepareAsyncStatement(stmts, this.statements.mInsertTodo);
+ let params = this.db.prepareAsyncParams(array);
+
+ this.#setupItemBaseParams(item, params);
+
+ this.#setDateParamHelper(params, "todo_entry", item.entryDate);
+ this.#setDateParamHelper(params, "todo_due", item.dueDate);
+ let dtstamp = item.stampTime;
+ params.bindByName("todo_stamp", dtstamp && dtstamp.nativeTime);
+ this.#setDateParamHelper(params, "todo_completed", item.getProperty("COMPLETED"));
+
+ params.bindByName("todo_complete", item.getProperty("PERCENT-COMPLETED"));
+
+ let someDate = item.entryDate || item.dueDate;
+ if (someDate && someDate.isDate) {
+ flags |= CAL_ITEM_FLAG.EVENT_ALLDAY;
+ }
+
+ params.bindByName("flags", flags);
+
+ array.addParams(params);
+ }
+
+ #setupItemBaseParams(item, params) {
+ params.bindByName("id", item.id);
+
+ this.#setDateParamHelper(params, "recurrence_id", item.recurrenceId);
+
+ let tmp = item.getProperty("CREATED");
+ params.bindByName("time_created", tmp && tmp.nativeTime);
+
+ tmp = item.getProperty("LAST-MODIFIED");
+ params.bindByName("last_modified", tmp && tmp.nativeTime);
+
+ params.bindByName("title", item.getProperty("SUMMARY"));
+ params.bindByName("priority", item.getProperty("PRIORITY"));
+ params.bindByName("privacy", item.getProperty("CLASS"));
+ params.bindByName("ical_status", item.getProperty("STATUS"));
+
+ params.bindByName("alarm_last_ack", item.alarmLastAck && item.alarmLastAck.nativeTime);
+ }
+
+ #prepareAttendees(stmts, item) {
+ let attendees = item.getAttendees();
+ if (item.organizer) {
+ attendees = attendees.concat([]);
+ attendees.push(item.organizer);
+ }
+ if (attendees.length > 0) {
+ let array = this.db.prepareAsyncStatement(stmts, this.statements.mInsertAttendee);
+ for (let att of attendees) {
+ let params = this.db.prepareAsyncParams(array);
+ params.bindByName("item_id", item.id);
+ this.#setDateParamHelper(params, "recurrence_id", item.recurrenceId);
+ params.bindByName("icalString", att.icalString);
+ array.addParams(params);
+ }
+
+ return CAL_ITEM_FLAG.HAS_ATTENDEES;
+ }
+
+ return 0;
+ }
+
+ #prepareProperty(stmts, item, propName, propValue) {
+ let array = this.db.prepareAsyncStatement(stmts, this.statements.mInsertProperty);
+ let params = this.db.prepareAsyncParams(array);
+ params.bindByName("key", propName);
+ let wPropValue = cal.wrapInstance(propValue, Ci.calIDateTime);
+ if (wPropValue) {
+ params.bindByName("value", wPropValue.nativeTime);
+ } else {
+ try {
+ params.bindByName("value", propValue);
+ } catch (e) {
+ // The storage service throws an NS_ERROR_ILLEGAL_VALUE in
+ // case pval is something complex (i.e not a string or
+ // number). Swallow this error, leaving the value empty.
+ if (e.result != Cr.NS_ERROR_ILLEGAL_VALUE) {
+ throw e;
+ }
+ params.bindByName("value", null);
+ }
+ }
+ params.bindByName("item_id", item.id);
+ this.#setDateParamHelper(params, "recurrence_id", item.recurrenceId);
+ array.addParams(params);
+ }
+
+ #prepareParameter(stmts, item, propName, paramName, propValue) {
+ let array = this.db.prepareAsyncStatement(stmts, this.statements.mInsertParameter);
+ let params = this.db.prepareAsyncParams(array);
+ params.bindByName("key1", propName);
+ params.bindByName("key2", paramName);
+ let wPropValue = cal.wrapInstance(propValue, Ci.calIDateTime);
+ if (wPropValue) {
+ params.bindByName("value", wPropValue.nativeTime);
+ } else {
+ try {
+ params.bindByName("value", propValue);
+ } catch (e) {
+ // The storage service throws an NS_ERROR_ILLEGAL_VALUE in
+ // case pval is something complex (i.e not a string or
+ // number). Swallow this error, leaving the value empty.
+ if (e.result != Cr.NS_ERROR_ILLEGAL_VALUE) {
+ throw e;
+ }
+ params.bindByName("value", null);
+ }
+ }
+ params.bindByName("item_id", item.id);
+ this.#setDateParamHelper(params, "recurrence_id", item.recurrenceId);
+ array.addParams(params);
+ }
+
+ #prepareProperties(stmts, item) {
+ let ret = 0;
+ for (let [name, value] of item.properties) {
+ ret = CAL_ITEM_FLAG.HAS_PROPERTIES;
+ if (item.isPropertyPromoted(name)) {
+ continue;
+ }
+ this.#prepareProperty(stmts, item, name, value);
+ // Overridden parameters still enumerate even if their value is now empty.
+ if (item.hasProperty(name)) {
+ for (let param of item.getParameterNames(name)) {
+ value = item.getPropertyParameter(name, param);
+ this.#prepareParameter(stmts, item, name, param, value);
+ }
+ }
+ }
+
+ let cats = item.getCategories();
+ if (cats.length > 0) {
+ ret = CAL_ITEM_FLAG.HAS_PROPERTIES;
+ this.#prepareProperty(stmts, item, "CATEGORIES", cal.category.arrayToString(cats));
+ }
+
+ return ret;
+ }
+
+ #prepareRecurrence(stmts, item) {
+ let flags = 0;
+
+ let rec = item.recurrenceInfo;
+ if (rec) {
+ flags = CAL_ITEM_FLAG.HAS_RECURRENCE;
+ let ritems = rec.getRecurrenceItems();
+ let array = this.db.prepareAsyncStatement(stmts, this.statements.mInsertRecurrence);
+ for (let ritem of ritems) {
+ let params = this.db.prepareAsyncParams(array);
+ params.bindByName("item_id", item.id);
+ params.bindByName("icalString", ritem.icalString);
+ array.addParams(params);
+ }
+
+ let exceptions = rec.getExceptionIds();
+ if (exceptions.length > 0) {
+ flags |= CAL_ITEM_FLAG.HAS_EXCEPTIONS;
+
+ // we need to serialize each exid as a separate
+ // event/todo; setupItemBase will handle
+ // writing the recurrenceId for us
+ for (let exid of exceptions) {
+ let ex = rec.getExceptionFor(exid);
+ if (!ex) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+ this.#prepareItem(stmts, ex);
+ }
+ }
+ } else if (item.recurrenceId && item.recurrenceId.isDate) {
+ flags |= CAL_ITEM_FLAG.RECURRENCE_ID_ALLDAY;
+ }
+
+ return flags;
+ }
+
+ #prepareAttachments(stmts, item) {
+ let attachments = item.getAttachments();
+ if (attachments && attachments.length > 0) {
+ let array = this.db.prepareAsyncStatement(stmts, this.statements.mInsertAttachment);
+ for (let att of attachments) {
+ let params = this.db.prepareAsyncParams(array);
+ this.#setDateParamHelper(params, "recurrence_id", item.recurrenceId);
+ params.bindByName("item_id", item.id);
+ params.bindByName("icalString", att.icalString);
+
+ array.addParams(params);
+ }
+ return CAL_ITEM_FLAG.HAS_ATTACHMENTS;
+ }
+ return 0;
+ }
+
+ #prepareRelations(stmts, item) {
+ let relations = item.getRelations();
+ if (relations && relations.length > 0) {
+ let array = this.db.prepareAsyncStatement(stmts, this.statements.mInsertRelation);
+ for (let rel of relations) {
+ let params = this.db.prepareAsyncParams(array);
+ this.#setDateParamHelper(params, "recurrence_id", item.recurrenceId);
+ params.bindByName("item_id", item.id);
+ params.bindByName("icalString", rel.icalString);
+
+ array.addParams(params);
+ }
+ return CAL_ITEM_FLAG.HAS_RELATIONS;
+ }
+ return 0;
+ }
+
+ #prepareAlarms(stmts, item) {
+ let alarms = item.getAlarms();
+ if (alarms.length < 1) {
+ return 0;
+ }
+
+ let array = this.db.prepareAsyncStatement(stmts, this.statements.mInsertAlarm);
+ for (let alarm of alarms) {
+ let params = this.db.prepareAsyncParams(array);
+ this.#setDateParamHelper(params, "recurrence_id", item.recurrenceId);
+ params.bindByName("item_id", item.id);
+ params.bindByName("icalString", alarm.icalString);
+
+ array.addParams(params);
+ }
+
+ return CAL_ITEM_FLAG.HAS_ALARMS;
+ }
+
+ /**
+ * Deletes the item with the given item id.
+ *
+ * @param {string} id The id of the item to delete.
+ * @param {boolean} keepMeta If true, leave metadata for the item.
+ */
+ async deleteItemById(id, keepMeta) {
+ let stmts = [];
+ this.db.prepareItemStatement(stmts, this.statements.mDeleteAttendees, "item_id", id);
+ this.db.prepareItemStatement(stmts, this.statements.mDeleteProperties, "item_id", id);
+ this.db.prepareItemStatement(stmts, this.statements.mDeleteRecurrence, "item_id", id);
+ this.db.prepareItemStatement(stmts, this.statements.mDeleteEvent, "id", id);
+ this.db.prepareItemStatement(stmts, this.statements.mDeleteTodo, "id", id);
+ this.db.prepareItemStatement(stmts, this.statements.mDeleteAttachments, "item_id", id);
+ this.db.prepareItemStatement(stmts, this.statements.mDeleteRelations, "item_id", id);
+ if (!keepMeta) {
+ this.db.prepareItemStatement(stmts, this.statements.mDeleteMetaData, "item_id", id);
+ }
+ this.db.prepareItemStatement(stmts, this.statements.mDeleteAlarms, "item_id", id);
+ await this.db.executeAsync(stmts);
+ }
+}
diff --git a/comm/calendar/providers/storage/CalStorageMetaDataModel.jsm b/comm/calendar/providers/storage/CalStorageMetaDataModel.jsm
new file mode 100644
index 0000000000..b004b3d45b
--- /dev/null
+++ b/comm/calendar/providers/storage/CalStorageMetaDataModel.jsm
@@ -0,0 +1,94 @@
+/* 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 = ["CalStorageMetaDataModel"];
+
+var { CalStorageModelBase } = ChromeUtils.import(
+ "resource:///modules/calendar/CalStorageModelBase.jsm"
+);
+
+/**
+ * CalStorageMetaDataModel provides methods for manipulating the metadata stored
+ * on items.
+ */
+class CalStorageMetaDataModel extends CalStorageModelBase {
+ /**
+ * Adds meta data for an item.
+ *
+ * @param {string} id
+ * @param {string} value
+ */
+ addMetaData(id, value) {
+ try {
+ this.db.prepareStatement(this.statements.mInsertMetaData);
+ let params = this.statements.mInsertMetaData.params;
+ params.item_id = id;
+ params.value = value;
+ this.statements.mInsertMetaData.executeStep();
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ this.db.logError("Unknown error!", e);
+ } else {
+ // The storage service throws an NS_ERROR_ILLEGAL_VALUE in
+ // case pval is something complex (i.e not a string or
+ // number). Swallow this error, leaving the value empty.
+ this.db.logError("Error setting metadata for id " + id + "!", e);
+ }
+ } finally {
+ this.statements.mInsertMetaData.reset();
+ }
+ }
+
+ /**
+ * Deletes meta data for an item using its id.
+ */
+ deleteMetaDataById(id) {
+ this.db.executeSyncItemStatement(this.statements.mDeleteMetaData, "item_id", id);
+ }
+
+ /**
+ * Gets meta data for an item given its id.
+ *
+ * @param {string} id
+ */
+ getMetaData(id) {
+ let query = this.statements.mSelectMetaData;
+ let value = null;
+ try {
+ this.db.prepareStatement(query);
+ query.params.item_id = id;
+
+ if (query.executeStep()) {
+ value = query.row.value;
+ }
+ } catch (e) {
+ this.db.logError("Error getting metadata for id " + id + "!", e);
+ } finally {
+ query.reset();
+ }
+
+ return value;
+ }
+
+ /**
+ * Returns the meta data for all items.
+ *
+ * @param {string} key - Specifies which column to return.
+ */
+ getAllMetaData(key) {
+ let query = this.statements.mSelectAllMetaData;
+ let results = [];
+ try {
+ this.db.prepareStatement(query);
+ while (query.executeStep()) {
+ results.push(query.row[key]);
+ }
+ } catch (e) {
+ this.db.logError(`Error getting all metadata ${key == "item_id" ? "IDs" : "values"} ` + e);
+ } finally {
+ query.reset();
+ }
+ return results;
+ }
+}
diff --git a/comm/calendar/providers/storage/CalStorageModelBase.jsm b/comm/calendar/providers/storage/CalStorageModelBase.jsm
new file mode 100644
index 0000000000..cf24606192
--- /dev/null
+++ b/comm/calendar/providers/storage/CalStorageModelBase.jsm
@@ -0,0 +1,65 @@
+/* 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 = ["CalStorageModelBase"];
+
+/**
+ * CalStorageModelBase is the parent class for the storage calendar models.
+ * The idea here is to leave most of the adjustments and integrity checks to
+ * CalStorageCalendar (or other classes) while focusing mostly on
+ * retrieval/persistence in the children of this class.
+ */
+class CalStorageModelBase {
+ /**
+ * @type {CalStorageDatabase}
+ */
+ db = null;
+
+ /**
+ * @type {CalStorageStatements}
+ */
+ statements = null;
+
+ /**
+ * @type {calICalendar}
+ */
+ calendar = null;
+
+ /**
+ * @param {CalStorageDatabase} db
+ * @param {CalStorageStatements} statements
+ * @param {calICalendar} calendar
+ *
+ * @throws - If unable to initialize SQL statements.
+ */
+ constructor(db, statements, calendar) {
+ this.db = db;
+ this.statements = statements;
+ this.calendar = calendar;
+ }
+
+ /**
+ * Delete all data stored for the calendar this model's database connection
+ * is associated with.
+ */
+ async deleteCalendar() {
+ let stmts = [];
+ if (this.statements.mDeleteEventExtras) {
+ for (let stmt of this.statements.mDeleteEventExtras) {
+ stmts.push(this.db.prepareStatement(stmt));
+ }
+ }
+
+ if (this.statements.mDeleteTodoExtras) {
+ for (let stmt of this.statements.mDeleteTodoExtras) {
+ stmts.push(this.db.prepareStatement(stmt));
+ }
+ }
+
+ stmts.push(this.db.prepareStatement(this.statements.mDeleteAllEvents));
+ stmts.push(this.db.prepareStatement(this.statements.mDeleteAllTodos));
+ stmts.push(this.db.prepareStatement(this.statements.mDeleteAllMetaData));
+ await this.db.executeAsync(stmts);
+ }
+}
diff --git a/comm/calendar/providers/storage/CalStorageModelFactory.jsm b/comm/calendar/providers/storage/CalStorageModelFactory.jsm
new file mode 100644
index 0000000000..cf36791eba
--- /dev/null
+++ b/comm/calendar/providers/storage/CalStorageModelFactory.jsm
@@ -0,0 +1,52 @@
+/* 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 = ["CalStorageModelFactory"];
+
+var { CalStorageItemModel } = ChromeUtils.import(
+ "resource:///modules/calendar/CalStorageItemModel.jsm"
+);
+var { CalStorageCachedItemModel } = ChromeUtils.import(
+ "resource:///modules/calendar/CalStorageCachedItemModel.jsm"
+);
+var { CalStorageOfflineModel } = ChromeUtils.import(
+ "resource:///modules/calendar/CalStorageOfflineModel.jsm"
+);
+var { CalStorageMetaDataModel } = ChromeUtils.import(
+ "resource:///modules/calendar/CalStorageMetaDataModel.jsm"
+);
+
+/**
+ * CalStorageModelFactory provides a convenience method for creating instances
+ * of the storage calendar models. Use to avoid having to import each one
+ * directly.
+ */
+class CalStorageModelFactory {
+ /**
+ * Creates an instance of a CalStorageModel for the specified type.
+ *
+ * @param {"item"|"offline"|"metadata"} type - The model type desired.
+ * @param {mozIStorageAsyncConnection} db - The database connection to use.
+ * @param {CalStorageStatement} stmts
+ * @param {CalStorageCalendar} calendar - The calendar associated with the
+ * model.
+ */
+ static createInstance(type, db, stmts, calendar) {
+ switch (type) {
+ case "item":
+ return new CalStorageItemModel(db, stmts, calendar);
+
+ case "cached-item":
+ return new CalStorageCachedItemModel(db, stmts, calendar);
+
+ case "offline":
+ return new CalStorageOfflineModel(db, stmts, calendar);
+
+ case "metadata":
+ return new CalStorageMetaDataModel(db, stmts, calendar);
+ }
+
+ throw new Error(`Unknown model type "${type}" specified!`);
+ }
+}
diff --git a/comm/calendar/providers/storage/CalStorageOfflineModel.jsm b/comm/calendar/providers/storage/CalStorageOfflineModel.jsm
new file mode 100644
index 0000000000..23f6cd5330
--- /dev/null
+++ b/comm/calendar/providers/storage/CalStorageOfflineModel.jsm
@@ -0,0 +1,54 @@
+/* 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 = ["CalStorageOfflineModel"];
+
+var { CalStorageModelBase } = ChromeUtils.import(
+ "resource:///modules/calendar/CalStorageModelBase.jsm"
+);
+
+/**
+ * CalStorageOfflineModel provides methods for manipulating the offline flags
+ * of items.
+ */
+class CalStorageOfflineModel extends CalStorageModelBase {
+ /**
+ * Returns the offline_journal column value for an item.
+ *
+ * @param {calIItemBase} item
+ *
+ * @returns {number}
+ */
+ async getItemOfflineFlag(item) {
+ let flag = null;
+ let query = item.isEvent() ? this.statements.mSelectEvent : this.statements.mSelectTodo;
+ this.db.prepareStatement(query);
+ query.params.id = item.id;
+ await this.db.executeAsync(query, row => {
+ flag = row.getResultByName("offline_journal") || null;
+ });
+ return flag;
+ }
+
+ /**
+ * Sets the offline_journal column value for an item.
+ *
+ * @param {calIItemBase} item
+ * @param {number} flag
+ */
+ async setOfflineJournalFlag(item, flag) {
+ let id = item.id;
+ let query = item.isEvent()
+ ? this.statements.mEditEventOfflineFlag
+ : this.statements.mEditTodoOfflineFlag;
+ this.db.prepareStatement(query);
+ query.params.id = id;
+ query.params.offline_journal = flag || null;
+ try {
+ await this.db.executeAsync(query);
+ } catch (e) {
+ this.db.logError("Error setting offline journal flag for " + item.title, e);
+ }
+ }
+}
diff --git a/comm/calendar/providers/storage/CalStorageStatements.jsm b/comm/calendar/providers/storage/CalStorageStatements.jsm
new file mode 100644
index 0000000000..4906e036e3
--- /dev/null
+++ b/comm/calendar/providers/storage/CalStorageStatements.jsm
@@ -0,0 +1,751 @@
+/* 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 = ["CalStorageStatements"];
+
+const cICL = Ci.calIChangeLog;
+
+/**
+ * CalStorageStatements contains the mozIStorageBaseStatements used by the
+ * various storage calendar models. Remember to call the finalize() method when
+ * shutting down the db.
+ */
+class CalStorageStatements {
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectEvent = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectTodo = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement} mSelectNonRecurringEventsByRange
+ */
+ mSelectNonRecurringEventsByRange = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement} mSelectNonRecurringTodosByRange
+ */
+ mSelectNonRecurringTodosByRange = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAttendeesForItem = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAttendeesForItemWithRecurrenceId = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAllAttendees = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectPropertiesForItem = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectPropertiesForItemWithRecurrenceId = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAllProperties = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectParametersForItem = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectParametersForItemWithRecurrenceId = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAllParameters = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectRecurrenceForItem = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAllRecurrences = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectEventsWithRecurrence = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectTodosWithRecurrence = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectEventExceptions = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAllEventExceptions = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectTodoExceptions = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAllTodoExceptions = null;
+
+ /**
+ * @type {mozIStorageStatement}
+ */
+ mSelectMetaData = null;
+
+ /**
+ * @type {mozIStorageStatement}
+ */
+ mSelectAllMetaData = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectRelationsForItemWithRecurrenceId = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAllRelations = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectRelationsForItem = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAlarmsForItem = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAlarmsForItemWithRecurrenceId = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAllAlarms = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAttachmentsForItem = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAttachmentsForItemWithRecurrenceId = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mSelectAllAttachments = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mInsertEvent = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mInsertTodo = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mInsertProperty = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mInsertParameter = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mInsertAttendee = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mInsertRecurrence = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mInsertAttachment = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mInsertRelation = null;
+
+ /**
+ * @type {mozIStorageStatement}
+ */
+ mInsertMetaData = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mInsertAlarm = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mEditEventOfflineFlag = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mEditTodoOfflineFlag = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteEvent = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteTodo = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteAttendees = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteProperties = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteParameters = null;
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteRecurrence = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteAttachments = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteRelations = null;
+
+ /**
+ * @type {mozIStorageStatement}
+ */
+ mDeleteMetaData = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteAlarms = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement[]}
+ */
+ mDeleteEventExtras = [];
+
+ /**
+ * @type {mozIStorageAsyncStatement[]}
+ */
+ mDeleteTodoExtras = [];
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteAllEvents = null;
+
+ /**
+ * @type {mozIStorageAsyncStatement}
+ */
+ mDeleteAllTodos = null;
+
+ /**
+ * @type {mozIStorageStatement}
+ */
+ mDeleteAllMetaData = null;
+
+ /**
+ * @param {CalStorageDatabase} db
+ *
+ * @throws - If unable to initialize SQL statements.
+ */
+ constructor(db) {
+ this.mSelectEvent = db.createAsyncStatement(
+ `SELECT * FROM cal_events
+ WHERE id = :id
+ AND cal_id = :cal_id
+ AND recurrence_id IS NULL
+ LIMIT 1`
+ );
+
+ this.mSelectTodo = db.createAsyncStatement(
+ `SELECT * FROM cal_todos
+ WHERE id = :id
+ AND cal_id = :cal_id
+ AND recurrence_id IS NULL
+ LIMIT 1`
+ );
+
+ // The more readable version of the next where-clause is:
+ // WHERE ((event_end > :range_start OR
+ // (event_end = :range_start AND
+ // event_start = :range_start))
+ // AND event_start < :range_end)
+ //
+ // but that doesn't work with floating start or end times. The logic
+ // is the same though.
+ // For readability, a few helpers:
+ let floatingEventStart = "event_start_tz = 'floating' AND event_start";
+ let nonFloatingEventStart = "event_start_tz != 'floating' AND event_start";
+ let floatingEventEnd = "event_end_tz = 'floating' AND event_end";
+ let nonFloatingEventEnd = "event_end_tz != 'floating' AND event_end";
+ // The query needs to take both floating and non floating into account.
+ this.mSelectNonRecurringEventsByRange = db.createAsyncStatement(
+ `SELECT * FROM cal_events
+ WHERE
+ ((${floatingEventEnd} > :range_start + :start_offset) OR
+ (${nonFloatingEventEnd} > :range_start) OR
+ (((${floatingEventEnd} = :range_start + :start_offset) OR
+ (${nonFloatingEventEnd} = :range_start)) AND
+ ((${floatingEventStart} = :range_start + :start_offset) OR
+ (${nonFloatingEventStart} = :range_start))))
+ AND
+ ((${floatingEventStart} < :range_end + :end_offset) OR
+ (${nonFloatingEventStart} < :range_end))
+ AND cal_id = :cal_id AND flags & 16 == 0 AND recurrence_id IS NULL
+ AND ((:offline_journal IS NULL
+ AND (offline_journal IS NULL
+ OR offline_journal != ${cICL.OFFLINE_FLAG_DELETED_RECORD}))
+ OR (offline_journal == :offline_journal))`
+ );
+
+ //
+ // WHERE (due > rangeStart AND (entry IS NULL OR entry < rangeEnd)) OR
+ // (due = rangeStart AND (entry IS NULL OR entry = rangeStart)) OR
+ // (due IS NULL AND (entry >= rangeStart AND entry < rangeEnd)) OR
+ // (entry IS NULL AND (completed > rangeStart OR completed IS NULL))
+ //
+ let floatingTodoEntry = "todo_entry_tz = 'floating' AND todo_entry";
+ let nonFloatingTodoEntry = "todo_entry_tz != 'floating' AND todo_entry";
+ let floatingTodoDue = "todo_due_tz = 'floating' AND todo_due";
+ let nonFloatingTodoDue = "todo_due_tz != 'floating' AND todo_due";
+ let floatingCompleted = "todo_completed_tz = 'floating' AND todo_completed";
+ let nonFloatingCompleted = "todo_completed_tz != 'floating' AND todo_completed";
+
+ this.mSelectNonRecurringTodosByRange = db.createAsyncStatement(
+ `SELECT * FROM cal_todos
+ WHERE
+ ((((${floatingTodoDue} > :range_start + :start_offset) OR
+ (${nonFloatingTodoDue} > :range_start)) AND
+ ((todo_entry IS NULL) OR
+ ((${floatingTodoEntry} < :range_end + :end_offset) OR
+ (${nonFloatingTodoEntry} < :range_end)))) OR
+ (((${floatingTodoDue} = :range_start + :start_offset) OR
+ (${nonFloatingTodoDue} = :range_start)) AND
+ ((todo_entry IS NULL) OR
+ ((${floatingTodoEntry} = :range_start + :start_offset) OR
+ (${nonFloatingTodoEntry} = :range_start)))) OR
+ ((todo_due IS NULL) AND
+ (((${floatingTodoEntry} >= :range_start + :start_offset) OR
+ (${nonFloatingTodoEntry} >= :range_start)) AND
+ ((${floatingTodoEntry} < :range_end + :end_offset) OR
+ (${nonFloatingTodoEntry} < :range_end)))) OR
+ ((todo_entry IS NULL) AND
+ (((${floatingCompleted} > :range_start + :start_offset) OR
+ (${nonFloatingCompleted} > :range_start)) OR
+ (todo_completed IS NULL))))
+ AND cal_id = :cal_id AND flags & 16 == 0 AND recurrence_id IS NULL
+ AND ((:offline_journal IS NULL
+ AND (offline_journal IS NULL
+ OR offline_journal != ${cICL.OFFLINE_FLAG_DELETED_RECORD}))
+ OR (offline_journal == :offline_journal))`
+ );
+
+ this.mSelectEventsWithRecurrence = db.createAsyncStatement(
+ `SELECT * FROM cal_events
+ WHERE flags & 16 == 16
+ AND cal_id = :cal_id
+ AND recurrence_id is NULL`
+ );
+
+ this.mSelectTodosWithRecurrence = db.createAsyncStatement(
+ `SELECT * FROM cal_todos
+ WHERE flags & 16 == 16
+ AND cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+
+ this.mSelectEventExceptions = db.createAsyncStatement(
+ `SELECT * FROM cal_events
+ WHERE id = :id
+ AND cal_id = :cal_id
+ AND recurrence_id IS NOT NULL`
+ );
+ this.mSelectAllEventExceptions = db.createAsyncStatement(
+ `SELECT * FROM cal_events
+ WHERE cal_id = :cal_id
+ AND recurrence_id IS NOT NULL`
+ );
+
+ this.mSelectTodoExceptions = db.createAsyncStatement(
+ `SELECT * FROM cal_todos
+ WHERE id = :id
+ AND cal_id = :cal_id
+ AND recurrence_id IS NOT NULL`
+ );
+ this.mSelectAllTodoExceptions = db.createAsyncStatement(
+ `SELECT * FROM cal_todos
+ WHERE cal_id = :cal_id
+ AND recurrence_id IS NOT NULL`
+ );
+
+ this.mSelectAttendeesForItem = db.createAsyncStatement(
+ `SELECT * FROM cal_attendees
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+
+ this.mSelectAttendeesForItemWithRecurrenceId = db.createAsyncStatement(
+ `SELECT * FROM cal_attendees
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id = :recurrence_id
+ AND recurrence_id_tz = :recurrence_id_tz`
+ );
+ this.mSelectAllAttendees = db.createAsyncStatement(
+ `SELECT item_id, icalString FROM cal_attendees
+ WHERE cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+
+ this.mSelectPropertiesForItem = db.createAsyncStatement(
+ `SELECT * FROM cal_properties
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+ this.mSelectPropertiesForItemWithRecurrenceId = db.createAsyncStatement(
+ `SELECT * FROM cal_properties
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id = :recurrence_id
+ AND recurrence_id_tz = :recurrence_id_tz`
+ );
+ this.mSelectAllProperties = db.createAsyncStatement(
+ `SELECT item_id, key, value FROM cal_properties
+ WHERE cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+
+ this.mSelectParametersForItem = db.createAsyncStatement(
+ `SELECT * FROM cal_parameters
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+ this.mSelectParametersForItemWithRecurrenceId = db.createAsyncStatement(
+ `SELECT * FROM cal_parameters
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id = :recurrence_id
+ AND recurrence_id_tz = :recurrence_id_tz`
+ );
+ this.mSelectAllParameters = db.createAsyncStatement(
+ `SELECT item_id, key1, key2, value FROM cal_parameters
+ WHERE cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+
+ this.mSelectRecurrenceForItem = db.createAsyncStatement(
+ `SELECT * FROM cal_recurrence
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id`
+ );
+ this.mSelectAllRecurrences = db.createAsyncStatement(
+ `SELECT item_id, icalString FROM cal_recurrence
+ WHERE cal_id = :cal_id`
+ );
+
+ this.mSelectAttachmentsForItem = db.createAsyncStatement(
+ `SELECT * FROM cal_attachments
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+ this.mSelectAttachmentsForItemWithRecurrenceId = db.createAsyncStatement(
+ `SELECT * FROM cal_attachments
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id = :recurrence_id
+ AND recurrence_id_tz = :recurrence_id_tz`
+ );
+ this.mSelectAllAttachments = db.createAsyncStatement(
+ `SELECT item_id, icalString FROM cal_attachments
+ WHERE cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+
+ this.mSelectRelationsForItem = db.createAsyncStatement(
+ `SELECT * FROM cal_relations
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+ this.mSelectRelationsForItemWithRecurrenceId = db.createAsyncStatement(
+ `SELECT * FROM cal_relations
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id = :recurrence_id
+ AND recurrence_id_tz = :recurrence_id_tz`
+ );
+ this.mSelectAllRelations = db.createAsyncStatement(
+ `SELECT item_id, icalString FROM cal_relations
+ WHERE cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+
+ this.mSelectMetaData = db.createStatement(
+ `SELECT * FROM cal_metadata
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id`
+ );
+
+ this.mSelectAllMetaData = db.createStatement(
+ `SELECT * FROM cal_metadata
+ WHERE cal_id = :cal_id`
+ );
+
+ this.mSelectAlarmsForItem = db.createAsyncStatement(
+ `SELECT icalString FROM cal_alarms
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+
+ this.mSelectAlarmsForItemWithRecurrenceId = db.createAsyncStatement(
+ `SELECT icalString FROM cal_alarms
+ WHERE item_id = :item_id
+ AND cal_id = :cal_id
+ AND recurrence_id = :recurrence_id
+ AND recurrence_id_tz = :recurrence_id_tz`
+ );
+ this.mSelectAllAlarms = db.createAsyncStatement(
+ `SELECT item_id, icalString FROM cal_alarms
+ WHERE cal_id = :cal_id
+ AND recurrence_id IS NULL`
+ );
+
+ // insert statements
+ this.mInsertEvent = db.createAsyncStatement(
+ `INSERT INTO cal_events
+ (cal_id, id, time_created, last_modified,
+ title, priority, privacy, ical_status, flags,
+ event_start, event_start_tz, event_end, event_end_tz, event_stamp,
+ recurrence_id, recurrence_id_tz, alarm_last_ack)
+ VALUES (:cal_id, :id, :time_created, :last_modified,
+ :title, :priority, :privacy, :ical_status, :flags,
+ :event_start, :event_start_tz, :event_end, :event_end_tz, :event_stamp,
+ :recurrence_id, :recurrence_id_tz, :alarm_last_ack)`
+ );
+
+ this.mInsertTodo = db.createAsyncStatement(
+ `INSERT INTO cal_todos
+ (cal_id, id, time_created, last_modified,
+ title, priority, privacy, ical_status, flags,
+ todo_entry, todo_entry_tz, todo_due, todo_due_tz, todo_stamp,
+ todo_completed, todo_completed_tz, todo_complete,
+ recurrence_id, recurrence_id_tz, alarm_last_ack)
+ VALUES (:cal_id, :id, :time_created, :last_modified,
+ :title, :priority, :privacy, :ical_status, :flags,
+ :todo_entry, :todo_entry_tz, :todo_due, :todo_due_tz, :todo_stamp,
+ :todo_completed, :todo_completed_tz, :todo_complete,
+ :recurrence_id, :recurrence_id_tz, :alarm_last_ack)`
+ );
+ this.mInsertProperty = db.createAsyncStatement(
+ `INSERT INTO cal_properties (cal_id, item_id, recurrence_id, recurrence_id_tz, key, value)
+ VALUES (:cal_id, :item_id, :recurrence_id, :recurrence_id_tz, :key, :value)`
+ );
+ this.mInsertParameter = db.createAsyncStatement(
+ `INSERT INTO cal_parameters (cal_id, item_id, recurrence_id, recurrence_id_tz, key1, key2, value)
+ VALUES (:cal_id, :item_id, :recurrence_id, :recurrence_id_tz, :key1, :key2, :value)`
+ );
+ this.mInsertAttendee = db.createAsyncStatement(
+ `INSERT INTO cal_attendees
+ (cal_id, item_id, recurrence_id, recurrence_id_tz, icalString)
+ VALUES (:cal_id, :item_id, :recurrence_id, :recurrence_id_tz, :icalString)`
+ );
+ this.mInsertRecurrence = db.createAsyncStatement(
+ `INSERT INTO cal_recurrence
+ (cal_id, item_id, icalString)
+ VALUES (:cal_id, :item_id, :icalString)`
+ );
+
+ this.mInsertAttachment = db.createAsyncStatement(
+ `INSERT INTO cal_attachments
+ (cal_id, item_id, icalString, recurrence_id, recurrence_id_tz)
+ VALUES (:cal_id, :item_id, :icalString, :recurrence_id, :recurrence_id_tz)`
+ );
+
+ this.mInsertRelation = db.createAsyncStatement(
+ `INSERT INTO cal_relations
+ (cal_id, item_id, icalString, recurrence_id, recurrence_id_tz)
+ VALUES (:cal_id, :item_id, :icalString, :recurrence_id, :recurrence_id_tz)`
+ );
+
+ this.mInsertMetaData = db.createStatement(
+ `INSERT INTO cal_metadata
+ (cal_id, item_id, value)
+ VALUES (:cal_id, :item_id, :value)`
+ );
+
+ this.mInsertAlarm = db.createAsyncStatement(
+ `INSERT INTO cal_alarms
+ (cal_id, item_id, icalString, recurrence_id, recurrence_id_tz)
+ VALUES (:cal_id, :item_id, :icalString, :recurrence_id, :recurrence_id_tz)`
+ );
+ // Offline Operations
+ this.mEditEventOfflineFlag = db.createStatement(
+ `UPDATE cal_events SET offline_journal = :offline_journal
+ WHERE id = :id
+ AND cal_id = :cal_id`
+ );
+
+ this.mEditTodoOfflineFlag = db.createStatement(
+ `UPDATE cal_todos SET offline_journal = :offline_journal
+ WHERE id = :id
+ AND cal_id = :cal_id`
+ );
+
+ // delete statements
+ this.mDeleteEvent = db.createAsyncStatement(
+ "DELETE FROM cal_events WHERE id = :id AND cal_id = :cal_id"
+ );
+ this.mDeleteTodo = db.createAsyncStatement(
+ "DELETE FROM cal_todos WHERE id = :id AND cal_id = :cal_id"
+ );
+ this.mDeleteAttendees = db.createAsyncStatement(
+ "DELETE FROM cal_attendees WHERE item_id = :item_id AND cal_id = :cal_id"
+ );
+ this.mDeleteProperties = db.createAsyncStatement(
+ "DELETE FROM cal_properties WHERE item_id = :item_id AND cal_id = :cal_id"
+ );
+ this.mDeleteParameters = db.createAsyncStatement(
+ "DELETE FROM cal_parameters WHERE item_id = :item_id AND cal_id = :cal_id"
+ );
+ this.mDeleteRecurrence = db.createAsyncStatement(
+ "DELETE FROM cal_recurrence WHERE item_id = :item_id AND cal_id = :cal_id"
+ );
+ this.mDeleteAttachments = db.createAsyncStatement(
+ "DELETE FROM cal_attachments WHERE item_id = :item_id AND cal_id = :cal_id"
+ );
+ this.mDeleteRelations = db.createAsyncStatement(
+ "DELETE FROM cal_relations WHERE item_id = :item_id AND cal_id = :cal_id"
+ );
+ this.mDeleteMetaData = db.createStatement(
+ "DELETE FROM cal_metadata WHERE item_id = :item_id AND cal_id = :cal_id"
+ );
+ this.mDeleteAlarms = db.createAsyncStatement(
+ "DELETE FROM cal_alarms WHERE item_id = :item_id AND cal_id = :cal_id"
+ );
+
+ // These are only used when deleting an entire calendar
+ let extrasTables = [
+ "cal_attendees",
+ "cal_properties",
+ "cal_parameters",
+ "cal_recurrence",
+ "cal_attachments",
+ "cal_metadata",
+ "cal_relations",
+ "cal_alarms",
+ ];
+
+ this.mDeleteEventExtras = [];
+ this.mDeleteTodoExtras = [];
+
+ for (let table in extrasTables) {
+ this.mDeleteEventExtras[table] = db.createAsyncStatement(
+ `DELETE FROM ${extrasTables[table]}
+ WHERE item_id IN
+ (SELECT id FROM cal_events WHERE cal_id = :cal_id)
+ AND cal_id = :cal_id`
+ );
+ this.mDeleteTodoExtras[table] = db.createAsyncStatement(
+ `DELETE FROM ${extrasTables[table]}
+ WHERE item_id IN
+ (SELECT id FROM cal_todos WHERE cal_id = :cal_id)
+ AND cal_id = :cal_id`
+ );
+ }
+
+ // Note that you must delete the "extras" _first_ using the above two
+ // statements, before you delete the events themselves.
+ this.mDeleteAllEvents = db.createAsyncStatement(
+ "DELETE from cal_events WHERE cal_id = :cal_id"
+ );
+ this.mDeleteAllTodos = db.createAsyncStatement("DELETE from cal_todos WHERE cal_id = :cal_id");
+
+ this.mDeleteAllMetaData = db.createStatement("DELETE FROM cal_metadata WHERE cal_id = :cal_id");
+ }
+
+ /**
+ * Ensures all Db statements are properly cleaned up before shutdown by
+ * calling their finalize() method.
+ */
+ finalize() {
+ for (let key of Object.keys(this)) {
+ if (this[key] instanceof Ci.mozIStorageBaseStatement) {
+ this[key].finalize();
+ }
+ }
+ for (let stmt of this.mDeleteEventExtras) {
+ stmt.finalize();
+ }
+ for (let stmt of this.mDeleteTodoExtras) {
+ stmt.finalize();
+ }
+ }
+}
diff --git a/comm/calendar/providers/storage/calStorageHelpers.jsm b/comm/calendar/providers/storage/calStorageHelpers.jsm
new file mode 100644
index 0000000000..2f4e303beb
--- /dev/null
+++ b/comm/calendar/providers/storage/calStorageHelpers.jsm
@@ -0,0 +1,121 @@
+/* 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 { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+const { CalTimezone } = ChromeUtils.import("resource:///modules/CalTimezone.jsm");
+
+var { ICAL } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm");
+
+const EXPORTED_SYMBOLS = ["CAL_ITEM_FLAG", "textToDate", "getTimezone", "newDateTime"];
+
+// Storage flags. These are used in the Database |flags| column to give
+// information about the item's features. For example, if the item has
+// attachments, the HAS_ATTACHMENTS flag is added to the flags column.
+var CAL_ITEM_FLAG = {
+ PRIVATE: 1,
+ HAS_ATTENDEES: 2,
+ HAS_PROPERTIES: 4,
+ EVENT_ALLDAY: 8,
+ HAS_RECURRENCE: 16,
+ HAS_EXCEPTIONS: 32,
+ HAS_ATTACHMENTS: 64,
+ HAS_RELATIONS: 128,
+ HAS_ALARMS: 256,
+ RECURRENCE_ID_ALLDAY: 512,
+};
+
+// The cache of foreign timezones
+var gForeignTimezonesCache = {};
+
+/**
+ * Transforms the text representation of this date object to a calIDateTime
+ * object.
+ *
+ * @param text The text to transform.
+ * @returns The resulting calIDateTime.
+ */
+function textToDate(text) {
+ let textval;
+ let timezone = "UTC";
+
+ if (text[0] == "Z") {
+ let strs = text.substr(2).split(":");
+ textval = parseInt(strs[0], 10);
+ timezone = strs[1].replace(/%:/g, ":").replace(/%%/g, "%");
+ } else {
+ textval = parseInt(text.substr(2), 10);
+ }
+
+ let date;
+ if (text[0] == "U" || text[0] == "Z") {
+ date = newDateTime(textval, timezone);
+ } else if (text[0] == "L") {
+ // is local time
+ date = newDateTime(textval, "floating");
+ }
+
+ if (text[1] == "D") {
+ date.isDate = true;
+ }
+ return date;
+}
+
+/**
+ * Gets the timezone for the given definition or identifier
+ *
+ * @param aTimezone The timezone data
+ * @returns The calITimezone object
+ */
+function getTimezone(aTimezone) {
+ let timezone = null;
+ if (aTimezone.startsWith("BEGIN:VTIMEZONE")) {
+ timezone = gForeignTimezonesCache[aTimezone]; // using full definition as key
+ if (!timezone) {
+ timezone = new CalTimezone(
+ ICAL.Timezone.fromData({
+ component: aTimezone,
+ })
+ );
+ gForeignTimezonesCache[aTimezone] = timezone;
+ }
+ } else {
+ timezone = cal.timezoneService.getTimezone(aTimezone);
+ }
+ return timezone;
+}
+
+/**
+ * Creates a new calIDateTime from the given native time and optionally
+ * the passed timezone. The timezone can either be the TZID of the timezone (in
+ * this case the timezone service will be asked for the definition), or a string
+ * representation of the timezone component (i.e a VTIMEZONE component).
+ *
+ * @param aNativeTime The native time, in microseconds
+ * @param aTimezone The timezone identifier or definition.
+ */
+function newDateTime(aNativeTime, aTimezone) {
+ let date = cal.createDateTime();
+
+ // Bug 751821 - Dates before 1970 were incorrectly stored with an unsigned nativeTime value, we need to
+ // convert back to a negative value
+ if (aNativeTime > 9223372036854776000) {
+ cal.WARN("[calStorageCalendar] Converting invalid native time value: " + aNativeTime);
+ aNativeTime = -9223372036854776000 + (aNativeTime - 9223372036854776000);
+ // Round to nearest second to fix microsecond rounding errors
+ aNativeTime = Math.round(aNativeTime / 1000000) * 1000000;
+ }
+
+ date.nativeTime = aNativeTime;
+ if (aTimezone) {
+ let timezone = getTimezone(aTimezone);
+ if (timezone) {
+ date = date.getInTimezone(timezone);
+ } else {
+ cal.ASSERT(false, "Timezone not available: " + aTimezone);
+ }
+ } else {
+ date.timezone = cal.dtz.floating;
+ }
+ return date;
+}
diff --git a/comm/calendar/providers/storage/calStorageUpgrade.jsm b/comm/calendar/providers/storage/calStorageUpgrade.jsm
new file mode 100644
index 0000000000..b5c23bd648
--- /dev/null
+++ b/comm/calendar/providers/storage/calStorageUpgrade.jsm
@@ -0,0 +1,1889 @@
+/* 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/. */
+
+/**
+ * Welcome to the storage database migration.
+ *
+ * If you would like to change anything in the database schema, you must follow
+ * some steps to make sure that upgrading from old versions works fine.
+ *
+ * First of all you must increment the DB_SCHEMA_VERSION variable below. Then
+ * you must write your upgrader. To do this, create a new function and add it to
+ * the upgrade object, similar to the existing upgraders below. An example is
+ * given below.
+ *
+ * An upgrader MUST update both the database (if it is passed) AND the table
+ * data javascript object. An example for a such object is in the v1/v2
+ * upgrader. The process of upgrading calls the latest upgrader with the
+ * database object and the current database version. The whole chain of
+ * upgraders is then called (down to v1). The first upgrader (v1/v2) provides
+ * the basic table data object. Each further upgrader then updates this object
+ * to correspond with the database tables and columns. No actual database calls
+ * are made until the first upgrader with a higher version than the current
+ * database version is called. When this version is arrived, both the table data
+ * object and the database are updated. This process continues until the
+ * database is at the latest version.
+ *
+ * Note that your upgrader is not necessarily called with a database object,
+ * for example if the user's database is already at a higher version. In this
+ * case your upgrader is called to compile the table data object. To make
+ * calling code easier, there are a bunch of helper functions below that can be
+ * called with a null database object and only call the database object if it is
+ * not null. If you need to call new functions on the database object, check out
+ * the createDBDelegate function below.
+ *
+ * When adding new tables to the table data object, please note that there is a
+ * special prefix for indexes. These are also kept in the table data object to
+ * make sure that getAllSql also includes CREATE INDEX statements. New tables
+ * MUST NOT be prefixed with "idx_". If you would like to add a new index,
+ * please use the createIndex function.
+ *
+ * The basic structure for an upgrader is (NN is current version, XX = NN - 1)
+ *
+ * upgrader.vNN = function upgrade_vNN(db, version) {
+ * let tbl = upgrade.vXX(version < XX && db, version);
+ * LOGdb(db, "Storage: Upgrading to vNN");
+ *
+ * beginTransaction(db);
+ * try {
+ * // Do stuff here
+ * setDbVersionAndCommit(db, NN);
+ * } catch (e) {
+ * throw reportErrorAndRollback(db, e);
+ * }
+ * return tbl;
+ * }
+ *
+ * Regardless of how your upgrader looks, make sure you:
+ * - use an sql transaction, if you have a database
+ * - If everything succeeds, call setDbVersionAndCommit to update the database
+ * version (setDbVersionAndCommit also commits the transaction)
+ * - If something fails, throw reportErrorAndRollback(db, e) to report the
+ * failure and roll back the transaction.
+ *
+ * If this documentation isn't sufficient to make upgrading understandable,
+ * please file a bug.
+ */
+
+var EXPORTED_SYMBOLS = [
+ "DB_SCHEMA_VERSION",
+ "getSql",
+ "getAllSql",
+ "getSqlTable",
+ "upgradeDB",
+ "backupDB",
+];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { CAL_ITEM_FLAG, textToDate, getTimezone, newDateTime } = ChromeUtils.import(
+ "resource:///modules/calendar/calStorageHelpers.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ CalAlarm: "resource:///modules/CalAlarm.jsm",
+ CalAttachment: "resource:///modules/CalAttachment.jsm",
+ CalAttendee: "resource:///modules/CalAttendee.jsm",
+ CalRelation: "resource:///modules/CalRelation.jsm",
+});
+
+// The current database version. Be sure to increment this when you create a new
+// updater.
+var DB_SCHEMA_VERSION = 23;
+
+/**
+ * Gets the SQL for the given table data and table name. This can be both a real
+ * table or the name of an index. Indexes must contain the idx_ prefix.
+ *
+ * @param tblName The name of the table or index to retrieve sql for
+ * @param tblData The table data object, as returned from the upgrade_v*
+ * functions. If null, then the latest table data is
+ * retrieved.
+ * @param alternateName (optional) The table or index name to be used in the
+ * resulting CREATE statement. If not set, tblName will
+ * be used.
+ * @returns The SQL Statement for the given table or index and
+ * version as a string.
+ */
+function getSql(tblName, tblData, alternateName) {
+ tblData = tblData || getSqlTable();
+ let altName = alternateName || tblName;
+ let sql;
+ if (tblName.substr(0, 4) == "idx_") {
+ // If this is an index, we need construct the SQL differently
+ let idxTbl = tblData[tblName].shift();
+ let idxOn = idxTbl + "(" + tblData[tblName].join(",") + ")";
+ sql = `CREATE INDEX ${altName} ON ${idxOn};`;
+ } else {
+ sql = `CREATE TABLE ${altName} (\n`;
+ for (let [key, type] of Object.entries(tblData[tblName])) {
+ sql += ` ${key} ${type},\n`;
+ }
+ }
+
+ return sql.replace(/,\s*$/, ");");
+}
+
+/**
+ * Gets all SQL for the given table data
+ *
+ * @param version The database schema version to retrieve. If null, the
+ * latest schema version will be used.
+ * @returns The SQL Statement for the given version as a string.
+ */
+function getAllSql(version) {
+ let tblData = getSqlTable(version);
+ let sql = "";
+ for (let tblName in tblData) {
+ sql += getSql(tblName, tblData) + "\n\n";
+ }
+ cal.LOG("Storage: Full SQL statement is " + sql);
+ return sql;
+}
+
+/**
+ * Get the JS object corresponding to the given schema version. This object will
+ * contain both tables and indexes, where indexes are prefixed with "idx_".
+ *
+ * @param schemaVersion The schema version to get. If null, the latest
+ * schema version will be used.
+ * @returns The javascript object containing the table
+ * definition.
+ */
+function getSqlTable(schemaVersion) {
+ let version = "v" + (schemaVersion || DB_SCHEMA_VERSION);
+ if (version in upgrade) {
+ return upgrade[version]();
+ }
+ return {};
+}
+
+/**
+ * Gets the current version of the storage database
+ */
+function getVersion(db) {
+ let selectSchemaVersion;
+ let version = null;
+
+ try {
+ selectSchemaVersion = createStatement(
+ db,
+ "SELECT version FROM cal_calendar_schema_version LIMIT 1"
+ );
+ if (selectSchemaVersion.executeStep()) {
+ version = selectSchemaVersion.row.version;
+ }
+
+ if (version !== null) {
+ // This is the only place to leave this function gracefully.
+ return version;
+ }
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ } finally {
+ if (selectSchemaVersion) {
+ selectSchemaVersion.finalize();
+ }
+ }
+
+ throw new Error("cal_calendar_schema_version SELECT returned no results");
+}
+
+/**
+ * Backup the database and notify the user via error console of the process
+ */
+function backupDB(db, currentVersion) {
+ cal.LOG("Storage: Backing up current database...");
+ try {
+ // Prepare filenames and path
+ let backupFilename = "local.v" + currentVersion + ".sqlite";
+ let backupPath = cal.provider.getCalendarDirectory();
+ backupPath.append("backup");
+ if (!backupPath.exists()) {
+ backupPath.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ }
+
+ // Create a backup file and notify the user via WARN, since LOG will not
+ // be visible unless a pref is set.
+ let file = Services.storage.backupDatabaseFile(db.databaseFile, backupFilename, backupPath);
+ cal.WARN(
+ "Storage: Upgrading to v" + DB_SCHEMA_VERSION + ", a backup was written to: " + file.path
+ );
+ } catch (e) {
+ cal.ERROR("Storage: Error creating backup file: " + e);
+ }
+}
+
+/**
+ * Upgrade the passed database.
+ *
+ * @param storageCalendar - An instance of CalStorageCalendar.
+ */
+function upgradeDB(storageCalendar) {
+ let db = storageCalendar.db;
+ cal.ASSERT(db, "Database has not been opened!", true);
+
+ if (db.tableExists("cal_calendar_schema_version")) {
+ let version = getVersion(db);
+
+ if (version < DB_SCHEMA_VERSION) {
+ upgradeExistingDB(db, version);
+ } else if (version > DB_SCHEMA_VERSION) {
+ handleTooNewSchema(storageCalendar);
+ return;
+ }
+ } else {
+ upgradeBrandNewDB(db);
+ }
+
+ ensureUpdatedTimezones(db);
+ storageCalendar.afterUpgradeDB();
+}
+
+/**
+ * Upgrade a brand new database.
+ *
+ * @param {mozIStorageAsyncConnection} db - New database to upgrade.
+ */
+function upgradeBrandNewDB(db) {
+ cal.LOG("Storage: Creating tables from scratch");
+ beginTransaction(db);
+ try {
+ executeSimpleSQL(db, getAllSql());
+ setDbVersionAndCommit(db, DB_SCHEMA_VERSION);
+ } catch (e) {
+ reportErrorAndRollback(db, e);
+ }
+}
+
+/**
+ * Upgrade an existing database.
+ *
+ * @param {mozIStorageAsyncConnection} db - Existing database to upgrade.
+ * @param {number} version - Version of the database before upgrading.
+ */
+function upgradeExistingDB(db, version) {
+ // First, create a backup
+ backupDB(db, version);
+
+ // Then start the latest upgrader
+ cal.LOG("Storage: Preparing to upgrade v" + version + " to v" + DB_SCHEMA_VERSION);
+ upgrade["v" + DB_SCHEMA_VERSION](db, version);
+}
+
+/**
+ * Called when the user has downgraded Thunderbird and the older version of
+ * Thunderbird does not know about the newer schema of their calendar data.
+ * Log an error, make a backup copy of the data by renaming the data file, and
+ * restart the database initialization process, which will create a new data
+ * file that will have the correct schema.
+ *
+ * The user will find that their calendar events/tasks are gone. They should
+ * have exported them to an ICS file before downgrading, and then they can
+ * import them to get them back.
+ *
+ * @param storageCalendar - An instance of CalStorageCalendar.
+ */
+function handleTooNewSchema(storageCalendar) {
+ // Create a string like this: "2020-05-11T21-30-17".
+ let dateTime = new Date().toISOString().split(".")[0].replace(/:/g, "-");
+
+ let copyFileName = `local-${dateTime}.sqlite`;
+
+ storageCalendar.db.databaseFile.renameTo(null, copyFileName);
+
+ storageCalendar.db.close();
+
+ let appName = cal.l10n.getAnyString("branding", "brand", "brandShortName");
+ let errorText = cal.l10n.getCalString("tooNewSchemaErrorText", [appName, copyFileName]);
+ cal.ERROR(errorText);
+
+ storageCalendar.prepareInitDB();
+}
+
+/**
+ * Sets the db version and commits any open transaction.
+ *
+ * @param db The mozIStorageConnection to commit on
+ * @param version The version to set
+ */
+function setDbVersionAndCommit(db, version) {
+ let sql =
+ "DELETE FROM cal_calendar_schema_version;" +
+ `INSERT INTO cal_calendar_schema_version (version) VALUES (${version})`;
+
+ executeSimpleSQL(db, sql);
+ if (db && db.transactionInProgress) {
+ commitTransaction(db);
+ }
+}
+
+/**
+ * Creates a function that calls the given function |funcName| on it's passed
+ * database. In addition, if no database is passed, the call is ignored.
+ *
+ * @param funcName The function name to delegate.
+ * @returns The delegate function for the passed named function.
+ */
+function createDBDelegate(funcName) {
+ return function (db, ...args) {
+ if (db) {
+ try {
+ return db[funcName](...args);
+ } catch (e) {
+ cal.ERROR(
+ "Error calling '" +
+ funcName +
+ "' db error: '" +
+ lastErrorString(db) +
+ "'.\nException: " +
+ e
+ );
+ cal.WARN(cal.STACK(10));
+ }
+ }
+ return null;
+ };
+}
+
+/**
+ * Creates a delegate function for a database getter. Returns a function that
+ * can be called to get the specified attribute, if a database is passed. If no
+ * database is passed, no error is thrown but null is returned.
+ *
+ * @param getterAttr The getter to delegate.
+ * @returns The function that delegates the getter.
+ */
+function createDBDelegateGetter(getterAttr) {
+ return function (db) {
+ return db ? db[getterAttr] : null;
+ };
+}
+
+// These functions use the db delegate to allow easier calling of common
+// database functions.
+var beginTransaction = createDBDelegate("beginTransaction");
+var commitTransaction = createDBDelegate("commitTransaction");
+var rollbackTransaction = createDBDelegate("rollbackTransaction");
+var createStatement = createDBDelegate("createStatement");
+var executeSimpleSQL = createDBDelegate("executeSimpleSQL");
+var removeFunction = createDBDelegate("removeFunction");
+var createFunction = createDBDelegate("createFunction");
+
+var lastErrorString = createDBDelegateGetter("lastErrorString");
+
+/**
+ * Helper function to create an index on the database if it doesn't already
+ * exist.
+ *
+ * @param tblData The table data object to save the index in.
+ * @param tblName The name of the table to index.
+ * @param colNameArray An array of columns to index over.
+ * @param db (optional) The database to create the index on.
+ */
+function createIndex(tblData, tblName, colNameArray, db) {
+ let idxName = "idx_" + tblName + "_" + colNameArray.join("_");
+ let idxOn = tblName + "(" + colNameArray.join(",") + ")";
+
+ // Construct the table data for this index
+ tblData[idxName] = colNameArray.concat([]);
+ tblData[idxName].unshift(tblName);
+
+ // Execute the sql, if there is a db
+ return executeSimpleSQL(db, `CREATE INDEX IF NOT EXISTS ${idxName} ON ${idxOn}`);
+}
+
+/**
+ * Often in an upgrader we want to log something only if there is a database. To
+ * make code less cludgy, here a helper function.
+ *
+ * @param db The database, or null if nothing should be logged.
+ * @param msg The message to log.
+ */
+function LOGdb(db, msg) {
+ if (db) {
+ cal.LOG(msg);
+ }
+}
+
+/**
+ * Report an error and roll back the last transaction.
+ *
+ * @param db The database to roll back on.
+ * @param e The exception to report
+ * @returns The passed exception, for chaining.
+ */
+function reportErrorAndRollback(db, e) {
+ if (db && db.transactionInProgress) {
+ rollbackTransaction(db);
+ }
+ cal.ERROR(
+ `++++++ Storage error! ++++++ DB Error: ${lastErrorString(db)}\n++++++ Exception: ${e}`
+ );
+ return e;
+}
+
+/**
+ * Make sure the timezones of the events in the database are up to date.
+ *
+ * @param db The database to bring up to date
+ */
+function ensureUpdatedTimezones(db) {
+ // check if timezone version has changed:
+ let selectTzVersion = createStatement(db, "SELECT version FROM cal_tz_version LIMIT 1");
+ let tzServiceVersion = cal.timezoneService.version;
+ let version;
+ try {
+ version = selectTzVersion.executeStep() ? selectTzVersion.row.version : null;
+ } finally {
+ selectTzVersion.finalize();
+ }
+
+ let versionComp = 1;
+ if (version) {
+ versionComp = Services.vc.compare(tzServiceVersion, version);
+ }
+
+ if (versionComp != 0) {
+ cal.LOG(
+ "[calStorageCalendar] Timezones have been changed from " +
+ version +
+ " to " +
+ tzServiceVersion +
+ ", updating calendar data."
+ );
+
+ let zonesToUpdate = [];
+ let getZones = createStatement(
+ db,
+ "SELECT DISTINCT(zone) FROM (" +
+ "SELECT recurrence_id_tz AS zone FROM cal_attendees WHERE recurrence_id_tz IS NOT NULL UNION " +
+ "SELECT recurrence_id_tz AS zone FROM cal_events WHERE recurrence_id_tz IS NOT NULL UNION " +
+ "SELECT event_start_tz AS zone FROM cal_events WHERE event_start_tz IS NOT NULL UNION " +
+ "SELECT event_end_tz AS zone FROM cal_events WHERE event_end_tz IS NOT NULL UNION " +
+ "SELECT recurrence_id_tz AS zone FROM cal_properties WHERE recurrence_id_tz IS NOT NULL UNION " +
+ "SELECT recurrence_id_tz AS zone FROM cal_todos WHERE recurrence_id_tz IS NOT NULL UNION " +
+ "SELECT todo_entry_tz AS zone FROM cal_todos WHERE todo_entry_tz IS NOT NULL UNION " +
+ "SELECT todo_due_tz AS zone FROM cal_todos WHERE todo_due_tz IS NOT NULL UNION " +
+ "SELECT recurrence_id_tz AS zone FROM cal_alarms WHERE recurrence_id_tz IS NOT NULL UNION " +
+ "SELECT recurrence_id_tz AS zone FROM cal_relations WHERE recurrence_id_tz IS NOT NULL UNION " +
+ "SELECT recurrence_id_tz AS zone FROM cal_attachments WHERE recurrence_id_tz IS NOT NULL" +
+ ");"
+ );
+ try {
+ while (getZones.executeStep()) {
+ let zone = getZones.row.zone;
+ // Send the timezones off to the timezone service to attempt conversion:
+ let timezone = getTimezone(zone);
+ if (timezone) {
+ let refTz = cal.timezoneService.getTimezone(timezone.tzid);
+ if (refTz && refTz.tzid != zone) {
+ zonesToUpdate.push({ oldTzId: zone, newTzId: refTz.tzid });
+ }
+ }
+ }
+ } catch (e) {
+ cal.ERROR("Error updating timezones: " + e + "\nDB Error " + lastErrorString(db));
+ } finally {
+ getZones.finalize();
+ }
+
+ beginTransaction(db);
+ try {
+ for (let update of zonesToUpdate) {
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ `UPDATE cal_attendees SET recurrence_id_tz = '${update.newTzId}' WHERE recurrence_id_tz = '${update.oldTzId}'; ` +
+ `UPDATE cal_events SET recurrence_id_tz = '${update.newTzId}' WHERE recurrence_id_tz = '${update.oldTzId}'; ` +
+ `UPDATE cal_events SET event_start_tz = '${update.newTzId}' WHERE event_start_tz = '${update.oldTzId}'; ` +
+ `UPDATE cal_events SET event_end_tz = '${update.newTzId}' WHERE event_end_tz = '${update.oldTzId}'; ` +
+ `UPDATE cal_properties SET recurrence_id_tz = '${update.newTzId}' WHERE recurrence_id_tz = '${update.oldTzId}'; ` +
+ `UPDATE cal_todos SET recurrence_id_tz = '${update.newTzId}' WHERE recurrence_id_tz = '${update.oldTzId}'; ` +
+ `UPDATE cal_todos SET todo_entry_tz = '${update.newTzId}' WHERE todo_entry_tz = '${update.oldTzId}'; ` +
+ `UPDATE cal_todos SET todo_due_tz = '${update.newTzId}' WHERE todo_due_tz = '${update.oldTzId}'; ` +
+ `UPDATE cal_alarms SET recurrence_id_tz = '${update.newTzId}' WHERE recurrence_id_tz = '${update.oldTzId}'; ` +
+ `UPDATE cal_relations SET recurrence_id_tz = '${update.newTzId}' WHERE recurrence_id_tz = '${update.oldTzId}'; ` +
+ `UPDATE cal_attachments SET recurrence_id_tz = '${update.newTzId}' WHERE recurrence_id_tz = '${update.oldTzId}';`
+ );
+ }
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ "DELETE FROM cal_tz_version; " +
+ `INSERT INTO cal_tz_version VALUES ('${cal.timezoneService.version}');`
+ );
+ commitTransaction(db);
+ } catch (e) {
+ cal.ASSERT(false, "Timezone update failed! DB Error: " + lastErrorString(db));
+ rollbackTransaction(db);
+ throw e;
+ }
+ }
+}
+
+/**
+ * Adds a column to the given table.
+ *
+ * @param tblData The table data object to apply the operation on.
+ * @param tblName The table name to add on
+ * @param colName The column name to add
+ * @param colType The type of the column to add
+ * @param db (optional) The database to apply the operation on
+ */
+function addColumn(tblData, tblName, colName, colType, db) {
+ cal.ASSERT(tblName in tblData, `Table ${tblName} is missing from table def`, true);
+ tblData[tblName][colName] = colType;
+
+ executeSimpleSQL(db, `ALTER TABLE ${tblName} ADD COLUMN ${colName} ${colType}`);
+}
+
+/**
+ * Deletes columns from the given table.
+ *
+ * @param tblData The table data object to apply the operation on.
+ * @param tblName The table name to delete on
+ * @param colNameArray An array of column names to delete
+ * @param db (optional) The database to apply the operation on
+ */
+function deleteColumns(tblData, tblName, colNameArray, db) {
+ for (let colName of colNameArray) {
+ delete tblData[tblName][colName];
+ }
+
+ let columns = Object.keys(tblData[tblName]);
+ executeSimpleSQL(db, getSql(tblName, tblData, tblName + "_temp"));
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ `INSERT INTO ${tblName}_temp (${columns.join(",")}) ` +
+ `SELECT ${columns.join(",")}` +
+ ` FROM ${tblName};`
+ );
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ `DROP TABLE ${tblName}; ` +
+ `ALTER TABLE ${tblName}_temp` +
+ ` RENAME TO ${tblName};`
+ );
+}
+
+/**
+ * Does a full copy of the given table
+ *
+ * @param tblData The table data object to apply the operation on.
+ * @param tblName The table name to copy
+ * @param newTblName The target table name.
+ * @param db (optional) The database to apply the operation on
+ * @param condition (optional) The condition to respect when copying
+ * @param selectOptions (optional) Extra options for the SELECT, i.e DISTINCT
+ */
+function copyTable(tblData, tblName, newTblName, db, condition, selectOptions) {
+ function objcopy(obj) {
+ return JSON.parse(JSON.stringify(obj));
+ }
+
+ tblData[newTblName] = objcopy(tblData[tblName]);
+
+ let columns = Object.keys(tblData[newTblName]);
+ executeSimpleSQL(db, getSql(newTblName, tblData));
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ `INSERT INTO ${newTblName} (${columns.join(",")}) ` +
+ `SELECT ${selectOptions} ${columns.join(",")}` +
+ ` FROM ${tblName} ${condition ? condition : ""};`
+ );
+}
+
+/**
+ * Alter the type of a certain column
+ *
+ * @param tblData The table data object to apply the operation on.
+ * @param tblName The table name to alter
+ * @param colNameArray An array of column names to delete
+ * @param newType The new type of the column
+ * @param db (optional) The database to apply the operation on
+ */
+function alterTypes(tblData, tblName, colNameArray, newType, db) {
+ for (let colName of colNameArray) {
+ tblData[tblName][colName] = newType;
+ }
+
+ let columns = Object.keys(tblData[tblName]);
+ executeSimpleSQL(db, getSql(tblName, tblData, tblName + "_temp"));
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ `INSERT INTO ${tblName}_temp (${columns.join(",")}) ` +
+ `SELECT ${columns.join(",")}` +
+ ` FROM ${tblName};`
+ );
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ `DROP TABLE ${tblName}; ` +
+ `ALTER TABLE ${tblName}_temp` +
+ ` RENAME TO ${tblName};`
+ );
+}
+
+/**
+ * Renames the given table, giving it a new name.
+ *
+ * @param tblData The table data object to apply the operation on.
+ * @param tblName The table name to rename.
+ * @param newTblName The new name of the table.
+ * @param db (optional) The database to apply the operation on.
+ * @param overwrite (optional) If true, the target table will be dropped
+ * before the rename
+ */
+function renameTable(tblData, tblName, newTblName, db, overwrite) {
+ if (overwrite) {
+ dropTable(tblData, newTblName, db);
+ }
+ tblData[newTblName] = tblData[tblName];
+ delete tblData[tblName];
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ `ALTER TABLE ${tblName}` +
+ ` RENAME TO ${newTblName}`
+ );
+}
+
+/**
+ * Drops the given table.
+ *
+ * @param tblData The table data object to apply the operation on.
+ * @param tblName The table name to drop.
+ * @param db (optional) The database to apply the operation on.
+ */
+function dropTable(tblData, tblName, db) {
+ delete tblData[tblName];
+
+ executeSimpleSQL(db, `DROP TABLE IF EXISTS ${tblName};`);
+}
+
+/**
+ * Creates the given table.
+ *
+ * @param tblData The table data object to apply the operation on.
+ * @param tblName The table name to add.
+ * @param def The table definition object.
+ * @param db (optional) The database to apply the operation on.
+ */
+function addTable(tblData, tblName, def, db) {
+ tblData[tblName] = def;
+
+ executeSimpleSQL(db, getSql(tblName, tblData));
+}
+
+/**
+ * Migrates the given columns to a single icalString, using the (previously
+ * created) user function for processing.
+ *
+ * @param tblData The table data object to apply the operation on.
+ * @param tblName The table name to migrate.
+ * @param userFuncName The name of the user function to call for migration
+ * @param oldColumns An array of columns to migrate to the new icalString
+ * column
+ * @param db (optional) The database to apply the operation on.
+ */
+function migrateToIcalString(tblData, tblName, userFuncName, oldColumns, db) {
+ addColumn(tblData, tblName, ["icalString"], "TEXT", db);
+ // prettier-ignore
+ let updateSql =
+ `UPDATE ${tblName} ` +
+ ` SET icalString = ${userFuncName}(${oldColumns.join(",")})`;
+ executeSimpleSQL(db, updateSql);
+ deleteColumns(tblData, tblName, oldColumns, db);
+
+ // If null was returned, its an invalid attendee. Make sure to remove them,
+ // they might break things later on.
+ let cleanupSql = `DELETE FROM ${tblName} WHERE icalString IS NULL`;
+ executeSimpleSQL(db, cleanupSql);
+}
+
+/**
+ * Maps a mozIStorageValueArray to a JS array, converting types correctly.
+ *
+ * @param storArgs The storage value array to convert
+ * @returns An array with the arguments as js values.
+ */
+function mapStorageArgs(storArgs) {
+ const mISVA = Ci.mozIStorageValueArray;
+ let mappedArgs = [];
+ for (let i = 0; i < storArgs.numEntries; i++) {
+ switch (storArgs.getTypeOfIndex(i)) {
+ case mISVA.VALUE_TYPE_NULL:
+ mappedArgs.push(null);
+ break;
+ case mISVA.VALUE_TYPE_INTEGER:
+ mappedArgs.push(storArgs.getInt64(i));
+ break;
+ case mISVA.VALUE_TYPE_FLOAT:
+ mappedArgs.push(storArgs.getDouble(i));
+ break;
+ case mISVA.VALUE_TYPE_TEXT:
+ case mISVA.VALUE_TYPE_BLOB:
+ mappedArgs.push(storArgs.getUTF8String(i));
+ break;
+ }
+ }
+
+ return mappedArgs;
+}
+
+/** Object holding upgraders */
+var upgrade = {};
+
+/**
+ * Returns the initial storage database schema. Note this is not the current
+ * schema, it will be modified by the upgrade.vNN() functions. This function
+ * returns the initial v1 with modifications from v2 applied.
+ *
+ * No bug - new recurrence system. exceptions supported now, along with
+ * everything else ical can throw at us. I hope.
+ * p=vlad
+ */
+// eslint-disable-next-line id-length
+upgrade.v2 = upgrade.v1 = function (db, version) {
+ LOGdb(db, "Storage: Upgrading to v1/v2");
+ let tblData = {
+ cal_calendar_schema_version: { version: "INTEGER" },
+
+ /* While this table is in v1, actually keeping it in the sql object will
+ * cause problems when migrating from storage.sdb to local.sqlite. There,
+ * all tables from storage.sdb will be moved to local.sqlite and so starting
+ * the application again afterwards causes a borked upgrade since its missing
+ * tables it expects.
+ *
+ * cal_calendars: {
+ * id: "INTEGER PRIMARY KEY",
+ * name: "STRING"
+ * },
+ */
+
+ cal_items: {
+ cal_id: "INTEGER",
+ item_type: "INTEGER",
+ id: "STRING",
+ time_created: "INTEGER",
+ last_modified: "INTEGER",
+ title: "STRING",
+ priority: "INTEGER",
+ privacy: "STRING",
+ ical_status: "STRING",
+ flags: "INTEGER",
+ event_start: "INTEGER",
+ event_end: "INTEGER",
+ event_stamp: "INTEGER",
+ todo_entry: "INTEGER",
+ todo_due: "INTEGER",
+ todo_completed: "INTEGER",
+ todo_complete: "INTEGER",
+ alarm_id: "INTEGER",
+ },
+
+ cal_attendees: {
+ item_id: "STRING",
+ attendee_id: "STRING",
+ common_name: "STRING",
+ rsvp: "INTEGER",
+ role: "STRING",
+ status: "STRING",
+ type: "STRING",
+ },
+
+ cal_alarms: {
+ id: "INTEGER PRIMARY KEY",
+ alarm_data: "BLOB",
+ },
+
+ cal_recurrence: {
+ item_id: "STRING",
+ recur_type: "INTEGER",
+ recur_index: "INTEGER",
+ is_negative: "BOOLEAN",
+ dates: "STRING",
+ end_date: "INTEGER",
+ count: "INTEGER",
+ interval: "INTEGER",
+ second: "STRING",
+ minute: "STRING",
+ hour: "STRING",
+ day: "STRING",
+ monthday: "STRING",
+ yearday: "STRING",
+ weekno: "STRING",
+ month: "STRING",
+ setpos: "STRING",
+ },
+
+ cal_properties: {
+ item_id: "STRING",
+ key: "STRING",
+ value: "BLOB",
+ },
+ };
+
+ for (let tbl in tblData) {
+ executeSimpleSQL(db, `DROP TABLE IF EXISTS ${tbl}`);
+ }
+ return tblData;
+};
+
+/**
+ * Upgrade to version 3.
+ * Bug 293707, updates to storage provider; calendar manager database locked
+ * fix, r=shaver, p=vlad
+ * p=vlad
+ */
+// eslint-disable-next-line id-length
+upgrade.v3 = function (db, version) {
+ function updateSql(tbl, field) {
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ `UPDATE ${tbl} SET ${field}_tz='UTC'` +
+ ` WHERE ${field} IS NOT NULL`
+ );
+ }
+
+ let tbl = upgrade.v2(version < 2 && db, version);
+ LOGdb(db, "Storage: Upgrading to v3");
+
+ beginTransaction(db);
+ try {
+ copyTable(tbl, "cal_items", "cal_events", db, "item_type = 0");
+ copyTable(tbl, "cal_items", "cal_todos", db, "item_type = 1");
+
+ dropTable(tbl, "cal_items", db);
+
+ let removeEventCols = [
+ "item_type",
+ "item_type",
+ "todo_entry",
+ "todo_due",
+ "todo_completed",
+ "todo_complete",
+ "alarm_id",
+ ];
+ deleteColumns(tbl, "cal_events", removeEventCols, db);
+
+ addColumn(tbl, "cal_events", "event_start_tz", "VARCHAR", db);
+ addColumn(tbl, "cal_events", "event_end_tz", "VARCHAR", db);
+ addColumn(tbl, "cal_events", "alarm_time", "INTEGER", db);
+ addColumn(tbl, "cal_events", "alarm_time_tz", "VARCHAR", db);
+
+ let removeTodoCols = ["item_type", "event_start", "event_end", "event_stamp", "alarm_id"];
+ deleteColumns(tbl, "cal_todos", removeTodoCols, db);
+
+ addColumn(tbl, "cal_todos", "todo_entry_tz", "VARCHAR", db);
+ addColumn(tbl, "cal_todos", "todo_due_tz", "VARCHAR", db);
+ addColumn(tbl, "cal_todos", "todo_completed_tz", "VARCHAR", db);
+ addColumn(tbl, "cal_todos", "alarm_time", "INTEGER", db);
+ addColumn(tbl, "cal_todos", "alarm_time_tz", "VARCHAR", db);
+
+ dropTable(tbl, "cal_alarms", db);
+
+ // The change between 2 and 3 includes the splitting of cal_items into
+ // cal_events and cal_todos, and the addition of columns for
+ // event_start_tz, event_end_tz, todo_entry_tz, todo_due_tz.
+ // These need to default to "UTC" if their corresponding time is
+ // given, since that's what the default was for v2 calendars
+
+ // Fix up the new timezone columns
+ updateSql("cal_events", "event_start");
+ updateSql("cal_events", "event_end");
+ updateSql("cal_todos", "todo_entry");
+ updateSql("cal_todos", "todo_due");
+ updateSql("cal_todos", "todo_completed");
+
+ setDbVersionAndCommit(db, 3);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+ return tbl;
+};
+
+/**
+ * Upgrade to version 4.
+ * Bug 293183 - implement exception support for recurrence.
+ * r=shaver,p=vlad
+ */
+// eslint-disable-next-line id-length
+upgrade.v4 = function (db, version) {
+ let tbl = upgrade.v3(version < 3 && db, version);
+ LOGdb(db, "Storage: Upgrading to v4");
+
+ beginTransaction(db);
+ try {
+ for (let tblid of ["events", "todos", "attendees", "properties"]) {
+ addColumn(tbl, "cal_" + tblid, "recurrence_id", "INTEGER", db);
+ addColumn(tbl, "cal_" + tblid, "recurrence_id_tz", "VARCHAR", db);
+ }
+ setDbVersionAndCommit(db, 4);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+
+ return tbl;
+};
+
+/**
+ * Bug 315051 - Switch to storing alarms based on offsets from start/end time
+ * rather than as absolute times. Ensure that missed alarms are fired.
+ * r=dmose, p=jminta
+ */
+// eslint-disable-next-line id-length
+upgrade.v5 = function (db, version) {
+ let tbl = upgrade.v4(version < 4 && db, version);
+ LOGdb(db, "Storage: Upgrading to v5");
+
+ beginTransaction(db);
+ try {
+ for (let tblid of ["events", "todos"]) {
+ addColumn(tbl, "cal_" + tblid, "alarm_offset", "INTEGER", db);
+ addColumn(tbl, "cal_" + tblid, "alarm_related", "INTEGER", db);
+ addColumn(tbl, "cal_" + tblid, "alarm_last_ack", "INTEGER", db);
+ }
+ setDbVersionAndCommit(db, 5);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+
+ return tbl;
+};
+
+/**
+ * Bug 333688 - Converts STRING and VARCHAR columns to TEXT to avoid SQLite's
+ * auto-conversion of strings to numbers (10e4 to 10000)
+ * r=ctalbert,jminta p=lilmatt
+ */
+// eslint-disable-next-line id-length
+upgrade.v6 = function (db, version) {
+ let tbl = upgrade.v5(version < 5 && db, version);
+ LOGdb(db, "Storage: Upgrading to v6");
+
+ beginTransaction(db);
+ try {
+ let eventCols = [
+ "id",
+ "title",
+ "privacy",
+ "ical_status",
+ "recurrence_id_tz",
+ "event_start_tz",
+ "event_end_tz",
+ "alarm_time_tz",
+ ];
+ alterTypes(tbl, "cal_events", eventCols, "TEXT", db);
+
+ let todoCols = [
+ "id",
+ "title",
+ "privacy",
+ "ical_status",
+ "recurrence_id_tz",
+ "todo_entry_tz",
+ "todo_due_tz",
+ "todo_completed_tz",
+ "alarm_time_tz",
+ ];
+ alterTypes(tbl, "cal_todos", todoCols, "TEXT", db);
+
+ let attendeeCols = [
+ "item_id",
+ "recurrence_id_tz",
+ "attendee_id",
+ "common_name",
+ "role",
+ "status",
+ "type",
+ ];
+ alterTypes(tbl, "cal_attendees", attendeeCols, "TEXT", db);
+
+ let recurrenceCols = [
+ "item_id",
+ "recur_type",
+ "dates",
+ "second",
+ "minute",
+ "hour",
+ "day",
+ "monthday",
+ "yearday",
+ "weekno",
+ "month",
+ "setpos",
+ ];
+ alterTypes(tbl, "cal_recurrence", recurrenceCols, "TEXT", db);
+
+ let propertyCols = ["item_id", "recurrence_id_tz", "key"];
+ alterTypes(tbl, "cal_properties", propertyCols, "TEXT", db);
+ setDbVersionAndCommit(db, 6);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+
+ return tbl;
+};
+
+/**
+ * Bug 369010: Migrate all old tzids in storage to new one.
+ * r=ctalbert,dmose p=lilmatt
+ */
+// eslint-disable-next-line id-length
+upgrade.v7 = function (db, version) {
+ // No schema changes in v7
+ let tbl = upgrade.v6(db, version);
+ LOGdb(db, "Storage: Upgrading to v7");
+ return tbl;
+};
+
+/**
+ * Bug 410931 - Update internal timezone definitions
+ * r=ctalbert, p=dbo,nth10sd,hb
+ */
+// eslint-disable-next-line id-length
+upgrade.v8 = function (db, version) {
+ // No schema changes in v8
+ let tbl = upgrade.v7(db, version);
+ LOGdb(db, "Storage: Upgrading to v8");
+ return tbl;
+};
+
+/**
+ * Bug 363191 - Handle Timezones more efficiently (Timezone Database)
+ * r=philipp,ctalbert, p=dbo
+ */
+// eslint-disable-next-line id-length
+upgrade.v9 = function (db, version) {
+ // No schema changes in v9
+ let tbl = upgrade.v8(db, version);
+ LOGdb(db, "Storage: Upgrading to v9");
+ return tbl;
+};
+
+/**
+ * Bug 413908 – Events using internal timezones are no longer updated to
+ * recent timezone version;
+ * r=philipp, p=dbo
+ */
+upgrade.v10 = function (db, version) {
+ let tbl = upgrade.v9(version < 9 && db, version);
+ LOGdb(db, "Storage: Upgrading to v10");
+
+ beginTransaction(db);
+ try {
+ addTable(tbl, "cal_tz_version", { version: "TEXT" }, db);
+ setDbVersionAndCommit(db, 10);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+ return tbl;
+};
+
+/**
+ * Fix bug 319909 - Failure to properly serialize/unserialize ics ATTACH
+ * properties.
+ * r=philipp,p=fred.jen@web.de
+ */
+upgrade.v11 = function (db, version) {
+ let tbl = upgrade.v10(version < 10 && db, version);
+ LOGdb(db, "Storage: Upgrading to v11");
+
+ beginTransaction(db);
+ try {
+ addTable(
+ tbl,
+ "cal_attachments",
+ {
+ item_id: "TEXT",
+ data: "BLOB",
+ format_type: "TEXT",
+ encoding: "TEXT",
+ },
+ db
+ );
+ setDbVersionAndCommit(db, 11);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+ return tbl;
+};
+
+/**
+ * Bug 449031 - Add meta data API to memory/storage
+ * r=philipp, p=dbo
+ */
+upgrade.v12 = function (db, version) {
+ let tbl = upgrade.v11(version < 11 && db, version);
+ LOGdb(db, "Storage: Upgrading to v12");
+
+ beginTransaction(db);
+ try {
+ addColumn(tbl, "cal_attendees", "is_organizer", "BOOLEAN", db);
+ addColumn(tbl, "cal_attendees", "properties", "BLOB", db);
+
+ addTable(
+ tbl,
+ "cal_metadata",
+ {
+ cal_id: "INTEGER",
+ item_id: "TEXT UNIQUE",
+ value: "BLOB",
+ },
+ db
+ );
+ setDbVersionAndCommit(db, 12);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+
+ return tbl;
+};
+
+/**
+ * Bug 449401 - storage provider doesn't cleanly separate items of the same id
+ * across different calendars
+ * r=dbo,philipp, p=wsourdeau@inverse.ca
+ */
+upgrade.v13 = function (db, version) {
+ let tbl = upgrade.v12(version < 12 && db, version);
+ LOGdb(db, "Storage: Upgrading to v13");
+
+ beginTransaction(db);
+ try {
+ alterTypes(tbl, "cal_metadata", ["item_id"], "TEXT", db);
+
+ let calIds = {};
+ if (db) {
+ for (let itemTable of ["events", "todos"]) {
+ let stmt = createStatement(db, `SELECT id, cal_id FROM cal_${itemTable}`);
+ try {
+ while (stmt.executeStep()) {
+ calIds[stmt.row.id] = stmt.row.cal_id;
+ }
+ } finally {
+ stmt.finalize();
+ }
+ }
+ }
+ let tables = ["attendees", "recurrence", "properties", "attachments"];
+ for (let tblid of tables) {
+ addColumn(tbl, "cal_" + tblid, "cal_id", "INTEGER", db);
+
+ for (let itemId in calIds) {
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ `UPDATE cal_${tblid}` +
+ ` SET cal_id = ${calIds[itemId]}` +
+ ` WHERE item_id = '${itemId}'`
+ );
+ }
+ }
+
+ executeSimpleSQL(db, "DROP INDEX IF EXISTS idx_cal_properies_item_id");
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ "CREATE INDEX IF NOT EXISTS" +
+ " idx_cal_properies_item_id" +
+ " ON cal_properties(cal_id, item_id);"
+ );
+ setDbVersionAndCommit(db, 13);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+ return tbl;
+};
+
+/**
+ * Bug 446303 - use the "RELATED-TO" property.
+ * r=philipp,dbo, p=fred.jen@web.de
+ */
+upgrade.v14 = function (db, version) {
+ let tbl = upgrade.v13(version < 13 && db, version);
+ LOGdb(db, "Storage: Upgrading to v14");
+
+ beginTransaction(db);
+ try {
+ addTable(
+ tbl,
+ "cal_relations",
+ {
+ cal_id: "INTEGER",
+ item_id: "TEXT",
+ rel_type: "TEXT",
+ rel_id: "TEXT",
+ },
+ db
+ );
+ setDbVersionAndCommit(db, 14);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+ return tbl;
+};
+
+/**
+ * Bug 463282 - Tasks cannot be created or imported (regression).
+ * r=philipp,berend, p=dbo
+ */
+upgrade.v15 = function (db, version) {
+ let tbl = upgrade.v14(version < 14 && db, version);
+ LOGdb(db, "Storage: Upgrading to v15");
+
+ beginTransaction(db);
+ try {
+ addColumn(tbl, "cal_todos", "todo_stamp", "INTEGER", db);
+ setDbVersionAndCommit(db, 15);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+ return tbl;
+};
+
+/**
+ * Bug 353492 - support multiple alarms per events/task, support
+ * absolute alarms with fixed date/time - Storage Provider support for multiple
+ * alarms.
+ * r=dbo,ssitter, p=philipp
+ *
+ * This upgrader is a bit special. To fix bug 494140, we decided to change the
+ * upgrading code afterwards to make sure no data is lost for people upgrading
+ * from 0.9 -> 1.0b1 and later. The v17 upgrader will merely take care of the
+ * upgrade if a user is upgrading from 1.0pre -> 1.0b1 or later.
+ */
+upgrade.v16 = function (db, version) {
+ let tbl = upgrade.v15(version < 15 && db, version);
+ LOGdb(db, "Storage: Upgrading to v16");
+ beginTransaction(db);
+ try {
+ createFunction(db, "translateAlarm", 4, {
+ onFunctionCall(storArgs) {
+ try {
+ let [aOffset, aRelated, aAlarmTime, aTzId] = mapStorageArgs(storArgs);
+
+ let alarm = new lazy.CalAlarm();
+ if (aOffset) {
+ alarm.related = parseInt(aRelated, 10) + 1;
+ alarm.offset = cal.createDuration();
+ alarm.offset.inSeconds = aOffset;
+ } else if (aAlarmTime) {
+ alarm.related = Ci.calIAlarm.ALARM_RELATED_ABSOLUTE;
+ let alarmDate = cal.createDateTime();
+ alarmDate.nativeTime = aAlarmTime;
+ if (aTzId == "floating") {
+ // The current calDateTime code assumes that if a
+ // date is floating then we can just assign the new
+ // timezone. I have the feeling this is wrong so I
+ // filed bug 520463. Since we want to release 1.0b1
+ // soon, I will just fix this on the "client side"
+ // and do the conversion here.
+ alarmDate.timezone = cal.timezoneService.defaultTimezone;
+ alarmDate = alarmDate.getInTimezone(cal.dtz.UTC);
+ } else {
+ alarmDate.timezone = cal.timezoneService.getTimezone(aTzId);
+ }
+ alarm.alarmDate = alarmDate;
+ }
+ return alarm.icalString;
+ } catch (e) {
+ // Errors in this function are not really logged. Do this
+ // separately.
+ cal.ERROR("Error converting alarms: " + e);
+ throw e;
+ }
+ },
+ });
+
+ addTable(
+ tbl,
+ "cal_alarms",
+ {
+ cal_id: "INTEGER",
+ item_id: "TEXT",
+ // Note the following two columns were not originally part of the
+ // v16 upgrade, see note above function.
+ recurrence_id: "INTEGER",
+ recurrence_id_tz: "TEXT",
+ icalString: "TEXT",
+ },
+ db
+ );
+
+ let copyDataOver = function (tblName) {
+ const transAlarm = "translateAlarm(alarm_offset, alarm_related, alarm_time, alarm_time_tz)";
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ "INSERT INTO cal_alarms (cal_id, item_id," +
+ " recurrence_id, " +
+ " recurrence_id_tz, " +
+ " icalString)" +
+ " SELECT cal_id, id, recurrence_id," +
+ ` recurrence_id_tz, ${transAlarm}` +
+ ` FROM ${tblName}` +
+ " WHERE alarm_offset IS NOT NULL" +
+ " OR alarm_time IS NOT NULL;"
+ );
+ };
+ copyDataOver("cal_events");
+ copyDataOver("cal_todos");
+ removeFunction(db, "translateAlarm");
+
+ // Make sure the alarm flag is set on the item
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ "UPDATE cal_events " +
+ ` SET flags = flags | ${CAL_ITEM_FLAG.HAS_ALARMS}` +
+ " WHERE id IN" +
+ " (SELECT item_id " +
+ " FROM cal_alarms " +
+ " WHERE cal_alarms.cal_id = cal_events.cal_id)"
+ );
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ "UPDATE cal_todos " +
+ ` SET flags = flags | ${CAL_ITEM_FLAG.HAS_ALARMS}` +
+ " WHERE id IN" +
+ " (SELECT item_id " +
+ " FROM cal_alarms " +
+ " WHERE cal_alarms.cal_id = cal_todos.cal_id)"
+ );
+
+ // Remote obsolete columns
+ let cols = ["alarm_time", "alarm_time_tz", "alarm_offset", "alarm_related"];
+ for (let tblid of ["events", "todos"]) {
+ deleteColumns(tbl, "cal_" + tblid, cols, db);
+ }
+
+ setDbVersionAndCommit(db, 16);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+
+ return tbl;
+};
+
+/**
+ * Bug 494140 - Multiple reminders,relations,attachments created by modifying
+ * repeating event.
+ * r=dbo,ssitter, p=philipp
+ *
+ * This upgrader is special. In bug 494140 we decided it would be better to fix
+ * the v16 upgrader so 0.9 users can update to 1.0b1 and later without dataloss.
+ * Therefore all this upgrader does is handle users of 1.0pre before the
+ * mentioned bug.
+ */
+upgrade.v17 = function (db, version) {
+ let tbl = upgrade.v16(version < 16 && db, version);
+ LOGdb(db, "Storage: Upgrading to v17");
+ beginTransaction(db);
+ try {
+ for (let tblName of ["alarms", "relations", "attachments"]) {
+ let hasColumns = true;
+ let stmt;
+ try {
+ // Stepping this statement will fail if the columns don't exist.
+ // We don't use the delegate here since it would show an error to
+ // the user, even through we expect the error. If the db is null,
+ // then swallowing the error is ok too since the cols will
+ // already be added in v16.
+ stmt = db.createStatement(
+ `SELECT recurrence_id_tz, recurrence_id FROM cal_${tblName} LIMIT 1`
+ );
+ stmt.executeStep();
+ } catch (e) {
+ // An error happened, which means the cols don't exist
+ hasColumns = false;
+ } finally {
+ if (stmt) {
+ stmt.finalize();
+ }
+ }
+
+ // Only add the columns if they are not there yet (i.e added in v16)
+ // Since relations were broken all along, also make sure and add the
+ // columns to the javascript object if there is no database.
+ if (!hasColumns || !db) {
+ addColumn(tbl, "cal_" + tblName, "recurrence_id", "INTEGER", db);
+ addColumn(tbl, "cal_" + tblName, "recurrence_id_tz", "TEXT", db);
+ }
+
+ // Clear out entries that are exactly the same. This corrects alarms
+ // created in 1.0pre and relations and attachments created in 0.9.
+ copyTable(tbl, "cal_" + tblName, "cal_" + tblName + "_v17", db, null, "DISTINCT");
+ renameTable(tbl, "cal_" + tblName + "_v17", "cal_" + tblName, db, true);
+ }
+ setDbVersionAndCommit(db, 17);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+
+ return tbl;
+};
+
+/**
+ * Bug 529326 - Create indexes for the local calendar
+ * r=mschroeder, p=philipp
+ *
+ * This bug adds some indexes to improve performance. If you would like to add
+ * additional indexes, please read http://www.sqlite.org/optoverview.html first.
+ */
+upgrade.v18 = function (db, version) {
+ let tbl = upgrade.v17(version < 17 && db, version);
+ LOGdb(db, "Storage: Upgrading to v18");
+ beginTransaction(db);
+ try {
+ // These fields are often indexed over
+ let simpleIds = ["cal_id", "item_id"];
+ let allIds = simpleIds.concat(["recurrence_id", "recurrence_id_tz"]);
+
+ // Alarms, Attachments, Attendees, Relations
+ for (let tblName of ["alarms", "attachments", "attendees", "relations"]) {
+ createIndex(tbl, "cal_" + tblName, allIds, db);
+ }
+
+ // Events and Tasks
+ for (let tblName of ["events", "todos"]) {
+ createIndex(tbl, "cal_" + tblName, ["flags", "cal_id", "recurrence_id"], db);
+ createIndex(tbl, "cal_" + tblName, ["id", "cal_id", "recurrence_id"], db);
+ }
+
+ // Metadata
+ createIndex(tbl, "cal_metadata", simpleIds, db);
+
+ // Properties. Remove the index we used to create first, since our index
+ // is much more complete.
+ executeSimpleSQL(db, "DROP INDEX IF EXISTS idx_cal_properies_item_id");
+ createIndex(tbl, "cal_properties", allIds, db);
+
+ // Recurrence
+ createIndex(tbl, "cal_recurrence", simpleIds, db);
+
+ setDbVersionAndCommit(db, 18);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+
+ return tbl;
+};
+
+/**
+ * Bug 479867 - Cached calendars don't set id correctly, causing duplicate
+ * events to be shown for multiple cached calendars
+ * r=simon.at.orcl, p=philipp,dbo
+ */
+upgrade.v19 = function (db, version) {
+ let tbl = upgrade.v18(version < 18 && db, version);
+ LOGdb(db, "Storage: Upgrading to v19");
+ beginTransaction(db);
+ try {
+ let tables = [
+ "cal_alarms",
+ "cal_attachments",
+ "cal_attendees",
+ "cal_events",
+ "cal_metadata",
+ "cal_properties",
+ "cal_recurrence",
+ "cal_relations",
+ "cal_todos",
+ ];
+ // Change types of column to TEXT.
+ for (let tblName of tables) {
+ alterTypes(tbl, tblName, ["cal_id"], "TEXT", db);
+ }
+ setDbVersionAndCommit(db, 19);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+
+ return tbl;
+};
+
+/**
+ * Bug 380060 - Offline Sync feature for calendar
+ * Setting a offline_journal column in cal_events tables
+ * r=philipp, p=redDragon
+ */
+upgrade.v20 = function (db, version) {
+ let tbl = upgrade.v19(version < 19 && db, version);
+ LOGdb(db, "Storage: Upgrading to v20");
+ beginTransaction(db);
+ try {
+ // Adding a offline_journal column
+ for (let tblName of ["cal_events", "cal_todos"]) {
+ addColumn(tbl, tblName, ["offline_journal"], "INTEGER", db);
+ }
+ setDbVersionAndCommit(db, 20);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+ return tbl;
+};
+
+/**
+ * Bug 785659 - Get rid of calIRecurrenceDateSet
+ * Migrate x-dateset to x-date in the storage database
+ * r=mmecca, p=philipp
+ */
+upgrade.v21 = function (db, version) {
+ let tbl = upgrade.v20(version < 20 && db, version);
+ LOGdb(db, "Storage: Upgrading to v21");
+ beginTransaction(db);
+
+ try {
+ // The following operation is only important on a live DB, since we are
+ // changing only the values on the DB, not the schema itself.
+ if (db) {
+ // Oh boy, here we go :-)
+ // Insert a new row with the following columns...
+ let insertSQL =
+ "INSERT INTO cal_recurrence " +
+ " (item_id, cal_id, recur_type, recur_index," +
+ " is_negative, dates, end_date, count," +
+ " interval, second, minute, hour, day," +
+ " monthday, yearday, weekno, month, setpos)" +
+ // ... by selecting some columns from the existing table ...
+ ' SELECT item_id, cal_id, "x-date" AS recur_type, ' +
+ // ... like a new recur_index, we need it to be maximum for this item ...
+ " (SELECT MAX(recur_index)+1" +
+ " FROM cal_recurrence AS rinner " +
+ " WHERE rinner.item_id = router.item_id" +
+ " AND rinner.cal_id = router.cal_id) AS recur_index," +
+ " is_negative," +
+ // ... the string until the first comma in the current dates field
+ ' SUBSTR(dates, 0, LENGTH(dates) - LENGTH(LTRIM(dates, REPLACE(dates, ",", ""))) + 1) AS dates,' +
+ " end_date, count, interval, second, minute," +
+ " hour, day, monthday, yearday, weekno, month," +
+ " setpos" +
+ // ... from the recurrence table ...
+ " FROM cal_recurrence AS router " +
+ // ... but only on fields that are x-datesets ...
+ ' WHERE recur_type = "x-dateset" ' +
+ // ... and are not already empty.
+ ' AND dates != ""';
+ dump(insertSQL + "\n");
+
+ // Now we need to remove the first segment from the dates field
+ let updateSQL =
+ "UPDATE cal_recurrence" +
+ ' SET dates = SUBSTR(dates, LENGTH(dates) - LENGTH(LTRIM(dates, REPLACE(dates, ",", ""))) + 2)' +
+ ' WHERE recur_type = "x-dateset"' +
+ ' AND dates != ""';
+
+ // Create the statements
+ let insertStmt = createStatement(db, insertSQL);
+ let updateStmt = createStatement(db, updateSQL);
+
+ // Repeat these two statements until the update affects 0 rows
+ // (because the dates field on all x-datesets is empty)
+ do {
+ insertStmt.execute();
+ updateStmt.execute();
+ } while (db.affectedRows > 0);
+
+ // Finally we can delete the x-dateset rows. Note this will leave
+ // gaps in recur_index, but that's ok since its only used for
+ // ordering anyway and will be overwritten on the next item write.
+ executeSimpleSQL(db, 'DELETE FROM cal_recurrence WHERE recur_type = "x-dateset"');
+ }
+
+ setDbVersionAndCommit(db, 21);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+ return tbl;
+};
+
+/**
+ * Bug 785733 - Move some properties to use icalString in database.
+ * Use the full icalString in attendees, attachments, relations and recurrence
+ * tables.
+ * r=mmecca, p=philipp
+ */
+upgrade.v22 = function (db, version) {
+ let tbl = upgrade.v21(version < 21 && db, version);
+ LOGdb(db, "Storage: Upgrading to v22");
+ beginTransaction(db);
+ try {
+ // Update attachments to using icalString directly
+ createFunction(db, "translateAttachment", 3, {
+ onFunctionCall(storArgs) {
+ try {
+ let [aData, aFmtType, aEncoding] = mapStorageArgs(storArgs);
+
+ let attach = new lazy.CalAttachment();
+ attach.uri = Services.io.newURI(aData);
+ attach.formatType = aFmtType;
+ attach.encoding = aEncoding;
+ return attach.icalString;
+ } catch (e) {
+ cal.ERROR("Error converting attachment: " + e);
+ throw e;
+ }
+ },
+ });
+ migrateToIcalString(
+ tbl,
+ "cal_attachments",
+ "translateAttachment",
+ ["data", "format_type", "encoding"],
+ db
+ );
+
+ // Update relations to using icalString directly
+ createFunction(db, "translateRelation", 2, {
+ onFunctionCall(storArgs) {
+ try {
+ let [aRelType, aRelId] = mapStorageArgs(storArgs);
+ let relation = new lazy.CalRelation();
+ relation.relType = aRelType;
+ relation.relId = aRelId;
+ return relation.icalString;
+ } catch (e) {
+ cal.ERROR("Error converting relation: " + e);
+ throw e;
+ }
+ },
+ });
+ migrateToIcalString(tbl, "cal_relations", "translateRelation", ["rel_type", "rel_id"], db);
+
+ // Update attendees table to using icalString directly
+ createFunction(db, "translateAttendee", 8, {
+ onFunctionCall(storArgs) {
+ try {
+ let [aAttendeeId, aCommonName, aRsvp, aRole, aStatus, aType, aIsOrganizer, aProperties] =
+ mapStorageArgs(storArgs);
+
+ let attendee = new lazy.CalAttendee();
+
+ attendee.id = aAttendeeId;
+ attendee.commonName = aCommonName;
+
+ switch (aRsvp) {
+ case 0:
+ attendee.rsvp = "FALSE";
+ break;
+ case 1:
+ attendee.rsvp = "TRUE";
+ break;
+ // default: keep undefined
+ }
+
+ attendee.role = aRole;
+ attendee.participationStatus = aStatus;
+ attendee.userType = aType;
+ attendee.isOrganizer = !!aIsOrganizer;
+ if (aProperties) {
+ for (let pair of aProperties.split(",")) {
+ let [key, value] = pair.split(":");
+ attendee.setProperty(decodeURIComponent(key), decodeURIComponent(value));
+ }
+ }
+
+ return attendee.icalString;
+ } catch (e) {
+ // There are some attendees with a null ID. We are taking
+ // the opportunity to remove them here.
+ cal.ERROR("Error converting attendee, removing: " + e);
+ return null;
+ }
+ },
+ });
+ migrateToIcalString(
+ tbl,
+ "cal_attendees",
+ "translateAttendee",
+ [
+ "attendee_id",
+ "common_name",
+ "rsvp",
+ "role",
+ "status",
+ "type",
+ "is_organizer",
+ "properties",
+ ],
+ db
+ );
+
+ // Update recurrence table to using icalString directly
+ createFunction(db, "translateRecurrence", 17, {
+ onFunctionCall(storArgs) {
+ function parseInt10(x) {
+ return parseInt(x, 10);
+ }
+ try {
+ let [
+ // eslint-disable-next-line no-unused-vars
+ aIndex,
+ aType,
+ aIsNegative,
+ aDates,
+ aCount,
+ aEndDate,
+ aInterval,
+ aSecond,
+ aMinute,
+ aHour,
+ aDay,
+ aMonthday,
+ aYearday,
+ aWeekno,
+ aMonth,
+ aSetPos,
+ aTmpFlags,
+ ] = mapStorageArgs(storArgs);
+
+ let ritem;
+ if (aType == "x-date") {
+ ritem = cal.createRecurrenceDate();
+ ritem.date = textToDate(aDates);
+ ritem.isNegative = !!aIsNegative;
+ } else {
+ ritem = cal.createRecurrenceRule();
+ ritem.type = aType;
+ ritem.isNegative = !!aIsNegative;
+ if (aCount) {
+ try {
+ ritem.count = aCount;
+ } catch (exc) {
+ // Don't fail if setting an invalid count
+ }
+ } else if (aEndDate) {
+ let allday = (aTmpFlags & CAL_ITEM_FLAG.EVENT_ALLDAY) != 0;
+ let untilDate = newDateTime(aEndDate, allday ? "" : "UTC");
+ if (allday) {
+ untilDate.isDate = true;
+ }
+ ritem.untilDate = untilDate;
+ } else {
+ ritem.untilDate = null;
+ }
+
+ try {
+ ritem.interval = aInterval;
+ } catch (exc) {
+ // Don't fail if setting an invalid interval
+ }
+
+ let rtypes = {
+ SECOND: aSecond,
+ MINUTE: aMinute,
+ HOUR: aHour,
+ DAY: aDay,
+ MONTHDAY: aMonthday,
+ YEARDAY: aYearday,
+ WEEKNO: aWeekno,
+ MONTH: aMonth,
+ SETPOS: aSetPos,
+ };
+
+ for (let rtype in rtypes) {
+ if (rtypes[rtype]) {
+ let comp = "BY" + rtype;
+ let rstr = rtypes[rtype].toString();
+ let rarray = rstr.split(",").map(parseInt10);
+ ritem.setComponent(comp, rarray);
+ }
+ }
+ }
+
+ return ritem.icalString;
+ } catch (e) {
+ cal.ERROR("Error converting recurrence: " + e);
+ throw e;
+ }
+ },
+ });
+
+ // The old code relies on the item allday state, we need to temporarily
+ // copy this into the rec table so the above function can update easier.
+ // This column will be deleted during the migrateToIcalString call.
+ addColumn(tbl, "cal_recurrence", ["tmp_date_tz"], "", db);
+ executeSimpleSQL(
+ db,
+ // prettier-ignore
+ "UPDATE cal_recurrence SET tmp_date_tz = " +
+ "(SELECT e.flags FROM cal_events AS e " +
+ " WHERE e.id = cal_recurrence.item_id " +
+ " AND e.cal_id = cal_recurrence.cal_id " +
+ " UNION SELECT t.flags FROM cal_todos AS t " +
+ " WHERE t.id = cal_recurrence.item_id " +
+ " AND t.cal_id = cal_recurrence.cal_id)"
+ );
+
+ migrateToIcalString(
+ tbl,
+ "cal_recurrence",
+ "translateRecurrence",
+ [
+ "recur_index",
+ "recur_type",
+ "is_negative",
+ "dates",
+ "count",
+ "end_date",
+ "interval",
+ "second",
+ "minute",
+ "hour",
+ "day",
+ "monthday",
+ "yearday",
+ "weekno",
+ "month",
+ "setpos",
+ "tmp_date_tz",
+ ],
+ db
+ );
+
+ setDbVersionAndCommit(db, 22);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+ return tbl;
+};
+
+upgrade.v23 = function (db, version) {
+ let tbl = upgrade.v22(version < 22 && db, version);
+ LOGdb(db, "Storage: Upgrading to v23");
+ beginTransaction(db);
+ try {
+ addTable(
+ tbl,
+ "cal_parameters",
+ {
+ cal_id: "TEXT",
+ item_id: "TEXT",
+ recurrence_id: "INTEGER",
+ recurrence_id_tz: "TEXT",
+ key1: "TEXT",
+ key2: "TEXT",
+ value: "BLOB",
+ },
+ db
+ );
+ let allIds = ["cal_id", "item_id", "recurrence_id", "recurrence_id_tz"];
+ createIndex(tbl, "cal_parameters", allIds, db);
+ setDbVersionAndCommit(db, 23);
+ } catch (e) {
+ throw reportErrorAndRollback(db, e);
+ }
+ return tbl;
+};
diff --git a/comm/calendar/providers/storage/components.conf b/comm/calendar/providers/storage/components.conf
new file mode 100644
index 0000000000..a040500694
--- /dev/null
+++ b/comm/calendar/providers/storage/components.conf
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/
+
+Classes = [
+ {
+ 'cid': '{b3eaa1c4-5dfe-4c0a-b62a-b3a514218461}',
+ 'contract_ids': ['@mozilla.org/calendar/calendar;1?type=storage'],
+ 'jsm': 'resource:///modules/CalStorageCalendar.jsm',
+ 'constructor': 'CalStorageCalendar',
+ },
+] \ No newline at end of file
diff --git a/comm/calendar/providers/storage/moz.build b/comm/calendar/providers/storage/moz.build
new file mode 100644
index 0000000000..01343a30b5
--- /dev/null
+++ b/comm/calendar/providers/storage/moz.build
@@ -0,0 +1,28 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "CalStorageCalendar.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+EXTRA_JS_MODULES.calendar += [
+ "CalStorageCachedItemModel.jsm",
+ "CalStorageDatabase.jsm",
+ "calStorageHelpers.jsm",
+ "CalStorageItemModel.jsm",
+ "CalStorageMetaDataModel.jsm",
+ "CalStorageModelBase.jsm",
+ "CalStorageModelFactory.jsm",
+ "CalStorageOfflineModel.jsm",
+ "CalStorageStatements.jsm",
+ "calStorageUpgrade.jsm",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("Calendar", "Provider: Local Storage")