Revision control
Copy as Markdown
Other Tools
/* 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
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "l10n", () => new Localization(["calendar/calendar.ftl"], true));
export 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();
  },
  //
  // @see {calICalendarProvider}
  //
  get displayName() {
    return lazy.l10n.formatValueSync("memory-name");
  },
  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;
  },
  // @see {calICalendar}
  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<calIItemBase> addItem(in calIItemBase aItem);
  async addItem(aItem) {
    const newItem = aItem.clone();
    return this.adoptItem(newItem);
  },
  // Promise<calIItemBase> 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)
      );
    }
    // 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<calIItemBase> 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);
    }
    const 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");
    }
    const 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;
      const storedOldItem = this.mItems[aOldItem.id];
      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<void> deleteItem(in calIItemBase item);
  async deleteItem(item) {
    const 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<calIItemBase|null> getItem(in string id);
  async getItem(aId) {
    return this.mItems[aId] || null;
  },
  // ReadableStream<calIItemBase> getItems(in unsigned long itemFilter,
  //                                       in unsigned long count,
  //                                       in calIDateTime rangeStart,
  //                                       in calIDateTime rangeEnd)
  getItems(itemFilter, count, rangeStart, rangeEnd) {
    let itemsFound = [];
    //
    // filters
    //
    let wantUnrespondedInvitations =
      (itemFilter & Ci.calICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION) != 0;
    let superCal;
    try {
      superCal = this.superCalendar.QueryInterface(Ci.calISchedulingSupport);
    } catch (exc) {
      wantUnrespondedInvitations = false;
    }
    function checkUnrespondedInvitation(item) {
      const att = superCal.getInvitedAttendee(item);
      return att && att.participationStatus == "NEEDS-ACTION";
    }
    // item base type
    const wantEvents = (itemFilter & Ci.calICalendar.ITEM_FILTER_TYPE_EVENT) != 0;
    const wantTodos = (itemFilter & Ci.calICalendar.ITEM_FILTER_TYPE_TODO) != 0;
    if (!wantEvents && !wantTodos) {
      // bail.
      return CalReadableStreamFactory.createEmptyReadableStream();
    }
    // completed?
    const itemCompletedFilter = (itemFilter & Ci.calICalendar.ITEM_FILTER_COMPLETED_YES) != 0;
    const itemNotCompletedFilter = (itemFilter & Ci.calICalendar.ITEM_FILTER_COMPLETED_NO) != 0;
    function checkCompleted(item) {
      item.QueryInterface(Ci.calITodo);
      return item.isCompleted ? itemCompletedFilter : itemNotCompletedFilter;
    }
    // return occurrences?
    const itemReturnOccurrences = (itemFilter & Ci.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 & Ci.calICalendar.ITEM_FILTER_OFFLINE_CREATED) != 0) {
      requestedFlag = Ci.calIChangeLog.OFFLINE_FLAG_CREATED_RECORD;
    } else if ((itemFilter & Ci.calICalendar.ITEM_FILTER_OFFLINE_MODIFIED) != 0) {
      requestedFlag = Ci.calIChangeLog.OFFLINE_FLAG_MODIFIED_RECORD;
    } else if ((itemFilter & Ci.calICalendar.ITEM_FILTER_OFFLINE_DELETED) != 0) {
      requestedFlag = Ci.calIChangeLog.OFFLINE_FLAG_DELETED_RECORD;
    }
    const 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 != ${Ci.calIChangeLog.OFFLINE_FLAG_DELETED_RECORD}))
      //  OR offline_journal == :offline_journal
      return (
        (!reqFlag && (!itemFlag || itemFlag != Ci.calIChangeLog.OFFLINE_FLAG_DELETED_RECORD)) ||
        itemFlag == reqFlag
      );
    };
    const self = this;
    return CalReadableStreamFactory.createBoundedReadableStream(
      count,
      CalReadableStreamFactory.defaultQueueSize,
      {
        async start(controller) {
          return new Promise(resolve => {
            cal.iterate.forEach(
              self.mItems,
              ([, item]) => {
                const isEvent_ = item.isEvent();
                if (isEvent_) {
                  if (!wantEvents) {
                    return cal.iterate.forEach.CONTINUE;
                  }
                } else if (!wantTodos) {
                  return cal.iterate.forEach.CONTINUE;
                }
                const hasItemFlag = item.id in self.mOfflineFlags;
                const 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.
                  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] = Ci.calIChangeLog.OFFLINE_FLAG_CREATED_RECORD;
  },
  async modifyOfflineItem(aItem) {
    const oldFlag = this.mOfflineFlags[aItem.id];
    if (
      oldFlag != Ci.calIChangeLog.OFFLINE_FLAG_CREATED_RECORD &&
      oldFlag != Ci.calIChangeLog.OFFLINE_FLAG_DELETED_RECORD
    ) {
      this.mOfflineFlags[aItem.id] = Ci.calIChangeLog.OFFLINE_FLAG_MODIFIED_RECORD;
    }
    this.notifyOperationComplete(null, Cr.NS_OK, Ci.calIOperationListener.MODIFY, aItem.id, aItem);
    return aItem;
  },
  async deleteOfflineItem(aItem) {
    const oldFlag = this.mOfflineFlags[aItem.id];
    if (oldFlag == Ci.calIChangeLog.OFFLINE_FLAG_CREATED_RECORD) {
      delete this.mItems[aItem.id];
      delete this.mOfflineFlags[aItem.id];
    } else {
      this.mOfflineFlags[aItem.id] = Ci.calIChangeLog.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()];
  },
};