/* 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 = ["CalMemoryCalendar"]; var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); var { CalReadableStreamFactory } = ChromeUtils.import( "resource:///modules/CalReadableStreamFactory.jsm" ); var cICL = Ci.calIChangeLog; function CalMemoryCalendar() { this.initProviderBase(); this.initMemoryCalendar(); } var calMemoryCalendarClassID = Components.ID("{bda0dd7f-0a2f-4fcf-ba08-5517e6fbf133}"); var calMemoryCalendarInterfaces = [ "calICalendar", "calISchedulingSupport", "calIOfflineStorage", "calISyncWriteCalendar", "calICalendarProvider", ]; CalMemoryCalendar.prototype = { __proto__: cal.provider.BaseClass.prototype, classID: calMemoryCalendarClassID, QueryInterface: cal.generateQI(calMemoryCalendarInterfaces), classInfo: cal.generateCI({ classID: calMemoryCalendarClassID, contractID: "@mozilla.org/calendar/calendar;1?type=memory", classDescription: "Calendar Memory Provider", interfaces: calMemoryCalendarInterfaces, }), mItems: null, mOfflineFlags: null, mObservers: null, mMetaData: null, initMemoryCalendar() { this.mObservers = new cal.data.ObserverSet(Ci.calIObserver); this.mItems = {}; this.mOfflineFlags = {}; this.mMetaData = new Map(); }, // // calICalendarProvider interface // get displayName() { return cal.l10n.getCalString("memoryName"); }, get shortName() { return this.displayName; }, deleteCalendar(calendar, listener) { calendar = calendar.wrappedJSObject; calendar.mItems = {}; calendar.mMetaData = new Map(); try { listener.onDeleteCalendar(calendar, Cr.NS_OK, null); } catch (ex) { // Don't bail out if the listener fails } }, detectCalendars() { throw Components.Exception( "CalMemoryCalendar 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": case "requiresNetwork": return false; case "capabilities.priority.supported": return true; case "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 "memory"; }, // Promise addItem(in calIItemBase aItem); async addItem(aItem) { let newItem = aItem.clone(); return this.adoptItem(newItem); }, // Promise adoptItem(in calIItemBase aItem); async adoptItem(aItem) { if (this.readOnly) { throw Ci.calIErrors.CAL_IS_READONLY; } if (aItem.id == null && aItem.isMutable) { aItem.id = cal.getUUID(); } if (aItem.id == null) { this.notifyOperationComplete( null, Cr.NS_ERROR_FAILURE, Ci.calIOperationListener.ADD, aItem.id, "Can't set ID on non-mutable item to addItem" ); return Promise.reject( new Components.Exception("Can't set ID on non-mutable item to addItem", Cr.NS_ERROR_FAILURE) ); } // Lines below are commented because of the offline bug 380060, the // memory calendar cannot assume that a new item should not have an ID. // calCachedCalendar could send over an item with an id. /* if (this.mItems[aItem.id] != null) { if (this.relaxedMode) { // we possibly want to interact with the user before deleting delete this.mItems[aItem.id]; } else { this.notifyOperationComplete(aListener, Ci.calIErrors.DUPLICATE_ID, Ci.calIOperationListener.ADD, aItem.id, "ID already exists for addItem"); return; } } */ let parentItem = aItem.parentItem; if (parentItem != aItem) { parentItem = parentItem.clone(); parentItem.recurrenceInfo.modifyException(aItem, true); } parentItem.calendar = this.superCalendar; parentItem.makeImmutable(); this.mItems[aItem.id] = parentItem; // notify observers this.mObservers.notify("onAddItem", [aItem]); return aItem; }, // Promise modifyItem(in calIItemBase aNewItem, in calIItemBase aOldItem) async modifyItem(aNewItem, aOldItem) { if (this.readOnly) { throw Ci.calIErrors.CAL_IS_READONLY; } if (!aNewItem) { throw Components.Exception("aNewItem must be set", Cr.NS_ERROR_INVALID_ARG); } 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 (!aNewItem.id) { // this is definitely an error return reportError("ID for modifyItem item is null"); } let modifiedItem = aNewItem.parentItem.clone(); 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 our items array and use this as an old item // later on. if (!aOldItem) { aOldItem = this.mItems[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 = modifiedItem; } aOldItem = aOldItem.parentItem; } else if (!this.relaxedMode) { if (!aOldItem || !this.mItems[aNewItem.id]) { // no old item found? should be using addItem, then. return reportError( "ID for modifyItem doesn't exist, is null, or is from different calendar" ); } // do the old and new items match? if (aOldItem.id != modifiedItem.id) { return reportError("item ID mismatch between old and new items"); } aOldItem = aOldItem.parentItem; let storedOldItem = this.mItems[aOldItem.id]; // compareItems is not suitable here. See bug 418805. // Cannot compare here due to bug 380060 if (!cal.item.compareContent(storedOldItem, aOldItem)) { return reportError( "old item mismatch in modifyItem. storedId:" + storedOldItem.icalComponent + " old item:" + aOldItem.icalComponent ); } // offline bug if (aOldItem.generation != storedOldItem.generation) { return reportError("generation mismatch in modifyItem"); } 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(); this.mItems[modifiedItem.id] = modifiedItem; this.notifyOperationComplete( null, Cr.NS_OK, Ci.calIOperationListener.MODIFY, modifiedItem.id, modifiedItem ); // notify observers this.mObservers.notify("onModifyItem", [modifiedItem, aOldItem]); return modifiedItem; }, // Promise 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.id == null) { return onError("ID is null in deleteItem", Cr.NS_ERROR_FAILURE); } let oldItem; if (this.relaxedMode) { oldItem = item; } else { oldItem = this.mItems[item.id]; if (oldItem.generation != item.generation) { return onError("generation mismatch in deleteItem", Cr.NS_ERROR_FAILURE); } } delete this.mItems[item.id]; this.mMetaData.delete(item.id); this.notifyOperationComplete(null, Cr.NS_OK, Ci.calIOperationListener.DELETE, item.id, item); // notify observers this.mObservers.notify("onDeleteItem", [oldItem]); return null; }, // Promise getItem(in string id); async getItem(aId) { return this.mItems[aId] || null; }, // ReadableStream getItems(in unsigned long itemFilter, // in unsigned long count, // in calIDateTime rangeStart, // in calIDateTime rangeEnd) getItems(itemFilter, count, rangeStart, rangeEnd) { const calICalendar = Ci.calICalendar; let itemsFound = []; // // filters // let wantUnrespondedInvitations = (itemFilter & calICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION) != 0; let superCal; try { superCal = this.superCalendar.QueryInterface(Ci.calISchedulingSupport); } catch (exc) { wantUnrespondedInvitations = false; } function checkUnrespondedInvitation(item) { let att = superCal.getInvitedAttendee(item); return att && att.participationStatus == "NEEDS-ACTION"; } // item base type let wantEvents = (itemFilter & calICalendar.ITEM_FILTER_TYPE_EVENT) != 0; let wantTodos = (itemFilter & calICalendar.ITEM_FILTER_TYPE_TODO) != 0; if (!wantEvents && !wantTodos) { // bail. return CalReadableStreamFactory.createEmptyReadableStream(); } // completed? let itemCompletedFilter = (itemFilter & calICalendar.ITEM_FILTER_COMPLETED_YES) != 0; let itemNotCompletedFilter = (itemFilter & calICalendar.ITEM_FILTER_COMPLETED_NO) != 0; function checkCompleted(item) { item.QueryInterface(Ci.calITodo); return item.isCompleted ? itemCompletedFilter : itemNotCompletedFilter; } // return occurrences? let itemReturnOccurrences = (itemFilter & calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) != 0; rangeStart = cal.dtz.ensureDateTime(rangeStart); rangeEnd = cal.dtz.ensureDateTime(rangeEnd); let startTime = -9223372036854775000; if (rangeStart) { startTime = rangeStart.nativeTime; } let requestedFlag = 0; if ((itemFilter & calICalendar.ITEM_FILTER_OFFLINE_CREATED) != 0) { requestedFlag = cICL.OFFLINE_FLAG_CREATED_RECORD; } else if ((itemFilter & calICalendar.ITEM_FILTER_OFFLINE_MODIFIED) != 0) { requestedFlag = cICL.OFFLINE_FLAG_MODIFIED_RECORD; } else if ((itemFilter & calICalendar.ITEM_FILTER_OFFLINE_DELETED) != 0) { requestedFlag = cICL.OFFLINE_FLAG_DELETED_RECORD; } let matchOffline = function (itemFlag, reqFlag) { // Same as storage calendar sql query. For comparison: // reqFlag is :offline_journal (parameter), // itemFlag is offline_journal (field value) // ... // AND (:offline_journal IS NULL // AND (offline_journal IS NULL // OR offline_journal != ${cICL.OFFLINE_FLAG_DELETED_RECORD})) // OR offline_journal == :offline_journal return ( (!reqFlag && (!itemFlag || itemFlag != cICL.OFFLINE_FLAG_DELETED_RECORD)) || itemFlag == reqFlag ); }; let self = this; return CalReadableStreamFactory.createBoundedReadableStream( count, CalReadableStreamFactory.defaultQueueSize, { async start(controller) { return new Promise(resolve => { cal.iterate.forEach( self.mItems, ([id, item]) => { let isEvent_ = item.isEvent(); if (isEvent_) { if (!wantEvents) { return cal.iterate.forEach.CONTINUE; } } else if (!wantTodos) { return cal.iterate.forEach.CONTINUE; } let hasItemFlag = item.id in self.mOfflineFlags; let itemFlag = hasItemFlag ? self.mOfflineFlags[item.id] : 0; // If the offline flag doesn't match, skip the item if (!matchOffline(itemFlag, requestedFlag)) { return cal.iterate.forEach.CONTINUE; } if (itemReturnOccurrences && item.recurrenceInfo) { if (item.recurrenceInfo.recurrenceEndDate < startTime) { return cal.iterate.forEach.CONTINUE; } let startDate = rangeStart; if (!rangeStart && item.isTodo()) { startDate = item.entryDate; } let occurrences = item.recurrenceInfo.getOccurrences( startDate, rangeEnd, count ? count - itemsFound.length : 0 ); if (wantUnrespondedInvitations) { occurrences = occurrences.filter(checkUnrespondedInvitation); } if (!isEvent_) { occurrences = occurrences.filter(checkCompleted); } itemsFound = itemsFound.concat(occurrences); } else if ( (!wantUnrespondedInvitations || checkUnrespondedInvitation(item)) && (isEvent_ || checkCompleted(item)) && cal.item.checkIfInRange(item, rangeStart, rangeEnd) ) { // This needs fixing for recurring items, e.g. DTSTART of parent may occur before rangeStart. // This will be changed with bug 416975. itemsFound.push(item); } if (controller.maxTotalItemsReached) { return cal.iterate.forEach.BREAK; } return cal.iterate.forEach.CONTINUE; }, () => { controller.enqueue(itemsFound); controller.close(); resolve(); } ); }); }, } ); }, // // calIOfflineStorage interface // async addOfflineItem(aItem) { this.mOfflineFlags[aItem.id] = cICL.OFFLINE_FLAG_CREATED_RECORD; }, async modifyOfflineItem(aItem) { let oldFlag = this.mOfflineFlags[aItem.id]; if ( oldFlag != cICL.OFFLINE_FLAG_CREATED_RECORD && oldFlag != cICL.OFFLINE_FLAG_DELETED_RECORD ) { this.mOfflineFlags[aItem.id] = cICL.OFFLINE_FLAG_MODIFIED_RECORD; } this.notifyOperationComplete(null, Cr.NS_OK, Ci.calIOperationListener.MODIFY, aItem.id, aItem); return aItem; }, async deleteOfflineItem(aItem) { let oldFlag = this.mOfflineFlags[aItem.id]; if (oldFlag == cICL.OFFLINE_FLAG_CREATED_RECORD) { delete this.mItems[aItem.id]; delete this.mOfflineFlags[aItem.id]; } else { this.mOfflineFlags[aItem.id] = cICL.OFFLINE_FLAG_DELETED_RECORD; } // notify observers this.observers.notify("onDeleteItem", [aItem]); }, async getItemOfflineFlag(aItem) { return aItem && aItem.id in this.mOfflineFlags ? this.mOfflineFlags[aItem.id] : null; }, async resetItemOfflineFlag(aItem) { delete this.mOfflineFlags[aItem.id]; }, // // calISyncWriteCalendar interface // setMetaData(id, value) { this.mMetaData.set(id, value); }, deleteMetaData(id) { this.mMetaData.delete(id); }, getMetaData(id) { return this.mMetaData.get(id); }, getAllMetaDataIds() { return [...this.mMetaData.keys()]; }, getAllMetaDataValues() { return [...this.mMetaData.values()]; }, };