summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/src
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/base/src
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/base/src')
-rw-r--r--comm/calendar/base/src/CalAlarm.jsm693
-rw-r--r--comm/calendar/base/src/CalAlarmMonitor.jsm233
-rw-r--r--comm/calendar/base/src/CalAlarmService.jsm827
-rw-r--r--comm/calendar/base/src/CalAttachment.jsm169
-rw-r--r--comm/calendar/base/src/CalAttendee.jsm212
-rw-r--r--comm/calendar/base/src/CalCalendarManager.jsm1076
-rw-r--r--comm/calendar/base/src/CalDateTime.jsm202
-rw-r--r--comm/calendar/base/src/CalDefaultACLManager.jsm97
-rw-r--r--comm/calendar/base/src/CalDeletedItems.jsm200
-rw-r--r--comm/calendar/base/src/CalDuration.jsm106
-rw-r--r--comm/calendar/base/src/CalEvent.jsm225
-rw-r--r--comm/calendar/base/src/CalFreeBusyService.jsm89
-rw-r--r--comm/calendar/base/src/CalICSService.jsm604
-rw-r--r--comm/calendar/base/src/CalIcsParser.jsm334
-rw-r--r--comm/calendar/base/src/CalIcsSerializer.jsm77
-rw-r--r--comm/calendar/base/src/CalItipItem.jsm212
-rw-r--r--comm/calendar/base/src/CalMetronome.jsm142
-rw-r--r--comm/calendar/base/src/CalMimeConverter.jsm69
-rw-r--r--comm/calendar/base/src/CalPeriod.jsm87
-rw-r--r--comm/calendar/base/src/CalProtocolHandler.jsm63
-rw-r--r--comm/calendar/base/src/CalReadableStreamFactory.jsm314
-rw-r--r--comm/calendar/base/src/CalRecurrenceDate.jsm122
-rw-r--r--comm/calendar/base/src/CalRecurrenceInfo.jsm847
-rw-r--r--comm/calendar/base/src/CalRecurrenceRule.jsm268
-rw-r--r--comm/calendar/base/src/CalRelation.jsm125
-rw-r--r--comm/calendar/base/src/CalStartupService.jsm124
-rw-r--r--comm/calendar/base/src/CalTimezone.jsm77
-rw-r--r--comm/calendar/base/src/CalTimezoneService.jsm228
-rw-r--r--comm/calendar/base/src/CalTodo.jsm264
-rw-r--r--comm/calendar/base/src/CalTransactionManager.jsm372
-rw-r--r--comm/calendar/base/src/CalWeekInfoService.jsm113
-rw-r--r--comm/calendar/base/src/TimezoneDatabase.cpp114
-rw-r--r--comm/calendar/base/src/TimezoneDatabase.h20
-rw-r--r--comm/calendar/base/src/calApplicationUtils.js47
-rw-r--r--comm/calendar/base/src/calCachedCalendar.js957
-rw-r--r--comm/calendar/base/src/calICSService-worker.js21
-rw-r--r--comm/calendar/base/src/calInternalInterfaces.idl29
-rw-r--r--comm/calendar/base/src/calItemBase.js1198
-rw-r--r--comm/calendar/base/src/components.conf208
-rw-r--r--comm/calendar/base/src/moz.build71
40 files changed, 11236 insertions, 0 deletions
diff --git a/comm/calendar/base/src/CalAlarm.jsm b/comm/calendar/base/src/CalAlarm.jsm
new file mode 100644
index 0000000000..d6f0faffaa
--- /dev/null
+++ b/comm/calendar/base/src/CalAlarm.jsm
@@ -0,0 +1,693 @@
+/* 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 = ["CalAlarm"];
+
+var { PluralForm } = ChromeUtils.importESModule("resource://gre/modules/PluralForm.sys.mjs");
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ CalAttachment: "resource:///modules/CalAttachment.jsm",
+ CalAttendee: "resource:///modules/CalAttendee.jsm",
+ CalDateTime: "resource:///modules/CalDateTime.jsm",
+ CalDuration: "resource:///modules/CalDuration.jsm",
+});
+
+const ALARM_RELATED_ABSOLUTE = Ci.calIAlarm.ALARM_RELATED_ABSOLUTE;
+const ALARM_RELATED_START = Ci.calIAlarm.ALARM_RELATED_START;
+const ALARM_RELATED_END = Ci.calIAlarm.ALARM_RELATED_END;
+
+/**
+ * Constructor for `calIAlarm` objects.
+ *
+ * @class
+ * @implements {calIAlarm}
+ * @param {string} [icalString] - Optional iCal string for initializing existing alarms.
+ */
+function CalAlarm(icalString) {
+ this.wrappedJSObject = this;
+ this.mProperties = new Map();
+ this.mPropertyParams = {};
+ this.mAttendees = [];
+ this.mAttachments = [];
+ if (icalString) {
+ this.icalString = icalString;
+ }
+}
+
+CalAlarm.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIAlarm"]),
+ classID: Components.ID("{b8db7c7f-c168-4e11-becb-f26c1c4f5f8f}"),
+
+ mProperties: null,
+ mPropertyParams: null,
+ mAction: null,
+ mAbsoluteDate: null,
+ mOffset: null,
+ mDuration: null,
+ mAttendees: null,
+ mAttachments: null,
+ mSummary: null,
+ mDescription: null,
+ mLastAck: null,
+ mImmutable: false,
+ mRelated: 0,
+ mRepeat: 0,
+
+ /**
+ * calIAlarm
+ */
+
+ ensureMutable() {
+ if (this.mImmutable) {
+ throw Components.Exception("", Cr.NS_ERROR_OBJECT_IS_IMMUTABLE);
+ }
+ },
+
+ get isMutable() {
+ return !this.mImmutable;
+ },
+
+ makeImmutable() {
+ if (this.mImmutable) {
+ return;
+ }
+
+ const objectMembers = ["mAbsoluteDate", "mOffset", "mDuration", "mLastAck"];
+ for (let member of objectMembers) {
+ if (this[member] && this[member].isMutable) {
+ this[member].makeImmutable();
+ }
+ }
+
+ // Properties
+ for (let propval of this.mProperties.values()) {
+ if (propval?.isMutable) {
+ propval.makeImmutable();
+ }
+ }
+
+ this.mImmutable = true;
+ },
+
+ clone() {
+ let cloned = new CalAlarm();
+
+ cloned.mImmutable = false;
+
+ const simpleMembers = ["mAction", "mSummary", "mDescription", "mRelated", "mRepeat"];
+
+ const arrayMembers = ["mAttendees", "mAttachments"];
+
+ const objectMembers = ["mAbsoluteDate", "mOffset", "mDuration", "mLastAck"];
+
+ for (let member of simpleMembers) {
+ cloned[member] = this[member];
+ }
+
+ for (let member of arrayMembers) {
+ let newArray = [];
+ for (let oldElem of this[member]) {
+ newArray.push(oldElem.clone());
+ }
+ cloned[member] = newArray;
+ }
+
+ for (let member of objectMembers) {
+ if (this[member] && this[member].clone) {
+ cloned[member] = this[member].clone();
+ } else {
+ cloned[member] = this[member];
+ }
+ }
+
+ // X-Props
+ cloned.mProperties = new Map();
+ for (let [name, value] of this.mProperties.entries()) {
+ if (value instanceof lazy.CalDateTime || value instanceof Ci.calIDateTime) {
+ value = value.clone();
+ }
+
+ cloned.mProperties.set(name, value);
+
+ let propBucket = this.mPropertyParams[name];
+ if (propBucket) {
+ let newBucket = {};
+ for (let param in propBucket) {
+ newBucket[param] = propBucket[param];
+ }
+ cloned.mPropertyParams[name] = newBucket;
+ }
+ }
+ return cloned;
+ },
+
+ get related() {
+ return this.mRelated;
+ },
+ set related(aValue) {
+ this.ensureMutable();
+ switch (aValue) {
+ case ALARM_RELATED_ABSOLUTE:
+ this.mOffset = null;
+ break;
+ case ALARM_RELATED_START:
+ case ALARM_RELATED_END:
+ this.mAbsoluteDate = null;
+ break;
+ }
+
+ this.mRelated = aValue;
+ },
+
+ get action() {
+ return this.mAction || "DISPLAY";
+ },
+ set action(aValue) {
+ this.ensureMutable();
+ this.mAction = aValue;
+ },
+
+ get description() {
+ if (this.action == "AUDIO") {
+ return null;
+ }
+ return this.mDescription;
+ },
+ set description(aValue) {
+ this.ensureMutable();
+ this.mDescription = aValue;
+ },
+
+ get summary() {
+ if (this.mAction == "DISPLAY" || this.mAction == "AUDIO") {
+ return null;
+ }
+ return this.mSummary;
+ },
+ set summary(aValue) {
+ this.ensureMutable();
+ this.mSummary = aValue;
+ },
+
+ get offset() {
+ return this.mOffset;
+ },
+ set offset(aValue) {
+ if (aValue && !(aValue instanceof lazy.CalDuration) && !(aValue instanceof Ci.calIDuration)) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ if (this.related != ALARM_RELATED_START && this.related != ALARM_RELATED_END) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ this.ensureMutable();
+ this.mOffset = aValue;
+ },
+
+ get alarmDate() {
+ return this.mAbsoluteDate;
+ },
+ set alarmDate(aValue) {
+ if (aValue && !(aValue instanceof lazy.CalDateTime) && !(aValue instanceof Ci.calIDateTime)) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ if (this.related != ALARM_RELATED_ABSOLUTE) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ this.ensureMutable();
+ this.mAbsoluteDate = aValue;
+ },
+
+ get repeat() {
+ if (!this.mDuration) {
+ return 0;
+ }
+ return this.mRepeat || 0;
+ },
+ set repeat(aValue) {
+ this.ensureMutable();
+ if (aValue === null) {
+ this.mRepeat = null;
+ } else {
+ this.mRepeat = parseInt(aValue, 10);
+ if (isNaN(this.mRepeat)) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+ },
+
+ get repeatOffset() {
+ if (!this.mRepeat) {
+ return null;
+ }
+ return this.mDuration;
+ },
+ set repeatOffset(aValue) {
+ this.ensureMutable();
+ if (
+ aValue !== null &&
+ !(aValue instanceof lazy.CalDuration) &&
+ !(aValue instanceof Ci.calIDuration)
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ this.mDuration = aValue;
+ },
+
+ get repeatDate() {
+ if (
+ this.related != ALARM_RELATED_ABSOLUTE ||
+ !this.mAbsoluteDate ||
+ !this.mRepeat ||
+ !this.mDuration
+ ) {
+ return null;
+ }
+
+ let alarmDate = this.mAbsoluteDate.clone();
+
+ // All Day events are handled as 00:00:00
+ alarmDate.isDate = false;
+ alarmDate.addDuration(this.mDuration);
+ return alarmDate;
+ },
+
+ getAttendees() {
+ let attendees;
+ if (this.action == "AUDIO" || this.action == "DISPLAY") {
+ attendees = [];
+ } else {
+ attendees = this.mAttendees.concat([]);
+ }
+ return attendees;
+ },
+
+ addAttendee(aAttendee) {
+ // Make sure its not duplicate
+ this.deleteAttendee(aAttendee);
+
+ // Now check if its valid
+ if (this.action == "AUDIO" || this.action == "DISPLAY") {
+ throw new Error("Alarm type AUDIO/DISPLAY may not have attendees");
+ }
+
+ // And add it (again)
+ this.mAttendees.push(aAttendee);
+ },
+
+ deleteAttendee(aAttendee) {
+ let deleteId = aAttendee.id;
+ for (let i = 0; i < this.mAttendees.length; i++) {
+ if (this.mAttendees[i].id == deleteId) {
+ this.mAttendees.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ clearAttendees() {
+ this.mAttendees = [];
+ },
+
+ getAttachments() {
+ let attachments;
+ if (this.action == "AUDIO") {
+ attachments = this.mAttachments.length ? [this.mAttachments[0]] : [];
+ } else if (this.action == "DISPLAY") {
+ attachments = [];
+ } else {
+ attachments = this.mAttachments.concat([]);
+ }
+ return attachments;
+ },
+
+ addAttachment(aAttachment) {
+ // Make sure its not duplicate
+ this.deleteAttachment(aAttachment);
+
+ // Now check if its valid
+ if (this.action == "AUDIO" && this.mAttachments.length) {
+ throw new Error("Alarm type AUDIO may only have one attachment");
+ } else if (this.action == "DISPLAY") {
+ throw new Error("Alarm type DISPLAY may not have attachments");
+ }
+
+ // And add it (again)
+ this.mAttachments.push(aAttachment);
+ },
+
+ deleteAttachment(aAttachment) {
+ let deleteHash = aAttachment.hashId;
+ for (let i = 0; i < this.mAttachments.length; i++) {
+ if (this.mAttachments[i].hashId == deleteHash) {
+ this.mAttachments.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ clearAttachments() {
+ this.mAttachments = [];
+ },
+
+ get icalString() {
+ let comp = this.icalComponent;
+ return comp ? comp.serializeToICS() : "";
+ },
+ set icalString(val) {
+ this.ensureMutable();
+ this.icalComponent = cal.icsService.parseICS(val);
+ },
+
+ promotedProps: {
+ ACTION: "action",
+ TRIGGER: "offset",
+ REPEAT: "repeat",
+ DURATION: "duration",
+ SUMMARY: "summary",
+ DESCRIPTION: "description",
+ "X-MOZ-LASTACK": "lastAck",
+
+ // These have complex setters and will be ignored in setProperty
+ ATTACH: true,
+ ATTENDEE: true,
+ },
+
+ get icalComponent() {
+ let comp = cal.icsService.createIcalComponent("VALARM");
+
+ // Set up action (REQUIRED)
+ let actionProp = cal.icsService.createIcalProperty("ACTION");
+ actionProp.value = this.action;
+ comp.addProperty(actionProp);
+
+ // Set up trigger (REQUIRED)
+ let triggerProp = cal.icsService.createIcalProperty("TRIGGER");
+ if (this.related == ALARM_RELATED_ABSOLUTE && this.mAbsoluteDate) {
+ // Set the trigger to a specific datetime
+ triggerProp.setParameter("VALUE", "DATE-TIME");
+ triggerProp.valueAsDatetime = this.mAbsoluteDate.getInTimezone(cal.dtz.UTC);
+ } else if (this.related != ALARM_RELATED_ABSOLUTE && this.mOffset) {
+ triggerProp.valueAsIcalString = this.mOffset.icalString;
+ if (this.related == ALARM_RELATED_END) {
+ // An alarm related to the end of the event.
+ triggerProp.setParameter("RELATED", "END");
+ }
+ } else {
+ // No offset or absolute date is not valid.
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ comp.addProperty(triggerProp);
+
+ // Set up repeat and duration (OPTIONAL, but if one exists, the other
+ // MUST also exist)
+ if (this.repeat && this.repeatOffset) {
+ let repeatProp = cal.icsService.createIcalProperty("REPEAT");
+ let durationProp = cal.icsService.createIcalProperty("DURATION");
+
+ repeatProp.value = this.repeat;
+ durationProp.valueAsIcalString = this.repeatOffset.icalString;
+
+ comp.addProperty(repeatProp);
+ comp.addProperty(durationProp);
+ }
+
+ // Set up attendees (REQUIRED for EMAIL action)
+ /* TODO should we be strict here?
+ if (this.action == "EMAIL" && !this.getAttendees().length) {
+ throw Cr.NS_ERROR_NOT_INITIALIZED;
+ } */
+ for (let attendee of this.getAttendees()) {
+ comp.addProperty(attendee.icalProperty);
+ }
+
+ /* TODO should we be strict here?
+ if (this.action == "EMAIL" && !this.attachments.length) {
+ throw Cr.NS_ERROR_NOT_INITIALIZED;
+ } */
+
+ for (let attachment of this.getAttachments()) {
+ comp.addProperty(attachment.icalProperty);
+ }
+
+ // Set up summary (REQUIRED for EMAIL)
+ if (this.summary || this.action == "EMAIL") {
+ let summaryProp = cal.icsService.createIcalProperty("SUMMARY");
+ // Summary needs to have a non-empty value
+ summaryProp.value = this.summary || cal.l10n.getCalString("alarmDefaultSummary");
+ comp.addProperty(summaryProp);
+ }
+
+ // Set up the description (REQUIRED for DISPLAY and EMAIL)
+ if (this.description || this.action == "DISPLAY" || this.action == "EMAIL") {
+ let descriptionProp = cal.icsService.createIcalProperty("DESCRIPTION");
+ // description needs to have a non-empty value
+ descriptionProp.value = this.description || cal.l10n.getCalString("alarmDefaultDescription");
+ comp.addProperty(descriptionProp);
+ }
+
+ // Set up lastAck
+ if (this.lastAck) {
+ let lastAckProp = cal.icsService.createIcalProperty("X-MOZ-LASTACK");
+ lastAckProp.value = this.lastAck;
+ comp.addProperty(lastAckProp);
+ }
+
+ // Set up X-Props. mProperties contains only non-promoted props
+ // eslint-disable-next-line array-bracket-spacing
+ for (let [propName, propValue] of this.mProperties.entries()) {
+ let icalprop = cal.icsService.createIcalProperty(propName);
+ icalprop.value = propValue;
+
+ // Add parameters
+ let propBucket = this.mPropertyParams[propName];
+ if (propBucket) {
+ for (let paramName in propBucket) {
+ try {
+ icalprop.setParameter(paramName, propBucket[paramName]);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG(
+ "Warning: Invalid alarm parameter value " + paramName + "=" + propBucket[paramName]
+ );
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+ comp.addProperty(icalprop);
+ }
+ return comp;
+ },
+ set icalComponent(aComp) {
+ this.ensureMutable();
+ if (!aComp || aComp.componentType != "VALARM") {
+ // Invalid Component
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let actionProp = aComp.getFirstProperty("ACTION");
+ let triggerProp = aComp.getFirstProperty("TRIGGER");
+ let repeatProp = aComp.getFirstProperty("REPEAT");
+ let durationProp = aComp.getFirstProperty("DURATION");
+ let summaryProp = aComp.getFirstProperty("SUMMARY");
+ let descriptionProp = aComp.getFirstProperty("DESCRIPTION");
+ let lastAckProp = aComp.getFirstProperty("X-MOZ-LASTACK");
+
+ if (actionProp) {
+ this.action = actionProp.value;
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ if (triggerProp) {
+ if (triggerProp.getParameter("VALUE") == "DATE-TIME") {
+ this.mAbsoluteDate = triggerProp.valueAsDatetime;
+ this.related = ALARM_RELATED_ABSOLUTE;
+ } else {
+ this.mOffset = cal.createDuration(triggerProp.valueAsIcalString);
+
+ let related = triggerProp.getParameter("RELATED");
+ this.related = related == "END" ? ALARM_RELATED_END : ALARM_RELATED_START;
+ }
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ if (durationProp && repeatProp) {
+ this.repeatOffset = cal.createDuration(durationProp.valueAsIcalString);
+ this.repeat = repeatProp.value;
+ } else if (durationProp || repeatProp) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ } else {
+ this.repeatOffset = null;
+ this.repeat = 0;
+ }
+
+ // Set up attendees
+ this.clearAttendees();
+ for (let attendeeProp of cal.iterate.icalProperty(aComp, "ATTENDEE")) {
+ let attendee = new lazy.CalAttendee();
+ attendee.icalProperty = attendeeProp;
+ this.addAttendee(attendee);
+ }
+
+ // Set up attachments
+ this.clearAttachments();
+ for (let attachProp of cal.iterate.icalProperty(aComp, "ATTACH")) {
+ let attach = new lazy.CalAttachment();
+ attach.icalProperty = attachProp;
+ this.addAttachment(attach);
+ }
+
+ // Set up summary
+ this.summary = summaryProp ? summaryProp.value : null;
+
+ // Set up description
+ this.description = descriptionProp ? descriptionProp.value : null;
+
+ // Set up the alarm lastack. We can't use valueAsDatetime here since
+ // the default for an X-Prop is TEXT and in older versions we didn't set
+ // VALUE=DATE-TIME.
+ this.lastAck = lastAckProp ? cal.createDateTime(lastAckProp.valueAsIcalString) : null;
+
+ this.mProperties = new Map();
+ this.mPropertyParams = {};
+
+ // Other properties
+ for (let prop of cal.iterate.icalProperty(aComp)) {
+ if (!this.promotedProps[prop.propertyName]) {
+ this.setProperty(prop.propertyName, prop.value);
+
+ for (let [paramName, param] of cal.iterate.icalParameter(prop)) {
+ if (!(prop.propertyName in this.mPropertyParams)) {
+ this.mPropertyParams[prop.propertyName] = {};
+ }
+ this.mPropertyParams[prop.propertyName][paramName] = param;
+ }
+ }
+ }
+ },
+
+ hasProperty(aName) {
+ return this.getProperty(aName.toUpperCase()) != null;
+ },
+
+ getProperty(aName) {
+ let name = aName.toUpperCase();
+ if (name in this.promotedProps) {
+ if (this.promotedProps[name] === true) {
+ // Complex promoted props will return undefined
+ return undefined;
+ }
+ return this[this.promotedProps[name]];
+ }
+ return this.mProperties.get(name);
+ },
+
+ setProperty(aName, aValue) {
+ this.ensureMutable();
+ let name = aName.toUpperCase();
+ if (name in this.promotedProps) {
+ if (this.promotedProps[name] === true) {
+ cal.WARN(`Attempted to set complex property ${name} to a simple value ${aValue}`);
+ } else {
+ this[this.promotedProps[name]] = aValue;
+ }
+ } else {
+ this.mProperties.set(name, aValue);
+ }
+ return aValue;
+ },
+
+ deleteProperty(aName) {
+ this.ensureMutable();
+ let name = aName.toUpperCase();
+ if (name in this.promotedProps) {
+ this[this.promotedProps[name]] = null;
+ } else {
+ this.mProperties.delete(name);
+ }
+ },
+
+ get properties() {
+ return [...this.mProperties.entries()];
+ },
+
+ toString(aItem) {
+ function alarmString(aPrefix) {
+ if (!aItem || aItem.isEvent()) {
+ return aPrefix + "Event";
+ } else if (aItem.isTodo()) {
+ return aPrefix + "Task";
+ }
+ return aPrefix;
+ }
+
+ if (this.related == ALARM_RELATED_ABSOLUTE && this.mAbsoluteDate) {
+ // this is an absolute alarm. Use the calendar default timezone and
+ // format it.
+ let formatDate = this.mAbsoluteDate.getInTimezone(cal.dtz.defaultTimezone);
+ return cal.dtz.formatter.formatDateTime(formatDate);
+ } else if (this.related != ALARM_RELATED_ABSOLUTE && this.mOffset) {
+ // Relative alarm length
+ let alarmlen = Math.abs(this.mOffset.inSeconds / 60);
+ if (alarmlen == 0) {
+ // No need to get the other information if the alarm is at the start
+ // of the event/task.
+ if (this.related == ALARM_RELATED_START) {
+ return cal.l10n.getString("calendar-alarms", alarmString("reminderTitleAtStart"));
+ } else if (this.related == ALARM_RELATED_END) {
+ return cal.l10n.getString("calendar-alarms", alarmString("reminderTitleAtEnd"));
+ }
+ }
+
+ let unit;
+ if (alarmlen % 1440 == 0) {
+ // Alarm is in days
+ unit = "unitDays";
+ alarmlen /= 1440;
+ } else if (alarmlen % 60 == 0) {
+ unit = "unitHours";
+ alarmlen /= 60;
+ } else {
+ unit = "unitMinutes";
+ }
+ let localeUnitString = cal.l10n.getCalString(unit);
+ let unitString = PluralForm.get(alarmlen, localeUnitString).replace("#1", alarmlen);
+ let originStringName = "reminderCustomOrigin";
+
+ // Origin
+ switch (this.related) {
+ case ALARM_RELATED_START:
+ originStringName += "Begin";
+ break;
+ case ALARM_RELATED_END:
+ originStringName += "End";
+ break;
+ }
+
+ if (this.offset.isNegative) {
+ originStringName += "Before";
+ } else {
+ originStringName += "After";
+ }
+
+ let originString = cal.l10n.getString("calendar-alarms", alarmString(originStringName));
+ return cal.l10n.getString("calendar-alarms", "reminderCustomTitle", [
+ unitString,
+ originString,
+ ]);
+ }
+ // This is an incomplete alarm, but then again we should never reach
+ // this state.
+ return "[Incomplete calIAlarm]";
+ },
+};
diff --git a/comm/calendar/base/src/CalAlarmMonitor.jsm b/comm/calendar/base/src/CalAlarmMonitor.jsm
new file mode 100644
index 0000000000..0412e72850
--- /dev/null
+++ b/comm/calendar/base/src/CalAlarmMonitor.jsm
@@ -0,0 +1,233 @@
+/* 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 = ["CalAlarmMonitor"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(lazy, "CalEvent", "resource:///modules/CalEvent.jsm");
+
+function peekAlarmWindow() {
+ return Services.wm.getMostRecentWindow("Calendar:AlarmWindow");
+}
+
+/**
+ * The alarm monitor takes care of playing the alarm sound and opening one copy
+ * of the calendar-alarm-dialog. Both depend on their respective prefs to be
+ * set. This monitor is only used for DISPLAY type alarms.
+ */
+function CalAlarmMonitor() {
+ this.wrappedJSObject = this;
+ this.mAlarms = [];
+ // A map from itemId to item.
+ this._notifyingItems = new Map();
+
+ this.mSound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound);
+
+ Services.obs.addObserver(this, "alarm-service-startup");
+ Services.obs.addObserver(this, "alarm-service-shutdown");
+}
+
+var calAlarmMonitorClassID = Components.ID("{4b7ae030-ed79-11d9-8cd6-0800200c9a66}");
+var calAlarmMonitorInterfaces = [Ci.nsIObserver, Ci.calIAlarmServiceObserver];
+CalAlarmMonitor.prototype = {
+ mAlarms: null,
+
+ // This is a work-around for the fact that there is a delay between when
+ // we call openWindow and when it appears via getMostRecentWindow. If an
+ // alarm is fired in that time-frame, it will actually end up in another window.
+ mWindowOpening: null,
+
+ // nsISound instance used for playing all sounds
+ mSound: null,
+
+ classID: calAlarmMonitorClassID,
+ QueryInterface: cal.generateQI(["nsIObserver", "calIAlarmServiceObserver"]),
+ classInfo: cal.generateCI({
+ contractID: "@mozilla.org/calendar/alarm-monitor;1",
+ classDescription: "Calendar Alarm Monitor",
+ classID: calAlarmMonitorClassID,
+ interfaces: calAlarmMonitorInterfaces,
+ flags: Ci.nsIClassInfo.SINGLETON,
+ }),
+
+ /**
+ * nsIObserver
+ */
+ observe(aSubject, aTopic, aData) {
+ let alarmService = Cc["@mozilla.org/calendar/alarm-service;1"].getService(Ci.calIAlarmService);
+ switch (aTopic) {
+ case "alarm-service-startup":
+ alarmService.addObserver(this);
+ break;
+ case "alarm-service-shutdown":
+ alarmService.removeObserver(this);
+ break;
+ case "alertclickcallback": {
+ let item = this._notifyingItems.get(aData);
+ if (item) {
+ let calWindow = cal.window.getCalendarWindow();
+ if (calWindow) {
+ calWindow.openEventDialogForViewing(item, true);
+ }
+ }
+ break;
+ }
+ case "alertfinished":
+ this._notifyingItems.delete(aData);
+ break;
+ }
+ },
+
+ /**
+ * calIAlarmServiceObserver
+ */
+ onAlarm(aItem, aAlarm) {
+ if (aAlarm.action != "DISPLAY") {
+ // This monitor only looks for DISPLAY alarms.
+ return;
+ }
+
+ this.mAlarms.push([aItem, aAlarm]);
+
+ if (Services.prefs.getBoolPref("calendar.alarms.playsound", true)) {
+ // We want to make sure the user isn't flooded with alarms so we
+ // limit this using a preference. For example, if the user has 20
+ // events that fire an alarm in the same minute, then the alarm
+ // sound will only play 5 times. All alarms will be shown in the
+ // dialog nevertheless.
+ let maxAlarmSoundCount = Services.prefs.getIntPref("calendar.alarms.maxsoundsperminute", 5);
+ let now = new Date();
+
+ if (!this.mLastAlarmSoundDate || now - this.mLastAlarmSoundDate >= 60000) {
+ // Last alarm was long enough ago, reset counters. Note
+ // subtracting JSDate results in microseconds.
+ this.mAlarmSoundCount = 0;
+ this.mLastAlarmSoundDate = now;
+ } else {
+ // Otherwise increase the counter
+ this.mAlarmSoundCount++;
+ }
+
+ if (maxAlarmSoundCount > this.mAlarmSoundCount) {
+ // Only ring the alarm sound if we haven't hit the max count.
+ try {
+ let soundURL;
+ if (Services.prefs.getIntPref("calendar.alarms.soundType", 0) == 0) {
+ soundURL = "chrome://calendar/content/sound.wav";
+ } else {
+ soundURL = Services.prefs.getStringPref("calendar.alarms.soundURL", null);
+ }
+ if (soundURL && soundURL.length > 0) {
+ soundURL = Services.io.newURI(soundURL);
+ this.mSound.play(soundURL);
+ } else {
+ this.mSound.beep();
+ }
+ } catch (exc) {
+ cal.ERROR("Error playing alarm sound: " + exc);
+ }
+ }
+ }
+
+ if (!Services.prefs.getBoolPref("calendar.alarms.show", true)) {
+ return;
+ }
+
+ let calAlarmWindow = peekAlarmWindow();
+ if (!calAlarmWindow && (!this.mWindowOpening || this.mWindowOpening.closed)) {
+ this.mWindowOpening = Services.ww.openWindow(
+ null,
+ "chrome://calendar/content/calendar-alarm-dialog.xhtml",
+ "_blank",
+ "chrome,dialog=yes,all,resizable",
+ this
+ );
+ }
+ if (!this.mWindowOpening) {
+ calAlarmWindow.addWidgetFor(aItem, aAlarm);
+ }
+ },
+
+ /**
+ * @see {calIAlarmServiceObserver}
+ * @param {calIItemBase} item - The item to notify about.
+ */
+ onNotification(item) {
+ // Don't notify about canceled events.
+ if (item.status == "CANCELLED") {
+ return;
+ }
+ // Don't notify if you declined this event invitation.
+ if (
+ (item instanceof lazy.CalEvent || item instanceof Ci.calIEvent) &&
+ item.calendar instanceof Ci.calISchedulingSupport &&
+ item.calendar.isInvitation(item) &&
+ item.calendar.getInvitedAttendee(item)?.participationStatus == "DECLINED"
+ ) {
+ return;
+ }
+
+ let alert = Cc["@mozilla.org/alert-notification;1"].createInstance(Ci.nsIAlertNotification);
+ let alertsService = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+ alert.init(
+ item.id, // name
+ "chrome://messenger/skin/icons/new-mail-alert.png",
+ item.title,
+ item.getProperty("description"),
+ true, // clickable
+ item.id // cookie
+ );
+ this._notifyingItems.set(item.id, item);
+ alertsService.showAlert(alert, this);
+ },
+
+ window_onLoad() {
+ let calAlarmWindow = this.mWindowOpening;
+ this.mWindowOpening = null;
+ if (this.mAlarms.length > 0) {
+ for (let [item, alarm] of this.mAlarms) {
+ calAlarmWindow.addWidgetFor(item, alarm);
+ }
+ } else {
+ // Uh oh, it seems the alarms were removed even before the window
+ // finished loading. Looks like we can close it again
+ calAlarmWindow.closeIfEmpty();
+ }
+ },
+
+ onRemoveAlarmsByItem(aItem) {
+ let calAlarmWindow = peekAlarmWindow();
+ this.mAlarms = this.mAlarms.filter(([thisItem, alarm]) => {
+ let ret = aItem.hashId != thisItem.hashId;
+ if (!ret && calAlarmWindow) {
+ // window is open
+ calAlarmWindow.removeWidgetFor(thisItem, alarm);
+ }
+ return ret;
+ });
+ },
+
+ onRemoveAlarmsByCalendar(calendar) {
+ let calAlarmWindow = peekAlarmWindow();
+ this.mAlarms = this.mAlarms.filter(([thisItem, alarm]) => {
+ let ret = calendar.id != thisItem.calendar.id;
+
+ if (!ret && calAlarmWindow) {
+ // window is open
+ calAlarmWindow.removeWidgetFor(thisItem, alarm);
+ }
+ return ret;
+ });
+ },
+
+ onAlarmsLoaded(aCalendar) {
+ // the alarm dialog won't close while alarms are loading, check again now
+ let calAlarmWindow = peekAlarmWindow();
+ if (calAlarmWindow && this.mAlarms.length == 0) {
+ calAlarmWindow.closeIfEmpty();
+ }
+ },
+};
diff --git a/comm/calendar/base/src/CalAlarmService.jsm b/comm/calendar/base/src/CalAlarmService.jsm
new file mode 100644
index 0000000000..916582a9af
--- /dev/null
+++ b/comm/calendar/base/src/CalAlarmService.jsm
@@ -0,0 +1,827 @@
+/* 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 = ["CalAlarmService"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(lazy, "CalDateTime", "resource:///modules/CalDateTime.jsm");
+
+var kHoursBetweenUpdates = 6;
+
+function nowUTC() {
+ return cal.dtz.jsDateToDateTime(new Date()).getInTimezone(cal.dtz.UTC);
+}
+
+function newTimerWithCallback(aCallback, aDelay, aRepeating) {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ timer.initWithCallback(
+ aCallback,
+ aDelay,
+ aRepeating ? timer.TYPE_REPEATING_PRECISE : timer.TYPE_ONE_SHOT
+ );
+ return timer;
+}
+
+/**
+ * Keeps track of seemingly immutable items with alarms that we can't dismiss.
+ * Some servers quietly discard our modifications to repeating events which will
+ * cause dismissed alarms to re-appear if we do not keep track.
+ *
+ * We track the items by their "hashId" property storing it in the calendar
+ * property "alarms.ignored".
+ */
+const IgnoredAlarmsStore = {
+ /**
+ * @type {number}
+ */
+ maxItemsPerCalendar: 1500,
+
+ _getCache(item) {
+ return item.calendar.getProperty("alarms.ignored")?.split(",") || [];
+ },
+
+ /**
+ * Adds an item to the store. No alarms will be created for this item again.
+ *
+ * @param {calIItemBase} item
+ */
+ add(item) {
+ let cache = this._getCache(item);
+ let id = item.parentItem.hashId;
+ if (!cache.includes(id)) {
+ if (cache.length >= this.maxItemsPerCalendar) {
+ cache[0] = id;
+ } else {
+ cache.push(id);
+ }
+ }
+ item.calendar.setProperty("alarms.ignored", cache.join(","));
+ },
+
+ /**
+ * Returns true if the item's hashId is in the store.
+ *
+ * @param {calIItemBase} item
+ * @returns {boolean}
+ */
+ has(item) {
+ return this._getCache(item).includes(item.parentItem.hashId);
+ },
+};
+
+function CalAlarmService() {
+ this.wrappedJSObject = this;
+
+ this.mLoadedCalendars = {};
+ this.mTimerMap = {};
+ this.mNotificationTimerMap = {};
+ this.mObservers = new cal.data.ListenerSet(Ci.calIAlarmServiceObserver);
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gNotificationsTimes",
+ "calendar.notifications.times",
+ "",
+ () => this.initAlarms(cal.manager.getCalendars())
+ );
+
+ this.calendarObserver = {
+ QueryInterface: ChromeUtils.generateQI(["calIObserver"]),
+ alarmService: this,
+
+ calendarsInBatch: new Set(),
+
+ // calIObserver:
+ onStartBatch(calendar) {
+ this.calendarsInBatch.add(calendar);
+ },
+ onEndBatch(calendar) {
+ this.calendarsInBatch.delete(calendar);
+ },
+ onLoad(calendar) {
+ // ignore any onLoad events until initial getItems() call of startup has finished:
+ if (calendar && this.alarmService.mLoadedCalendars[calendar.id]) {
+ // a refreshed calendar signals that it has been reloaded
+ // (and cannot notify detailed changes), thus reget all alarms of it:
+ this.alarmService.initAlarms([calendar]);
+ }
+ },
+
+ onAddItem(aItem) {
+ // If we're in a batch, ignore this notification. We're going to reload anyway.
+ if (!this.calendarsInBatch.has(aItem.calendar)) {
+ this.alarmService.addAlarmsForOccurrences(aItem);
+ }
+ },
+ onModifyItem(aNewItem, aOldItem) {
+ // If we're in a batch, ignore this notification. We're going to reload anyway.
+ if (this.calendarsInBatch.has(aNewItem.calendar)) {
+ return;
+ }
+
+ if (!aNewItem.recurrenceId) {
+ // deleting an occurrence currently calls modifyItem(newParent, *oldOccurrence*)
+ aOldItem = aOldItem.parentItem;
+ }
+
+ this.onDeleteItem(aOldItem);
+ this.onAddItem(aNewItem);
+ },
+ onDeleteItem(aDeletedItem) {
+ this.alarmService.removeAlarmsForOccurrences(aDeletedItem);
+ },
+ onError(aCalendar, aErrNo, aMessage) {},
+ onPropertyChanged(aCalendar, aName, aValue, aOldValue) {
+ switch (aName) {
+ case "suppressAlarms":
+ case "disabled":
+ case "notifications.times":
+ this.alarmService.initAlarms([aCalendar]);
+ break;
+ }
+ },
+ onPropertyDeleting(aCalendar, aName) {
+ this.onPropertyChanged(aCalendar, aName);
+ },
+ };
+
+ this.calendarManagerObserver = {
+ QueryInterface: ChromeUtils.generateQI(["calICalendarManagerObserver"]),
+ alarmService: this,
+
+ onCalendarRegistered(aCalendar) {
+ this.alarmService.observeCalendar(aCalendar);
+ // initial refresh of alarms for new calendar:
+ this.alarmService.initAlarms([aCalendar]);
+ },
+ onCalendarUnregistering(aCalendar) {
+ // XXX todo: we need to think about calendar unregistration;
+ // there may still be dangling items (-> alarm dialog),
+ // dismissing those alarms may write data...
+ this.alarmService.unobserveCalendar(aCalendar);
+ delete this.alarmService.mLoadedCalendars[aCalendar.id];
+ },
+ onCalendarDeleting(aCalendar) {
+ this.alarmService.unobserveCalendar(aCalendar);
+ delete this.alarmService.mLoadedCalendars[aCalendar.id];
+ },
+ };
+}
+
+var calAlarmServiceClassID = Components.ID("{7a9200dd-6a64-4fff-a798-c5802186e2cc}");
+var calAlarmServiceInterfaces = [Ci.calIAlarmService, Ci.nsIObserver];
+CalAlarmService.prototype = {
+ mRangeStart: null,
+ mRangeEnd: null,
+ mUpdateTimer: null,
+ mStarted: false,
+ mTimerMap: null,
+ mObservers: null,
+ mTimezone: null,
+
+ classID: calAlarmServiceClassID,
+ QueryInterface: cal.generateQI(["calIAlarmService", "nsIObserver"]),
+ classInfo: cal.generateCI({
+ classID: calAlarmServiceClassID,
+ contractID: "@mozilla.org/calendar/alarm-service;1",
+ classDescription: "Calendar Alarm Service",
+ interfaces: calAlarmServiceInterfaces,
+ flags: Ci.nsIClassInfo.SINGLETON,
+ }),
+
+ _logger: console.createInstance({
+ prefix: "calendar.alarms",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "calendar.alarms.loglevel",
+ }),
+
+ /**
+ * nsIObserver
+ */
+ observe(aSubject, aTopic, aData) {
+ // This will also be called on app-startup, but nothing is done yet, to
+ // prevent unwanted dialogs etc. See bug 325476 and 413296
+ if (aTopic == "profile-after-change" || aTopic == "wake_notification") {
+ this.shutdown();
+ this.startup();
+ }
+ if (aTopic == "xpcom-shutdown") {
+ this.shutdown();
+ }
+ },
+
+ /**
+ * calIAlarmService APIs
+ */
+ get timezone() {
+ // TODO Do we really need this? Do we ever set the timezone to something
+ // different than the default timezone?
+ return this.mTimezone || cal.dtz.defaultTimezone;
+ },
+
+ set timezone(aTimezone) {
+ this.mTimezone = aTimezone;
+ },
+
+ async snoozeAlarm(aItem, aAlarm, aDuration) {
+ // Right now we only support snoozing all alarms for the given item for
+ // aDuration.
+
+ // Make sure we're working with the parent, otherwise we'll accidentally
+ // create an exception
+ let newEvent = aItem.parentItem.clone();
+ let alarmTime = nowUTC();
+
+ // Set the last acknowledged time to now.
+ newEvent.alarmLastAck = alarmTime;
+
+ alarmTime = alarmTime.clone();
+ alarmTime.addDuration(aDuration);
+
+ let propName = "X-MOZ-SNOOZE-TIME";
+ if (aItem.parentItem != aItem) {
+ // This is the *really* hard case where we've snoozed a single
+ // instance of a recurring event. We need to not only know that
+ // there was a snooze, but also which occurrence was snoozed. Part
+ // of me just wants to create a local db of snoozes here...
+ propName = "X-MOZ-SNOOZE-TIME-" + aItem.recurrenceId.nativeTime;
+ }
+ newEvent.setProperty(propName, alarmTime.icalString);
+
+ // calling modifyItem will cause us to get the right callback
+ // and update the alarm properly
+ let modifiedItem = await newEvent.calendar.modifyItem(newEvent, aItem.parentItem);
+
+ if (modifiedItem.getProperty(propName) == alarmTime.icalString) {
+ return;
+ }
+
+ // The server did not persist our changes for some reason.
+ // Include the item in the ignored list so we skip displaying alarms for
+ // this item in the future.
+ IgnoredAlarmsStore.add(aItem);
+ },
+
+ async dismissAlarm(aItem, aAlarm) {
+ if (cal.acl.isCalendarWritable(aItem.calendar) && cal.acl.userCanModifyItem(aItem)) {
+ let now = nowUTC();
+ // We want the parent item, otherwise we're going to accidentally
+ // create an exception. We've relnoted (for 0.1) the slightly odd
+ // behavior this can cause if you move an event after dismissing an
+ // alarm
+ let oldParent = aItem.parentItem;
+ let newParent = oldParent.clone();
+ newParent.alarmLastAck = now;
+ // Make sure to clear out any snoozes that were here.
+ if (aItem.recurrenceId) {
+ newParent.deleteProperty("X-MOZ-SNOOZE-TIME-" + aItem.recurrenceId.nativeTime);
+ } else {
+ newParent.deleteProperty("X-MOZ-SNOOZE-TIME");
+ }
+
+ let modifiedItem = await newParent.calendar.modifyItem(newParent, oldParent);
+ if (modifiedItem.alarmLastAck && now.compare(modifiedItem.alarmLastAck) == 0) {
+ return;
+ }
+
+ // The server did not persist our changes for some reason.
+ // Include the item in the ignored list so we skip displaying alarms for
+ // this item in the future.
+ IgnoredAlarmsStore.add(aItem);
+ }
+ // if the calendar of the item is r/o, we simple remove the alarm
+ // from the list without modifying the item, so this works like
+ // effectively dismissing from a user's pov, since the alarm neither
+ // popups again in the current user session nor will be added after
+ // next restart, since it is missed then already
+ this.removeAlarmsForItem(aItem);
+ },
+
+ addObserver(aObserver) {
+ this.mObservers.add(aObserver);
+ },
+
+ removeObserver(aObserver) {
+ this.mObservers.delete(aObserver);
+ },
+
+ startup() {
+ if (this.mStarted) {
+ return;
+ }
+
+ Services.obs.addObserver(this, "profile-after-change");
+ Services.obs.addObserver(this, "xpcom-shutdown");
+ Services.obs.addObserver(this, "wake_notification");
+
+ // Make sure the alarm monitor is alive so it's observing the notification.
+ Cc["@mozilla.org/calendar/alarm-monitor;1"].getService(Ci.calIAlarmServiceObserver);
+ // Tell people that we're alive so they can start monitoring alarms.
+ Services.obs.notifyObservers(null, "alarm-service-startup");
+
+ cal.manager.addObserver(this.calendarManagerObserver);
+
+ for (let calendar of cal.manager.getCalendars()) {
+ this.observeCalendar(calendar);
+ }
+
+ /* set up a timer to update alarms every N hours */
+ let timerCallback = {
+ alarmService: this,
+ notify() {
+ let now = nowUTC();
+ let start;
+ if (this.alarmService.mRangeEnd) {
+ // This is a subsequent search, so we got all the past alarms before
+ start = this.alarmService.mRangeEnd.clone();
+ } else {
+ // This is our first search for alarms. We're going to look for
+ // alarms +/- 1 month from now. If someone sets an alarm more than
+ // a month ahead of an event, or doesn't start Thunderbird
+ // for a month, they'll miss some, but that's a slim chance
+ start = now.clone();
+ start.month -= Ci.calIAlarmService.MAX_SNOOZE_MONTHS;
+ this.alarmService.mRangeStart = start.clone();
+ }
+ let until = now.clone();
+ until.month += Ci.calIAlarmService.MAX_SNOOZE_MONTHS;
+
+ // We don't set timers for every future alarm, only those within 6 hours
+ let end = now.clone();
+ end.hour += kHoursBetweenUpdates;
+ this.alarmService.mRangeEnd = end.getInTimezone(cal.dtz.UTC);
+
+ this.alarmService.findAlarms(cal.manager.getCalendars(), start, until);
+ },
+ };
+ timerCallback.notify();
+
+ this.mUpdateTimer = newTimerWithCallback(timerCallback, kHoursBetweenUpdates * 3600000, true);
+
+ this.mStarted = true;
+ },
+
+ shutdown() {
+ if (!this.mStarted) {
+ return;
+ }
+
+ // Tell people that we're no longer running.
+ Services.obs.notifyObservers(null, "alarm-service-shutdown");
+
+ if (this.mUpdateTimer) {
+ this.mUpdateTimer.cancel();
+ this.mUpdateTimer = null;
+ }
+
+ cal.manager.removeObserver(this.calendarManagerObserver);
+
+ // Stop observing all calendars. This will also clear the timers.
+ for (let calendar of cal.manager.getCalendars()) {
+ this.unobserveCalendar(calendar);
+ }
+
+ this.mRangeEnd = null;
+
+ Services.obs.removeObserver(this, "profile-after-change");
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ Services.obs.removeObserver(this, "wake_notification");
+
+ this.mStarted = false;
+ },
+
+ observeCalendar(calendar) {
+ calendar.addObserver(this.calendarObserver);
+ },
+
+ unobserveCalendar(calendar) {
+ calendar.removeObserver(this.calendarObserver);
+ this.disposeCalendarTimers([calendar]);
+ this.mObservers.notify("onRemoveAlarmsByCalendar", [calendar]);
+ },
+
+ addAlarmsForItem(aItem) {
+ if ((aItem.isTodo() && aItem.isCompleted) || IgnoredAlarmsStore.has(aItem)) {
+ // If this is a task and it is completed or the id is in the ignored list,
+ // don't add the alarm.
+ return;
+ }
+
+ let showMissed = Services.prefs.getBoolPref("calendar.alarms.showmissed", true);
+
+ let alarms = aItem.getAlarms();
+ for (let alarm of alarms) {
+ let alarmDate = cal.alarms.calculateAlarmDate(aItem, alarm);
+
+ if (!alarmDate || alarm.action != "DISPLAY") {
+ // Only take care of DISPLAY alarms with an alarm date.
+ continue;
+ }
+
+ // Handle all day events. This is kinda weird, because they don't have
+ // a well defined startTime. We just consider the start/end to be
+ // midnight in the user's timezone.
+ if (alarmDate.isDate) {
+ alarmDate = alarmDate.getInTimezone(this.timezone);
+ alarmDate.isDate = false;
+ }
+ alarmDate = alarmDate.getInTimezone(cal.dtz.UTC);
+
+ // Check for snooze
+ let snoozeDate;
+ if (aItem.parentItem == aItem) {
+ snoozeDate = aItem.getProperty("X-MOZ-SNOOZE-TIME");
+ } else {
+ snoozeDate = aItem.parentItem.getProperty(
+ "X-MOZ-SNOOZE-TIME-" + aItem.recurrenceId.nativeTime
+ );
+ }
+
+ if (
+ snoozeDate &&
+ !(snoozeDate instanceof lazy.CalDateTime) &&
+ !(snoozeDate instanceof Ci.calIDateTime)
+ ) {
+ snoozeDate = cal.createDateTime(snoozeDate);
+ }
+
+ // an alarm can only be snoozed to a later time, if earlier it's from another alarm.
+ if (snoozeDate && snoozeDate.compare(alarmDate) > 0) {
+ // If the alarm was snoozed, the snooze time is more important.
+ alarmDate = snoozeDate;
+ }
+
+ let now = nowUTC();
+ if (alarmDate.timezone.isFloating) {
+ now = cal.dtz.now();
+ now.timezone = cal.dtz.floating;
+ }
+
+ if (alarmDate.compare(now) >= 0) {
+ // We assume that future alarms haven't been acknowledged
+ // Delay is in msec, so don't forget to multiply
+ let timeout = alarmDate.subtractDate(now).inSeconds * 1000;
+
+ // No sense in keeping an extra timeout for an alarm that's past
+ // our range.
+ let timeUntilRefresh = this.mRangeEnd.subtractDate(now).inSeconds * 1000;
+ if (timeUntilRefresh < timeout) {
+ continue;
+ }
+
+ this.addTimer(aItem, alarm, timeout);
+ } else if (
+ showMissed &&
+ cal.acl.isCalendarWritable(aItem.calendar) &&
+ cal.acl.userCanModifyItem(aItem)
+ ) {
+ // This alarm is in the past and the calendar is writable, so we
+ // could snooze or dismiss alarms. See if it has been previously
+ // ack'd.
+ let lastAck = aItem.parentItem.alarmLastAck;
+ if (lastAck && lastAck.compare(alarmDate) >= 0) {
+ // The alarm was previously dismissed or snoozed, no further
+ // action required.
+ continue;
+ } else {
+ // The alarm was not snoozed or dismissed, fire it now.
+ this.alarmFired(aItem, alarm);
+ }
+ }
+ }
+
+ this.addNotificationForItem(aItem);
+ },
+
+ removeAlarmsForItem(aItem) {
+ // make sure already fired alarms are purged out of the alarm window:
+ this.mObservers.notify("onRemoveAlarmsByItem", [aItem]);
+ // Purge alarms specifically for this item (i.e exception)
+ for (let alarm of aItem.getAlarms()) {
+ this.removeTimer(aItem, alarm);
+ }
+
+ this.removeNotificationForItem(aItem);
+ },
+
+ /**
+ * Get the timeouts before notifications are fired for an item.
+ *
+ * @param {calIItemBase} item - A calendar item instance.
+ * @returns {number[]} Timeouts of notifications in milliseconds in ascending order.
+ */
+ calculateNotificationTimeouts(item) {
+ let now = nowUTC();
+ let until = now.clone();
+ until.month += 1;
+ // We only care about items no more than a month ahead.
+ if (!cal.item.checkIfInRange(item, now, until)) {
+ return [];
+ }
+ let startDate = item[cal.dtz.startDateProp(item)];
+ let endDate = item[cal.dtz.endDateProp(item)];
+ let timeouts = [];
+ // The calendar level notifications setting overrides the global setting.
+ let prefValue = (
+ item.calendar.getProperty("notifications.times") || this.gNotificationsTimes
+ ).split(",");
+ for (let entry of prefValue) {
+ entry = entry.trim();
+ if (!entry) {
+ continue;
+ }
+ let [tag, value] = entry.split(":");
+ if (!value) {
+ value = tag;
+ tag = "";
+ }
+ let duration;
+ try {
+ duration = cal.createDuration(value);
+ } catch (e) {
+ this._logger.error(`Failed to parse ${entry}`, e);
+ continue;
+ }
+ let fireDate;
+ if (tag == "END" && endDate) {
+ fireDate = endDate.clone();
+ } else if (startDate) {
+ fireDate = startDate.clone();
+ } else {
+ continue;
+ }
+ fireDate.addDuration(duration);
+ let timeout = fireDate.subtractDate(now).inSeconds * 1000;
+ if (timeout > 0) {
+ timeouts.push(timeout);
+ }
+ }
+ return timeouts.sort((x, y) => x - y);
+ },
+
+ /**
+ * Set up notification timers for an item.
+ *
+ * @param {calIItemBase} item - A calendar item instance.
+ */
+ addNotificationForItem(item) {
+ let alarmTimerCallback = {
+ notify: () => {
+ this.mObservers.notify("onNotification", [item]);
+ this.removeFiredNotificationTimer(item);
+ },
+ };
+ let timeouts = this.calculateNotificationTimeouts(item);
+ let timers = timeouts.map(timeout => newTimerWithCallback(alarmTimerCallback, timeout, false));
+
+ if (timers.length > 0) {
+ this._logger.debug(
+ `addNotificationForItem hashId=${item.hashId}: adding ${timers.length} timers, timeouts=${timeouts}`
+ );
+ this.mNotificationTimerMap[item.calendar.id] =
+ this.mNotificationTimerMap[item.calendar.id] || {};
+ this.mNotificationTimerMap[item.calendar.id][item.hashId] = timers;
+ }
+ },
+
+ /**
+ * Remove notification timers for an item.
+ *
+ * @param {calIItemBase} item - A calendar item instance.
+ */
+ removeNotificationForItem(item) {
+ if (
+ !this.mNotificationTimerMap[item.calendar.id] ||
+ !this.mNotificationTimerMap[item.calendar.id][item.hashId]
+ ) {
+ return;
+ }
+
+ for (let timer of this.mNotificationTimerMap[item.calendar.id][item.hashId]) {
+ timer.cancel();
+ }
+
+ delete this.mNotificationTimerMap[item.calendar.id][item.hashId];
+
+ // If the calendar map is empty, remove it from the timer map
+ if (Object.keys(this.mNotificationTimerMap[item.calendar.id]).length == 0) {
+ delete this.mNotificationTimerMap[item.calendar.id];
+ }
+ },
+
+ /**
+ * Remove the first notification timers for an item to release some memory.
+ *
+ * @param {calIItemBase} item - A calendar item instance.
+ */
+ removeFiredNotificationTimer(item) {
+ // The first timer is fired first.
+ let removed = this.mNotificationTimerMap[item.calendar.id][item.hashId].shift();
+
+ let remainingTimersCount = this.mNotificationTimerMap[item.calendar.id][item.hashId].length;
+ this._logger.debug(
+ `removeFiredNotificationTimer hashId=${item.hashId}: removed=${removed.delay}, remaining ${remainingTimersCount} timers`
+ );
+ if (remainingTimersCount == 0) {
+ delete this.mNotificationTimerMap[item.calendar.id][item.hashId];
+ }
+
+ // If the calendar map is empty, remove it from the timer map
+ if (Object.keys(this.mNotificationTimerMap[item.calendar.id]).length == 0) {
+ delete this.mNotificationTimerMap[item.calendar.id];
+ }
+ },
+
+ getOccurrencesInRange(aItem) {
+ // We search 1 month in each direction for alarms. Therefore,
+ // we need occurrences between initial start date and 1 month from now
+ let until = nowUTC();
+ until.month += 1;
+
+ if (aItem && aItem.recurrenceInfo) {
+ return aItem.recurrenceInfo.getOccurrences(this.mRangeStart, until, 0);
+ }
+ return cal.item.checkIfInRange(aItem, this.mRangeStart, until) ? [aItem] : [];
+ },
+
+ addAlarmsForOccurrences(aParentItem) {
+ let occs = this.getOccurrencesInRange(aParentItem);
+
+ // Add an alarm for each occurrence
+ occs.forEach(this.addAlarmsForItem, this);
+ },
+
+ removeAlarmsForOccurrences(aParentItem) {
+ let occs = this.getOccurrencesInRange(aParentItem);
+
+ // Remove alarm for each occurrence
+ occs.forEach(this.removeAlarmsForItem, this);
+ },
+
+ addTimer(aItem, aAlarm, aTimeout) {
+ this.mTimerMap[aItem.calendar.id] = this.mTimerMap[aItem.calendar.id] || {};
+ this.mTimerMap[aItem.calendar.id][aItem.hashId] =
+ this.mTimerMap[aItem.calendar.id][aItem.hashId] || {};
+
+ let self = this;
+ let alarmTimerCallback = {
+ notify() {
+ self.alarmFired(aItem, aAlarm);
+ },
+ };
+
+ let timer = newTimerWithCallback(alarmTimerCallback, aTimeout, false);
+ this.mTimerMap[aItem.calendar.id][aItem.hashId][aAlarm.icalString] = timer;
+ },
+
+ removeTimer(aItem, aAlarm) {
+ /* Is the calendar in the timer map */
+ if (
+ aItem.calendar.id in this.mTimerMap &&
+ /* ...and is the item in the calendar map */
+ aItem.hashId in this.mTimerMap[aItem.calendar.id] &&
+ /* ...and is the alarm in the item map ? */
+ aAlarm.icalString in this.mTimerMap[aItem.calendar.id][aItem.hashId]
+ ) {
+ // First cancel the existing timer
+ let timer = this.mTimerMap[aItem.calendar.id][aItem.hashId][aAlarm.icalString];
+ timer.cancel();
+
+ // Remove the alarm from the item map
+ delete this.mTimerMap[aItem.calendar.id][aItem.hashId][aAlarm.icalString];
+
+ // If the item map is empty, remove it from the calendar map
+ if (this.mTimerMap[aItem.calendar.id][aItem.hashId].toSource() == "({})") {
+ delete this.mTimerMap[aItem.calendar.id][aItem.hashId];
+ }
+
+ // If the calendar map is empty, remove it from the timer map
+ if (this.mTimerMap[aItem.calendar.id].toSource() == "({})") {
+ delete this.mTimerMap[aItem.calendar.id];
+ }
+ }
+ },
+
+ disposeCalendarTimers(aCalendars) {
+ for (let calendar of aCalendars) {
+ if (calendar.id in this.mTimerMap) {
+ for (let hashId in this.mTimerMap[calendar.id]) {
+ let itemTimerMap = this.mTimerMap[calendar.id][hashId];
+ for (let icalString in itemTimerMap) {
+ let timer = itemTimerMap[icalString];
+ timer.cancel();
+ }
+ }
+ delete this.mTimerMap[calendar.id];
+ }
+ if (calendar.id in this.mNotificationTimerMap) {
+ for (let timers of Object.values(this.mNotificationTimerMap[calendar.id])) {
+ for (let timer of timers) {
+ timer.cancel();
+ }
+ }
+ delete this.mNotificationTimerMap[calendar.id];
+ }
+ }
+ },
+
+ async findAlarms(aCalendars, aStart, aUntil) {
+ const calICalendar = Ci.calICalendar;
+ let filter =
+ calICalendar.ITEM_FILTER_COMPLETED_ALL |
+ calICalendar.ITEM_FILTER_CLASS_OCCURRENCES |
+ calICalendar.ITEM_FILTER_TYPE_ALL;
+
+ await Promise.all(
+ aCalendars.map(async calendar => {
+ if (calendar.getProperty("suppressAlarms") && calendar.getProperty("disabled")) {
+ this.mLoadedCalendars[calendar.id] = true;
+ this.mObservers.notify("onAlarmsLoaded", [calendar]);
+ return;
+ }
+
+ // Assuming that suppressAlarms does not change anymore until next refresh.
+ this.mLoadedCalendars[calendar.id] = false;
+
+ for await (let items of cal.iterate.streamValues(
+ calendar.getItems(filter, 0, aStart, aUntil)
+ )) {
+ await new Promise((resolve, reject) => {
+ cal.iterate.forEach(
+ items,
+ item => {
+ try {
+ this.removeAlarmsForItem(item);
+ this.addAlarmsForItem(item);
+ } catch (e) {
+ console.error("Promise was rejected: " + e);
+ this.mLoadedCalendars[calendar.id] = true;
+ this.mObservers.notify("onAlarmsLoaded", [calendar]);
+ reject(e);
+ }
+ },
+ () => {
+ resolve();
+ }
+ );
+ });
+ }
+
+ // The calendar has been loaded, so until now, onLoad events can be ignored.
+ this.mLoadedCalendars[calendar.id] = true;
+
+ // Notify observers that the alarms for the calendar have been loaded.
+ this.mObservers.notify("onAlarmsLoaded", [calendar]);
+ })
+ );
+ },
+
+ initAlarms(aCalendars) {
+ // Purge out all alarm timers belonging to the refreshed/loaded calendars
+ this.disposeCalendarTimers(aCalendars);
+
+ // Purge out all alarms from dialog belonging to the refreshed/loaded calendars
+ for (let calendar of aCalendars) {
+ this.mLoadedCalendars[calendar.id] = false;
+ this.mObservers.notify("onRemoveAlarmsByCalendar", [calendar]);
+ }
+
+ // Total refresh similar to startup. We're going to look for
+ // alarms +/- 1 month from now. If someone sets an alarm more than
+ // a month ahead of an event, or doesn't start Thunderbird
+ // for a month, they'll miss some, but that's a slim chance
+ let start = nowUTC();
+ let until = start.clone();
+ start.month -= Ci.calIAlarmService.MAX_SNOOZE_MONTHS;
+ until.month += Ci.calIAlarmService.MAX_SNOOZE_MONTHS;
+ this.findAlarms(aCalendars, start, until);
+ },
+
+ alarmFired(aItem, aAlarm) {
+ if (
+ !aItem.calendar.getProperty("suppressAlarms") &&
+ !aItem.calendar.getProperty("disabled") &&
+ aItem.getProperty("STATUS") != "CANCELLED"
+ ) {
+ this.mObservers.notify("onAlarm", [aItem, aAlarm]);
+ }
+ },
+
+ get isLoading() {
+ for (let calId in this.mLoadedCalendars) {
+ // we need to exclude calendars which failed to load explicitly to
+ // prevent the alaram dialog to stay opened after dismissing all
+ // alarms if there is a network calendar that failed to load
+ let currentStatus = cal.manager.getCalendarById(calId).getProperty("currentStatus");
+ if (!this.mLoadedCalendars[calId] && Components.isSuccessCode(currentStatus)) {
+ return true;
+ }
+ }
+ return false;
+ },
+};
diff --git a/comm/calendar/base/src/CalAttachment.jsm b/comm/calendar/base/src/CalAttachment.jsm
new file mode 100644
index 0000000000..df21188c8e
--- /dev/null
+++ b/comm/calendar/base/src/CalAttachment.jsm
@@ -0,0 +1,169 @@
+/* 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 = ["CalAttachment"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+/**
+ * Constructor for `calIAttachment` objects.
+ *
+ * @class
+ * @implements {calIAttachment}
+ * @param {string} [icalString] - Optional iCal string for initializing existing attachments.
+ */
+function CalAttachment(icalString) {
+ this.wrappedJSObject = this;
+ this.mProperties = new Map();
+ if (icalString) {
+ this.icalString = icalString;
+ }
+}
+
+CalAttachment.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIAttachment"]),
+ classID: Components.ID("{5f76b352-ab75-4c2b-82c9-9206dbbf8571}"),
+
+ mData: null,
+ mHashId: null,
+
+ get hashId() {
+ if (!this.mHashId) {
+ let cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+ let data = new TextEncoder().encode(this.rawData);
+ cryptoHash.init(cryptoHash.MD5);
+ cryptoHash.update(data, data.length);
+ this.mHashId = cryptoHash.finish(true);
+ }
+ return this.mHashId;
+ },
+
+ /**
+ * calIAttachment
+ */
+
+ get uri() {
+ let uri = null;
+ if (this.getParameter("VALUE") != "BINARY") {
+ // If this is not binary data, its likely an uri. Attempt to convert
+ // and throw otherwise.
+ try {
+ uri = Services.io.newURI(this.mData);
+ } catch (e) {
+ // Its possible that the uri contains malformed data. Often
+ // callers don't expect an exception here, so we just catch
+ // it and return null.
+ }
+ }
+
+ return uri;
+ },
+ set uri(aUri) {
+ // An uri is the default format, remove any value type parameters
+ this.deleteParameter("VALUE");
+ this.setData(aUri.spec);
+ },
+
+ get rawData() {
+ return this.mData;
+ },
+ set rawData(aData) {
+ // Setting the raw data lets us assume this is binary data. Make sure
+ // the value parameter is set
+ this.setParameter("VALUE", "BINARY");
+ this.setData(aData);
+ },
+
+ get formatType() {
+ return this.getParameter("FMTTYPE");
+ },
+ set formatType(aType) {
+ this.setParameter("FMTTYPE", aType);
+ },
+
+ get encoding() {
+ return this.getParameter("ENCODING");
+ },
+ set encoding(aValue) {
+ this.setParameter("ENCODING", aValue);
+ },
+
+ get icalProperty() {
+ let icalatt = cal.icsService.createIcalProperty("ATTACH");
+
+ for (let [key, value] of this.mProperties.entries()) {
+ try {
+ icalatt.setParameter(key, value);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid attachment parameter value " + key + "=" + value);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ if (this.mData) {
+ icalatt.value = this.mData;
+ }
+ return icalatt;
+ },
+
+ set icalProperty(attProp) {
+ // Reset the property bag for the parameters, it will be re-initialized
+ // from the ical property.
+ this.mProperties = new Map();
+ this.setData(attProp.value);
+
+ for (let [name, value] of cal.iterate.icalParameter(attProp)) {
+ this.setParameter(name, value);
+ }
+ },
+
+ get icalString() {
+ let comp = this.icalProperty;
+ return comp ? comp.icalString : "";
+ },
+ set icalString(val) {
+ let prop = cal.icsService.createIcalPropertyFromString(val);
+ if (prop.propertyName != "ATTACH") {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ this.icalProperty = prop;
+ },
+
+ getParameter(aName) {
+ return this.mProperties.get(aName);
+ },
+
+ setParameter(aName, aValue) {
+ if (aValue || aValue === 0) {
+ return this.mProperties.set(aName, aValue);
+ }
+ return this.mProperties.delete(aName);
+ },
+
+ deleteParameter(aName) {
+ this.mProperties.delete(aName);
+ },
+
+ clone() {
+ let newAttachment = new CalAttachment();
+ newAttachment.mData = this.mData;
+ newAttachment.mHashId = this.mHashId;
+ for (let [name, value] of this.mProperties.entries()) {
+ newAttachment.mProperties.set(name, value);
+ }
+ return newAttachment;
+ },
+
+ setData(aData) {
+ // Sets the data and invalidates the hash so it will be recalculated
+ this.mHashId = null;
+ this.mData = aData;
+ return this.mData;
+ },
+};
diff --git a/comm/calendar/base/src/CalAttendee.jsm b/comm/calendar/base/src/CalAttendee.jsm
new file mode 100644
index 0000000000..5edceabded
--- /dev/null
+++ b/comm/calendar/base/src/CalAttendee.jsm
@@ -0,0 +1,212 @@
+/* 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/. */
+
+/* import-globals-from calItemBase.js */
+
+var EXPORTED_SYMBOLS = ["CalAttendee"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+Services.scriptloader.loadSubScript("resource:///components/calItemBase.js");
+
+/**
+ * Constructor for `calIAttendee` objects.
+ *
+ * @class
+ * @implements {calIAttendee}
+ * @param {string} [icalString] - Optional iCal string for initializing existing attendees.
+ */
+function CalAttendee(icalString) {
+ this.wrappedJSObject = this;
+ this.mProperties = new Map();
+ if (icalString) {
+ this.icalString = icalString;
+ }
+}
+
+CalAttendee.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIAttendee"]),
+ classID: Components.ID("{5c8dcaa3-170c-4a73-8142-d531156f664d}"),
+
+ mImmutable: false,
+ get isMutable() {
+ return !this.mImmutable;
+ },
+
+ modify() {
+ if (this.mImmutable) {
+ throw Components.Exception("", Cr.NS_ERROR_OBJECT_IS_IMMUTABLE);
+ }
+ },
+
+ makeImmutable() {
+ this.mImmutable = true;
+ },
+
+ clone() {
+ let a = new CalAttendee();
+
+ if (this.mIsOrganizer) {
+ a.isOrganizer = true;
+ }
+
+ const allProps = ["id", "commonName", "rsvp", "role", "participationStatus", "userType"];
+ for (let prop of allProps) {
+ a[prop] = this[prop];
+ }
+
+ for (let [key, value] of this.mProperties.entries()) {
+ a.setProperty(key, value);
+ }
+
+ return a;
+ },
+ // XXX enforce legal values for our properties;
+
+ icalAttendeePropMap: [
+ { cal: "rsvp", ics: "RSVP" },
+ { cal: "commonName", ics: "CN" },
+ { cal: "participationStatus", ics: "PARTSTAT" },
+ { cal: "userType", ics: "CUTYPE" },
+ { cal: "role", ics: "ROLE" },
+ ],
+
+ mIsOrganizer: false,
+ get isOrganizer() {
+ return this.mIsOrganizer;
+ },
+ set isOrganizer(bool) {
+ this.mIsOrganizer = bool;
+ },
+
+ // icalatt is a calIcalProperty of type attendee
+ set icalProperty(icalatt) {
+ this.modify();
+ this.id = icalatt.valueAsIcalString;
+ this.mIsOrganizer = icalatt.propertyName == "ORGANIZER";
+
+ let promotedProps = {};
+ for (let prop of this.icalAttendeePropMap) {
+ this[prop.cal] = icalatt.getParameter(prop.ics);
+ // Don't copy these to the property bag.
+ promotedProps[prop.ics] = true;
+ }
+
+ // Reset the property bag for the parameters, it will be re-initialized
+ // from the ical property.
+ this.mProperties = new Map();
+
+ for (let [name, value] of cal.iterate.icalParameter(icalatt)) {
+ if (!promotedProps[name]) {
+ this.setProperty(name, value);
+ }
+ }
+ },
+
+ get icalProperty() {
+ let icalatt;
+ if (this.mIsOrganizer) {
+ icalatt = cal.icsService.createIcalProperty("ORGANIZER");
+ } else {
+ icalatt = cal.icsService.createIcalProperty("ATTENDEE");
+ }
+
+ if (!this.id) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ icalatt.valueAsIcalString = this.id;
+ for (let i = 0; i < this.icalAttendeePropMap.length; i++) {
+ let prop = this.icalAttendeePropMap[i];
+ if (this[prop.cal]) {
+ try {
+ icalatt.setParameter(prop.ics, this[prop.cal]);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid attendee parameter value " + prop.ics + "=" + this[prop.cal]);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+ for (let [key, value] of this.mProperties.entries()) {
+ try {
+ icalatt.setParameter(key, value);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid attendee parameter value " + key + "=" + value);
+ } else {
+ throw e;
+ }
+ }
+ }
+ return icalatt;
+ },
+
+ get icalString() {
+ let comp = this.icalProperty;
+ return comp ? comp.icalString : "";
+ },
+ set icalString(val) {
+ let prop = cal.icsService.createIcalPropertyFromString(val);
+ if (prop.propertyName != "ORGANIZER" && prop.propertyName != "ATTENDEE") {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ this.icalProperty = prop;
+ },
+
+ get properties() {
+ return [...this.mProperties.entries()];
+ },
+
+ // The has/get/set/deleteProperty methods are case-insensitive.
+ getProperty(aName) {
+ return this.mProperties.get(aName.toUpperCase());
+ },
+ setProperty(aName, aValue) {
+ this.modify();
+ if (aValue || !isNaN(parseInt(aValue, 10))) {
+ this.mProperties.set(aName.toUpperCase(), aValue);
+ } else {
+ this.mProperties.delete(aName.toUpperCase());
+ }
+ },
+ deleteProperty(aName) {
+ this.modify();
+ this.mProperties.delete(aName.toUpperCase());
+ },
+
+ mId: null,
+ get id() {
+ return this.mId;
+ },
+ set id(aId) {
+ this.modify();
+ // RFC 1738 para 2.1 says we should be using lowercase mailto: urls
+ // we enforce prepending the mailto prefix for email type ids as migration code bug 1199942
+ this.mId = aId ? cal.email.prependMailTo(aId) : null;
+ },
+
+ toString() {
+ const emailRE = new RegExp("^mailto:", "i");
+ let stringRep = (this.id || "").replace(emailRE, "");
+ let commonName = this.commonName;
+
+ if (commonName) {
+ stringRep = commonName + " <" + stringRep + ">";
+ }
+
+ return stringRep;
+ },
+};
+
+makeMemberAttr(CalAttendee, "mCommonName", "commonName", null);
+makeMemberAttr(CalAttendee, "mRsvp", "rsvp", null);
+makeMemberAttr(CalAttendee, "mRole", "role", null);
+makeMemberAttr(CalAttendee, "mParticipationStatus", "participationStatus", "NEEDS-ACTION");
+makeMemberAttr(CalAttendee, "mUserType", "userType", "INDIVIDUAL");
diff --git a/comm/calendar/base/src/CalCalendarManager.jsm b/comm/calendar/base/src/CalCalendarManager.jsm
new file mode 100644
index 0000000000..117737a635
--- /dev/null
+++ b/comm/calendar/base/src/CalCalendarManager.jsm
@@ -0,0 +1,1076 @@
+/* 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/. */
+
+/* import-globals-from calCachedCalendar.js */
+
+var EXPORTED_SYMBOLS = ["CalCalendarManager"];
+
+const { AddonManager } = ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs");
+const { Preferences } = ChromeUtils.importESModule("resource://gre/modules/Preferences.sys.mjs");
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+const { calCachedCalendar } = ChromeUtils.import("resource:///components/calCachedCalendar.js");
+const { setTimeout } = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs");
+
+var REGISTRY_BRANCH = "calendar.registry.";
+var MAX_INT = Math.pow(2, 31) - 1;
+var MIN_INT = -MAX_INT;
+
+function CalCalendarManager() {
+ this.wrappedJSObject = this;
+ this.mObservers = new cal.data.ListenerSet(Ci.calICalendarManagerObserver);
+ this.mCalendarObservers = new cal.data.ListenerSet(Ci.calIObserver);
+
+ this.providerImplementations = {};
+}
+
+var calCalendarManagerClassID = Components.ID("{f42585e7-e736-4600-985d-9624c1c51992}");
+var calCalendarManagerInterfaces = [Ci.calICalendarManager, Ci.calIStartupService, Ci.nsIObserver];
+CalCalendarManager.prototype = {
+ classID: calCalendarManagerClassID,
+ QueryInterface: cal.generateQI(["calICalendarManager", "calIStartupService", "nsIObserver"]),
+ classInfo: cal.generateCI({
+ classID: calCalendarManagerClassID,
+ contractID: "@mozilla.org/calendar/manager;1",
+ classDescription: "Calendar Manager",
+ interfaces: calCalendarManagerInterfaces,
+ flags: Ci.nsIClassInfo.SINGLETON,
+ }),
+
+ get networkCalendarCount() {
+ return this.mNetworkCalendarCount;
+ },
+ get readOnlyCalendarCount() {
+ return this.mReadonlyCalendarCount;
+ },
+ get calendarCount() {
+ return this.mCalendarCount;
+ },
+
+ // calIStartupService:
+ startup(aCompleteListener) {
+ AddonManager.addAddonListener(gCalendarManagerAddonListener);
+ this.mCache = null;
+ this.mCalObservers = null;
+ this.mRefreshTimer = {};
+ this.setupOfflineObservers();
+ this.mNetworkCalendarCount = 0;
+ this.mReadonlyCalendarCount = 0;
+ this.mCalendarCount = 0;
+
+ // We only add the observer if the pref is set and only check for the
+ // pref on startup to avoid checking for every http request
+ if (Services.prefs.getBoolPref("calendar.network.multirealm", false)) {
+ Services.obs.addObserver(this, "http-on-examine-response");
+ }
+
+ aCompleteListener.onResult(null, Cr.NS_OK);
+ },
+
+ shutdown(aCompleteListener) {
+ for (let id in this.mCache) {
+ let calendar = this.mCache[id];
+ calendar.removeObserver(this.mCalObservers[calendar.id]);
+ }
+
+ this.cleanupOfflineObservers();
+
+ AddonManager.removeAddonListener(gCalendarManagerAddonListener);
+
+ // Remove the observer if the pref is set. This might fail when the
+ // user flips the pref, but we assume he is going to restart anyway
+ // afterwards.
+ if (Services.prefs.getBoolPref("calendar.network.multirealm", false)) {
+ Services.obs.removeObserver(this, "http-on-examine-response");
+ }
+
+ aCompleteListener.onResult(null, Cr.NS_OK);
+ },
+
+ setupOfflineObservers() {
+ Services.obs.addObserver(this, "network:offline-status-changed");
+ },
+
+ cleanupOfflineObservers() {
+ Services.obs.removeObserver(this, "network:offline-status-changed");
+ },
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "timer-callback": {
+ // Refresh all the calendars that can be refreshed.
+ for (let calendar of this.getCalendars()) {
+ maybeRefreshCalendar(calendar);
+ }
+ break;
+ }
+ case "network:offline-status-changed": {
+ for (let id in this.mCache) {
+ let calendar = this.mCache[id];
+ if (calendar instanceof calCachedCalendar) {
+ calendar.onOfflineStatusChanged(aData == "offline");
+ }
+ }
+ break;
+ }
+ case "http-on-examine-response": {
+ try {
+ let channel = aSubject.QueryInterface(Ci.nsIHttpChannel);
+ if (channel.notificationCallbacks) {
+ // We use the notification callbacks to get the calendar interface, which likely works
+ // for our requests since getInterface is called from the calendar provider context.
+ let authHeader = channel.getResponseHeader("WWW-Authenticate");
+ let calendar = channel.notificationCallbacks.getInterface(Ci.calICalendar);
+ if (calendar && !calendar.getProperty("capabilities.realmrewrite.disabled")) {
+ // The provider may choose to explicitly disable the rewriting, for example if all
+ // calendars on a domain have the same credentials
+ let escapedName = calendar.name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
+ authHeader = appendToRealm(authHeader, "(" + escapedName + ")");
+ channel.setResponseHeader("WWW-Authenticate", authHeader, false);
+ }
+ }
+ } catch (e) {
+ if (e.result != Cr.NS_NOINTERFACE && e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw e;
+ }
+ // Possible reasons we got here:
+ // - Its not a http channel (wtf? Oh well)
+ // - The owner is not a calICalendar (looks like its not our deal)
+ // - The WWW-Authenticate header is missing (that's ok)
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * calICalendarManager interface
+ */
+ createCalendar(type, uri) {
+ try {
+ let calendar;
+ if (Cc["@mozilla.org/calendar/calendar;1?type=" + type]) {
+ calendar = Cc["@mozilla.org/calendar/calendar;1?type=" + type].createInstance(
+ Ci.calICalendar
+ );
+ } else if (this.providerImplementations[type]) {
+ let CalendarProvider = this.providerImplementations[type];
+ calendar = new CalendarProvider();
+ if (calendar.QueryInterface) {
+ calendar = calendar.QueryInterface(Ci.calICalendar);
+ }
+ } else {
+ // Don't notify the user with an extra dialog if the provider interface is missing.
+ return null;
+ }
+
+ calendar.uri = uri;
+ return calendar;
+ } catch (ex) {
+ let rc = ex;
+ if (ex instanceof Ci.nsIException) {
+ rc = ex.result;
+ }
+
+ let uiMessage = cal.l10n.getCalString("unableToCreateProvider", [uri.spec]);
+
+ // Log the original exception via error console to provide more debug info
+ cal.ERROR(ex);
+
+ // Log the possibly translated message via the UI.
+ let paramBlock = Cc["@mozilla.org/embedcomp/dialogparam;1"].createInstance(
+ Ci.nsIDialogParamBlock
+ );
+ paramBlock.SetNumberStrings(3);
+ paramBlock.SetString(0, uiMessage);
+ paramBlock.SetString(1, "0x" + rc.toString(0x10));
+ paramBlock.SetString(2, ex);
+ Services.ww.openWindow(
+ null,
+ "chrome://calendar/content/calendar-error-prompt.xhtml",
+ "_blank",
+ "chrome,dialog=yes,alwaysRaised=yes",
+ paramBlock
+ );
+ return null;
+ }
+ },
+
+ /**
+ * Creates a calendar and takes care of initial setup, including enabled/disabled properties and
+ * cached calendars. If the provider doesn't exist, returns a dummy calendar that is
+ * force-disabled.
+ *
+ * @param {string} id - The calendar id.
+ * @param {string} ctype - The calendar type. See {@link calICalendar#type}.
+ * @param {string} uri - The calendar uri.
+ * @returns {calICalendar} The initialized calendar or dummy calendar.
+ */
+ initializeCalendar(id, ctype, uri) {
+ let calendar = this.createCalendar(ctype, uri);
+ if (calendar) {
+ calendar.id = id;
+ if (calendar.getProperty("auto-enabled")) {
+ calendar.deleteProperty("disabled");
+ calendar.deleteProperty("auto-enabled");
+ }
+
+ calendar = maybeWrapCachedCalendar(calendar);
+ } else {
+ // Create dummy calendar that stays disabled for this run.
+ calendar = new calDummyCalendar(ctype);
+ calendar.id = id;
+ calendar.uri = uri;
+ // Try to enable on next startup if calendar has been enabled.
+ if (!calendar.getProperty("disabled")) {
+ calendar.setProperty("auto-enabled", true);
+ }
+ calendar.setProperty("disabled", true);
+ }
+
+ return calendar;
+ },
+
+ /**
+ * Update calendar registrations for the given type. If the provider is missing then the calendars
+ * are replaced with a dummy calendar, and vice versa.
+ *
+ * @param {string} type - The calendar type to update. See {@link calICalendar#type}.
+ * @param {boolean} [clearCache=false] - If true, the calendar cache is also cleared.
+ */
+ updateDummyCalendarRegistration(type, clearCache = false) {
+ let hasImplementation = !!this.providerImplementations[type];
+
+ let calendars = Object.values(this.mCache).filter(calendar => {
+ // Calendars backed by providers despite missing provider implementation, or dummy calendars
+ // despite having a provider implementation.
+ let isDummyCalendar = calendar instanceof calDummyCalendar;
+ return calendar.type == type && hasImplementation == isDummyCalendar;
+ });
+ this.updateCalendarRegistration(calendars, clearCache);
+ },
+
+ /**
+ * Update the calendar registrations for the given set of calendars. This essentially unregisters
+ * the calendar, then sets it up again using id, type and uri. This is similar to what happens on
+ * startup.
+ *
+ * @param {calICalendar[]} calendars - The calendars to update.
+ * @param {boolean} [clearCache=false] - If true, the calendar cache is also cleared.
+ */
+ updateCalendarRegistration(calendars, clearCache = false) {
+ let sortOrderPref = Services.prefs.getStringPref("calendar.list.sortOrder", "").split(" ");
+ let sortOrder = {};
+ for (let i = 0; i < sortOrderPref.length; i++) {
+ sortOrder[sortOrderPref[i]] = i;
+ }
+
+ let needsRefresh = [];
+ for (let calendar of calendars) {
+ try {
+ this.notifyObservers("onCalendarUnregistering", [calendar]);
+ this.unsetupCalendar(calendar, clearCache);
+
+ let replacement = this.initializeCalendar(calendar.id, calendar.type, calendar.uri);
+ replacement.setProperty("initialSortOrderPos", sortOrder[calendar.id]);
+
+ this.setupCalendar(replacement);
+ needsRefresh.push(replacement);
+ } catch (e) {
+ cal.ERROR(
+ `Can't create calendar for ${calendar.id} (${calendar.type}, ${calendar.uri.spec}): ${e}`
+ );
+ }
+ }
+
+ // Do this in a second pass so that all provider calendars are available.
+ for (let calendar of needsRefresh) {
+ maybeRefreshCalendar(calendar);
+ this.notifyObservers("onCalendarRegistered", [calendar]);
+ }
+ },
+
+ /**
+ * Register a calendar provider with the given JavaScript implementation.
+ *
+ * @param {string} type - The calendar type string, see {@link calICalendar#type}.
+ * @param {object} impl - The class that implements calICalendar.
+ */
+ registerCalendarProvider(type, impl) {
+ this.assureCache();
+
+ cal.ASSERT(
+ !this.providerImplementations.hasOwnProperty(type),
+ "[CalCalendarManager::registerCalendarProvider] provider already exists",
+ true
+ );
+
+ this.providerImplementations[type] = impl;
+ this.updateDummyCalendarRegistration(type);
+ },
+
+ /**
+ * Unregister a calendar provider by type. Already registered calendars will be replaced by a
+ * dummy calendar that is force-disabled.
+ *
+ * @param {string} type - The calendar type string, see {@link calICalendar#type}.
+ * @param {boolean} temporary - If true, cached calendars will not be cleared.
+ */
+ unregisterCalendarProvider(type, temporary = false) {
+ cal.ASSERT(
+ this.providerImplementations.hasOwnProperty(type),
+ "[CalCalendarManager::unregisterCalendarProvider] provider doesn't exist or is builtin",
+ true
+ );
+ delete this.providerImplementations[type];
+ this.updateDummyCalendarRegistration(type, !temporary);
+ },
+
+ /**
+ * Checks if a calendar provider has been dynamically registered with the given type. This does
+ * not check for the built-in XPCOM providers.
+ *
+ * @param {string} type - The calendar type string, see {@link calICalendar#type}.
+ * @returns {boolean} True, if the calendar provider type is registered.
+ */
+ hasCalendarProvider(type) {
+ return !!this.providerImplementations[type];
+ },
+
+ registerCalendar(calendar) {
+ this.assureCache();
+
+ // If the calendar is already registered, bail out
+ cal.ASSERT(
+ !calendar.id || !(calendar.id in this.mCache),
+ "[CalCalendarManager::registerCalendar] calendar already registered!",
+ true
+ );
+
+ if (!calendar.id) {
+ calendar.id = cal.getUUID();
+ }
+
+ Services.prefs.setStringPref(getPrefBranchFor(calendar.id) + "type", calendar.type);
+ Services.prefs.setStringPref(getPrefBranchFor(calendar.id) + "uri", calendar.uri.spec);
+
+ calendar = maybeWrapCachedCalendar(calendar);
+
+ this.setupCalendar(calendar);
+ flushPrefs();
+
+ maybeRefreshCalendar(calendar);
+ this.notifyObservers("onCalendarRegistered", [calendar]);
+ },
+
+ /**
+ * Sets up a calendar, this is the initialization required during calendar registration. See
+ * {@link #unsetupCalendar} to revert these steps.
+ *
+ * @param {calICalendar} calendar - The calendar to set up.
+ */
+ setupCalendar(calendar) {
+ this.mCache[calendar.id] = calendar;
+
+ // Add an observer to track readonly-mode triggers
+ let newObserver = new calMgrCalendarObserver(calendar, this);
+ calendar.addObserver(newObserver);
+ this.mCalObservers[calendar.id] = newObserver;
+
+ // Set up statistics
+ if (calendar.getProperty("requiresNetwork") !== false) {
+ this.mNetworkCalendarCount++;
+ }
+ if (calendar.readOnly) {
+ this.mReadonlyCalendarCount++;
+ }
+ this.mCalendarCount++;
+
+ // Set up the refresh timer
+ this.setupRefreshTimer(calendar);
+ },
+
+ /**
+ * Reverts the calendar registration setup steps from {@link #setupCalendar}.
+ *
+ * @param {calICalendar} calendar - The calendar to undo setup for.
+ * @param {boolean} [clearCache=false] - If true, the cache is cleared for this calendar.
+ */
+ unsetupCalendar(calendar, clearCache = false) {
+ if (this.mCache) {
+ delete this.mCache[calendar.id];
+ }
+
+ if (clearCache && calendar.wrappedJSObject instanceof calCachedCalendar) {
+ calendar.wrappedJSObject.onCalendarUnregistering();
+ }
+
+ calendar.removeObserver(this.mCalObservers[calendar.id]);
+
+ if (calendar.readOnly) {
+ this.mReadonlyCalendarCount--;
+ }
+
+ if (calendar.getProperty("requiresNetwork") !== false) {
+ this.mNetworkCalendarCount--;
+ }
+ this.mCalendarCount--;
+
+ this.clearRefreshTimer(calendar);
+ },
+
+ setupRefreshTimer(aCalendar) {
+ // Add the refresh timer for this calendar
+ let refreshInterval = aCalendar.getProperty("refreshInterval");
+ if (refreshInterval === null) {
+ // Default to 30 minutes, in case the value is missing
+ refreshInterval = 30;
+ }
+
+ this.clearRefreshTimer(aCalendar);
+
+ if (refreshInterval > 0) {
+ this.mRefreshTimer[aCalendar.id] = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ this.mRefreshTimer[aCalendar.id].initWithCallback(
+ new timerCallback(aCalendar),
+ refreshInterval * 60000,
+ Ci.nsITimer.TYPE_REPEATING_SLACK
+ );
+ }
+ },
+
+ clearRefreshTimer(aCalendar) {
+ if (aCalendar.id in this.mRefreshTimer && this.mRefreshTimer[aCalendar.id]) {
+ this.mRefreshTimer[aCalendar.id].cancel();
+ delete this.mRefreshTimer[aCalendar.id];
+ }
+ },
+
+ unregisterCalendar(calendar) {
+ this.notifyObservers("onCalendarUnregistering", [calendar]);
+ this.unsetupCalendar(calendar, true);
+
+ deletePrefBranch(calendar.id);
+ flushPrefs();
+ },
+
+ removeCalendar(calendar, mode = 0) {
+ const cICM = Ci.calICalendarManager;
+
+ let removeModes = new Set(calendar.getProperty("capabilities.removeModes") || ["unsubscribe"]);
+ if (!removeModes.has("unsubscribe") && !removeModes.has("delete")) {
+ // Removing is not allowed
+ return;
+ }
+
+ if (mode & cICM.REMOVE_NO_UNREGISTER && this.mCache && calendar.id in this.mCache) {
+ throw new Components.Exception("Can't remove a registered calendar");
+ } else if (!(mode & cICM.REMOVE_NO_UNREGISTER)) {
+ this.unregisterCalendar(calendar);
+ }
+
+ // This observer notification needs to be fired for both unsubscribe
+ // and delete, we don't differ this at the moment.
+ this.notifyObservers("onCalendarDeleting", [calendar]);
+
+ // For deleting, we also call the deleteCalendar method from the provider.
+ if (removeModes.has("delete") && (mode & cICM.REMOVE_NO_DELETE) == 0) {
+ let wrappedCalendar = calendar.QueryInterface(Ci.calICalendarProvider);
+ wrappedCalendar.deleteCalendar(calendar, null);
+ }
+ },
+
+ getCalendarById(aId) {
+ if (aId in this.mCache) {
+ return this.mCache[aId];
+ }
+ return null;
+ },
+
+ getCalendars() {
+ this.assureCache();
+ let calendars = [];
+ for (let id in this.mCache) {
+ let calendar = this.mCache[id];
+ calendars.push(calendar);
+ }
+ return calendars;
+ },
+
+ /**
+ * Load calendars from the pref branch, if they haven't already been loaded. The calendar
+ * instances will end up in mCache and are refreshed when complete.
+ */
+ assureCache() {
+ if (this.mCache) {
+ return;
+ }
+
+ this.mCache = {};
+ this.mCalObservers = {};
+
+ let allCals = {};
+ for (let key of Services.prefs.getChildList(REGISTRY_BRANCH)) {
+ // merge down all keys
+ allCals[key.substring(0, key.indexOf(".", REGISTRY_BRANCH.length))] = true;
+ }
+
+ for (let calBranch in allCals) {
+ let id = calBranch.substring(REGISTRY_BRANCH.length);
+ let ctype = Services.prefs.getStringPref(calBranch + ".type", null);
+ let curi = Services.prefs.getStringPref(calBranch + ".uri", null);
+
+ try {
+ if (!ctype || !curi) {
+ // sanity check
+ deletePrefBranch(id);
+ continue;
+ }
+
+ let uri = Services.io.newURI(curi);
+ let calendar = this.initializeCalendar(id, ctype, uri);
+ this.setupCalendar(calendar);
+ } catch (exc) {
+ cal.ERROR(`Can't create calendar for ${id} (${ctype}, ${curi}): ${exc}`);
+ }
+ }
+
+ let shouldResyncGoogleCalDav = false;
+ if (!Services.prefs.prefHasUserValue("calendar.caldav.googleResync")) {
+ // Some users' calendars got into a bad state due to Google rate-limit
+ // problems so this code triggers a full resync.
+ shouldResyncGoogleCalDav = true;
+ }
+
+ // do refreshing in a second step, when *all* calendars are already available
+ // via getCalendars():
+ for (let calendar of Object.values(this.mCache)) {
+ let delay = 0;
+
+ // The special-casing of ICS here is a very ugly hack. We can delay most
+ // cached calendars without an issue, but the ICS implementation has two
+ // properties which make that dangerous in its case:
+ //
+ // 1) ICS files can only be written whole cloth. Since it's a plain file,
+ // we need to know the entire contents of what we want to write.
+ //
+ // 2) It is backed by a memory calendar which it regards as its source of
+ // truth, and the backing calendar is only populated on a refresh.
+ //
+ // The combination of these two means that any update to the ICS calendar
+ // before the memory calendar is populated will erase everything in the
+ // calendar (except potentially the added item if that's what we're
+ // doing). A 15 second window for data loss-inducing updates isn't huge,
+ // but it's more than we should bet on.
+ //
+ // Why not fix this a different way? Trying to populate the memory
+ // calendar outside of a refresh causes the caching calendar to get
+ // confused about event ownership and identity, leading to bogus observer
+ // notifications and potential duplication of events in some parts of the
+ // interface. Having the ICS calendar refresh itself internally can cause
+ // disabled calendars to behave improperly, since calendars don't actually
+ // enforce their own disablement and may not know if they're disabled
+ // until after we try to refresh. Having the ICS calendar ensure it has
+ // refreshed itself before trying to make updates would require a fair bit
+ // of refactoring in its processing queue and, while it should probably
+ // happen, fingers crossed we can rework the provider architecture to make
+ // many of these problems less of an issue first.
+ const canDelay = calendar.getProperty("cache.enabled") && calendar.type != "ics";
+
+ if (canDelay) {
+ // If the calendar is cached, we don't need to refresh it RIGHT NOW, so let's wait a
+ // while and let other things happen first.
+ delay = 15000;
+
+ if (
+ shouldResyncGoogleCalDav &&
+ calendar.type == "caldav" &&
+ calendar.uri.prePath == "https://apidata.googleusercontent.com"
+ ) {
+ cal.LOG(`CalDAV: Resetting sync token of ${calendar.name} to perform a full resync`);
+ let calCachedCalendar = calendar.wrappedJSObject;
+ let calDavCalendar = calCachedCalendar.mUncachedCalendar.wrappedJSObject;
+ calDavCalendar.mWebdavSyncToken = null;
+ calDavCalendar.saveCalendarProperties();
+ }
+ }
+ setTimeout(() => maybeRefreshCalendar(calendar), delay);
+ }
+
+ if (shouldResyncGoogleCalDav) {
+ // Record the fact that we've scheduled a resync, so that we only do it once.
+ // Store the date instead of a boolean because we might want to use this again some day.
+ Services.prefs.setIntPref("calendar.caldav.googleResync", Date.now() / 1000);
+ }
+ },
+
+ getCalendarPref_(calendar, name) {
+ cal.ASSERT(calendar, "Invalid Calendar!");
+ cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!");
+ cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!");
+
+ let branch = getPrefBranchFor(calendar.id) + name;
+ let value = Preferences.get(branch, null);
+
+ if (typeof value == "string" && value.startsWith("bignum:")) {
+ let converted = Number(value.substr(7));
+ if (!isNaN(converted)) {
+ value = converted;
+ }
+ }
+ return value;
+ },
+
+ setCalendarPref_(calendar, name, value) {
+ cal.ASSERT(calendar, "Invalid Calendar!");
+ cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!");
+ cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!");
+
+ let branch = getPrefBranchFor(calendar.id) + name;
+
+ if (
+ typeof value == "number" &&
+ (value > MAX_INT || value < MIN_INT || !Number.isInteger(value))
+ ) {
+ // This is something the preferences service can't store directly.
+ // Convert to string and tag it so we know how to handle it.
+ value = "bignum:" + value;
+ }
+
+ // Delete before to allow pref-type changes, then set the pref.
+ Services.prefs.clearUserPref(branch);
+ if (value !== null && value !== undefined) {
+ Preferences.set(branch, value);
+ }
+ },
+
+ deleteCalendarPref_(calendar, name) {
+ cal.ASSERT(calendar, "Invalid Calendar!");
+ cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!");
+ cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!");
+ Services.prefs.clearUserPref(getPrefBranchFor(calendar.id) + name);
+ },
+
+ mObservers: null,
+ addObserver(aObserver) {
+ this.mObservers.add(aObserver);
+ },
+ removeObserver(aObserver) {
+ this.mObservers.delete(aObserver);
+ },
+ notifyObservers(functionName, args) {
+ this.mObservers.notify(functionName, args);
+ },
+
+ mCalendarObservers: null,
+ addCalendarObserver(aObserver) {
+ return this.mCalendarObservers.add(aObserver);
+ },
+ removeCalendarObserver(aObserver) {
+ return this.mCalendarObservers.delete(aObserver);
+ },
+ notifyCalendarObservers(functionName, args) {
+ this.mCalendarObservers.notify(functionName, args);
+ },
+};
+
+function equalMessage(msg1, msg2) {
+ if (
+ msg1.GetString(0) == msg2.GetString(0) &&
+ msg1.GetString(1) == msg2.GetString(1) &&
+ msg1.GetString(2) == msg2.GetString(2)
+ ) {
+ return true;
+ }
+ return false;
+}
+
+function calMgrCalendarObserver(calendar, calMgr) {
+ this.calendar = calendar;
+ // We compare this to determine if the state actually changed.
+ this.storedReadOnly = calendar.readOnly;
+ this.announcedMessages = [];
+ this.calMgr = calMgr;
+}
+
+calMgrCalendarObserver.prototype = {
+ calendar: null,
+ storedReadOnly: null,
+ calMgr: null,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWindowMediatorListener", "calIObserver"]),
+
+ // calIObserver:
+ onStartBatch() {
+ return this.calMgr.notifyCalendarObservers("onStartBatch", arguments);
+ },
+ onEndBatch() {
+ return this.calMgr.notifyCalendarObservers("onEndBatch", arguments);
+ },
+ onLoad(calendar) {
+ return this.calMgr.notifyCalendarObservers("onLoad", arguments);
+ },
+ onAddItem(aItem) {
+ return this.calMgr.notifyCalendarObservers("onAddItem", arguments);
+ },
+ onModifyItem(aNewItem, aOldItem) {
+ return this.calMgr.notifyCalendarObservers("onModifyItem", arguments);
+ },
+ onDeleteItem(aDeletedItem) {
+ return this.calMgr.notifyCalendarObservers("onDeleteItem", arguments);
+ },
+ onError(aCalendar, aErrNo, aMessage) {
+ this.calMgr.notifyCalendarObservers("onError", arguments);
+ this.announceError(aCalendar, aErrNo, aMessage);
+ },
+
+ onPropertyChanged(aCalendar, aName, aValue, aOldValue) {
+ this.calMgr.notifyCalendarObservers("onPropertyChanged", arguments);
+ switch (aName) {
+ case "requiresNetwork":
+ this.calMgr.mNetworkCalendarCount += aValue ? 1 : -1;
+ break;
+ case "readOnly":
+ this.calMgr.mReadonlyCalendarCount += aValue ? 1 : -1;
+ break;
+ case "refreshInterval":
+ this.calMgr.setupRefreshTimer(aCalendar);
+ break;
+ case "cache.enabled":
+ this.changeCalendarCache(...arguments);
+ break;
+ case "disabled":
+ if (!aValue && aCalendar.canRefresh) {
+ aCalendar.refresh();
+ }
+ break;
+ }
+ },
+
+ changeCalendarCache(aCalendar, aName, aValue, aOldValue) {
+ const cICM = Ci.calICalendarManager;
+ aOldValue = aOldValue || false;
+ aValue = aValue || false;
+
+ // hack for bug 1182264 to deal with calendars, which have set cache.enabled, but in fact do
+ // not support caching (like storage calendars) - this also prevents enabling cache again
+ if (aCalendar.getProperty("cache.supported") === false) {
+ if (aCalendar.getProperty("cache.enabled") === true) {
+ aCalendar.deleteProperty("cache.enabled");
+ }
+ return;
+ }
+
+ if (aOldValue != aValue) {
+ // Try to find the current sort order
+ let sortOrderPref = Services.prefs.getStringPref("calendar.list.sortOrder", "").split(" ");
+ let initialSortOrderPos = null;
+ for (let i = 0; i < sortOrderPref.length; ++i) {
+ if (sortOrderPref[i] == aCalendar.id) {
+ initialSortOrderPos = i;
+ }
+ }
+ // Enabling or disabling cache on a calendar re-creates
+ // it so the registerCalendar call can wrap/unwrap the
+ // calCachedCalendar facade saving the user the need to
+ // restart Thunderbird and making sure a new Id is used.
+ this.calMgr.removeCalendar(aCalendar, cICM.REMOVE_NO_DELETE);
+ let newCal = this.calMgr.createCalendar(aCalendar.type, aCalendar.uri);
+ newCal.name = aCalendar.name;
+
+ // TODO: if properties get added this list will need to be adjusted,
+ // ideally we should add a "getProperties" method to calICalendar.idl
+ // to retrieve all non-transient properties for a calendar.
+ let propsToCopy = [
+ "color",
+ "disabled",
+ "auto-enabled",
+ "cache.enabled",
+ "refreshInterval",
+ "suppressAlarms",
+ "calendar-main-in-composite",
+ "calendar-main-default",
+ "readOnly",
+ "imip.identity.key",
+ "username",
+ ];
+ for (let prop of propsToCopy) {
+ newCal.setProperty(prop, aCalendar.getProperty(prop));
+ }
+
+ if (initialSortOrderPos != null) {
+ newCal.setProperty("initialSortOrderPos", initialSortOrderPos);
+ }
+ this.calMgr.registerCalendar(newCal);
+ } else if (aCalendar.wrappedJSObject instanceof calCachedCalendar) {
+ // any attempt to switch this flag will reset the cached calendar;
+ // could be useful for users in case the cache may be corrupted.
+ aCalendar.wrappedJSObject.setupCachedCalendar();
+ }
+ },
+
+ onPropertyDeleting(aCalendar, aName) {
+ this.onPropertyChanged(aCalendar, aName, false, true);
+ },
+
+ // Error announcer specific functions
+ announceError(aCalendar, aErrNo, aMessage) {
+ let paramBlock = Cc["@mozilla.org/embedcomp/dialogparam;1"].createInstance(
+ Ci.nsIDialogParamBlock
+ );
+ let props = Services.strings.createBundle("chrome://calendar/locale/calendar.properties");
+ let errMsg;
+ paramBlock.SetNumberStrings(3);
+ if (!this.storedReadOnly && this.calendar.readOnly) {
+ // Major errors change the calendar to readOnly
+ errMsg = props.formatStringFromName("readOnlyMode", [this.calendar.name]);
+ } else if (!this.storedReadOnly && !this.calendar.readOnly) {
+ // Minor errors don't, but still tell the user something went wrong
+ errMsg = props.formatStringFromName("minorError", [this.calendar.name]);
+ } else {
+ // The calendar was already in readOnly mode, but still tell the user
+ errMsg = props.formatStringFromName("stillReadOnlyError", [this.calendar.name]);
+ }
+
+ // When possible, change the error number into its name, to
+ // make it slightly more readable.
+ let errCode = "0x" + aErrNo.toString(16);
+ const calIErrors = Ci.calIErrors;
+ // Check if it is worth enumerating all the error codes.
+ if (aErrNo & calIErrors.ERROR_BASE) {
+ for (let err in calIErrors) {
+ if (calIErrors[err] == aErrNo) {
+ errCode = err;
+ }
+ }
+ }
+
+ let message;
+ switch (aErrNo) {
+ case calIErrors.CAL_UTF8_DECODING_FAILED:
+ message = props.GetStringFromName("utf8DecodeError");
+ break;
+ case calIErrors.ICS_MALFORMEDDATA:
+ message = props.GetStringFromName("icsMalformedError");
+ break;
+ case calIErrors.MODIFICATION_FAILED:
+ errMsg = cal.l10n.getCalString("errorWriting2", [aCalendar.name]);
+ message = cal.l10n.getCalString("errorWritingDetails");
+ if (aMessage) {
+ message = aMessage + "\n" + message;
+ }
+ break;
+ default:
+ message = aMessage;
+ }
+
+ paramBlock.SetString(0, errMsg);
+ paramBlock.SetString(1, errCode);
+ paramBlock.SetString(2, message);
+
+ this.storedReadOnly = this.calendar.readOnly;
+ let errorCode = cal.l10n.getCalString("errorCode", [errCode]);
+ let errorDescription = cal.l10n.getCalString("errorDescription", [message]);
+ let summary = errMsg + " " + errorCode + ". " + errorDescription;
+
+ // Log warnings in error console.
+ // Report serious errors in both error console and in prompt window.
+ if (aErrNo == calIErrors.MODIFICATION_FAILED) {
+ console.error(summary);
+ this.announceParamBlock(paramBlock);
+ } else {
+ cal.WARN(summary);
+ }
+ },
+
+ announceParamBlock(paramBlock) {
+ function awaitLoad(event) {
+ promptWindow.addEventListener("unload", awaitUnload, { capture: false, once: true });
+ }
+ let awaitUnload = event => {
+ // unloaded (user closed prompt window),
+ // remove paramBlock and unload listener.
+ try {
+ // remove the message that has been shown from
+ // the list of all announced messages.
+ this.announcedMessages = this.announcedMessages.filter(msg => {
+ return !equalMessage(msg, paramBlock);
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ // silently don't do anything if this message already has been
+ // announced without being acknowledged.
+ if (this.announcedMessages.some(equalMessage.bind(null, paramBlock))) {
+ return;
+ }
+
+ // this message hasn't been announced recently, remember the details of
+ // the message for future reference.
+ this.announcedMessages.push(paramBlock);
+
+ // Will remove paramBlock from announced messages when promptWindow is
+ // closed. (Closing fires unloaded event, but promptWindow is also
+ // unloaded [to clean it?] before loading, so wait for detected load
+ // event before detecting unload event that signifies user closed this
+ // prompt window.)
+ let promptUrl = "chrome://calendar/content/calendar-error-prompt.xhtml";
+ let features = "chrome,dialog=yes,alwaysRaised=yes";
+ let promptWindow = Services.ww.openWindow(null, promptUrl, "_blank", features, paramBlock);
+ promptWindow.addEventListener("load", awaitLoad, { capture: false, once: true });
+ },
+};
+
+function calDummyCalendar(type) {
+ this.initProviderBase();
+ this.type = type;
+}
+calDummyCalendar.prototype = {
+ __proto__: cal.provider.BaseClass.prototype,
+
+ getProperty(aName) {
+ switch (aName) {
+ case "force-disabled":
+ return true;
+ default:
+ return this.__proto__.__proto__.getProperty.apply(this, arguments);
+ }
+ },
+};
+
+function getPrefBranchFor(id) {
+ return REGISTRY_BRANCH + id + ".";
+}
+
+/**
+ * Removes a calendar from the preferences.
+ *
+ * @param {string} id - ID of the calendar to remove.
+ */
+function deletePrefBranch(id) {
+ for (let prefName of Services.prefs.getChildList(getPrefBranchFor(id))) {
+ Services.prefs.clearUserPref(prefName);
+ }
+}
+
+/**
+ * Helper to refresh a calendar, if it can be refreshed and isn't disabled.
+ *
+ * @param {calICalendar} calendar - The calendar to refresh.
+ */
+function maybeRefreshCalendar(calendar) {
+ if (!calendar.getProperty("disabled") && calendar.canRefresh) {
+ let refreshInterval = calendar.getProperty("refreshInterval");
+ if (refreshInterval != "0") {
+ calendar.refresh();
+ }
+ }
+}
+
+/**
+ * Wrap a calendar using {@link calCachedCalendar}, if the cache is supported and enabled.
+ * Otherwise just return the passed in calendar.
+ *
+ * @param {calICalendar} calendar - The calendar to potentially wrap.
+ * @returns {calICalendar} The potentially wrapped calendar.
+ */
+function maybeWrapCachedCalendar(calendar) {
+ if (
+ calendar.getProperty("cache.supported") !== false &&
+ (calendar.getProperty("cache.enabled") || calendar.getProperty("cache.always"))
+ ) {
+ calendar = new calCachedCalendar(calendar);
+ }
+ return calendar;
+}
+
+/**
+ * Helper function to flush the preferences file. If the application crashes
+ * after a calendar has been created using the prefs registry, then the calendar
+ * won't show up. Writing the prefs helps counteract.
+ */
+function flushPrefs() {
+ Services.prefs.savePrefFile(null);
+}
+
+/**
+ * Callback object for the refresh timer. Should be called as an object, i.e
+ * let foo = new timerCallback(calendar);
+ *
+ * @param aCalendar The calendar to refresh on notification
+ */
+function timerCallback(aCalendar) {
+ this.notify = function (aTimer) {
+ if (!aCalendar.getProperty("disabled") && aCalendar.canRefresh) {
+ aCalendar.refresh();
+ }
+ };
+}
+
+var gCalendarManagerAddonListener = {
+ onDisabling(aAddon, aNeedsRestart) {
+ if (!this.queryUninstallProvider(aAddon)) {
+ // If the addon should not be disabled, then re-enable it.
+ aAddon.userDisabled = false;
+ }
+ },
+
+ onUninstalling(aAddon, aNeedsRestart) {
+ if (!this.queryUninstallProvider(aAddon)) {
+ // If the addon should not be uninstalled, then cancel the uninstall.
+ aAddon.cancelUninstall();
+ }
+ },
+
+ queryUninstallProvider(aAddon) {
+ const uri = "chrome://calendar/content/calendar-providerUninstall-dialog.xhtml";
+ const features = "chrome,titlebar,resizable,modal";
+ let affectedCalendars = cal.manager
+ .getCalendars()
+ .filter(calendar => calendar.providerID == aAddon.id);
+ if (!affectedCalendars.length) {
+ // If no calendars are affected, then everything is fine.
+ return true;
+ }
+
+ let args = { shouldUninstall: false, extension: aAddon };
+
+ // Now find a window. The best choice would be the most recent
+ // addons window, otherwise the most recent calendar window, or we
+ // create a new toplevel window.
+ let win =
+ Services.wm.getMostRecentWindow("Extension:Manager") || cal.window.getCalendarWindow();
+ if (win) {
+ win.openDialog(uri, "CalendarProviderUninstallDialog", features, args);
+ } else {
+ // Use the window watcher to open a parentless window.
+ Services.ww.openWindow(null, uri, "CalendarProviderUninstallWindow", features, args);
+ }
+
+ // Now that we are done, check if the dialog was accepted or canceled.
+ return args.shouldUninstall;
+ },
+};
+
+function appendToRealm(authHeader, appendStr) {
+ let isEscaped = false;
+ let idx = authHeader.search(/realm="(.*?)(\\*)"/);
+ if (idx > -1) {
+ let remain = authHeader.substr(idx + 7);
+ idx += 7;
+ while (remain.length && !isEscaped) {
+ let match = remain.match(/(.*?)(\\*)"/);
+ idx += match[0].length;
+
+ isEscaped = match[2].length % 2 == 0;
+ if (!isEscaped) {
+ remain = remain.substr(match[0].length);
+ }
+ }
+ return authHeader.substr(0, idx - 1) + " " + appendStr + authHeader.substr(idx - 1);
+ }
+ return authHeader;
+}
diff --git a/comm/calendar/base/src/CalDateTime.jsm b/comm/calendar/base/src/CalDateTime.jsm
new file mode 100644
index 0000000000..8c7daefa32
--- /dev/null
+++ b/comm/calendar/base/src/CalDateTime.jsm
@@ -0,0 +1,202 @@
+/* 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 = ["CalDateTime"];
+
+const { ICAL, unwrap, unwrapSetter } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm");
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(lazy, "CalDuration", "resource:///modules/CalDuration.jsm");
+ChromeUtils.defineModuleGetter(lazy, "CalTimezone", "resource:///modules/CalTimezone.jsm");
+
+var UNIX_TIME_TO_PRTIME = 1000000;
+
+function CalDateTime(innerObject) {
+ this.wrappedJSObject = this;
+ this.innerObject = innerObject || ICAL.Time.epochTime.clone();
+}
+
+CalDateTime.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIDateTime"]),
+ classID: Components.ID("{36783242-ec94-4d8a-9248-d2679edd55b9}"),
+
+ isMutable: true,
+ makeImmutable() {
+ this.isMutable = false;
+ },
+ clone() {
+ return new CalDateTime(this.innerObject.clone());
+ },
+
+ isValid: true,
+ innerObject: null,
+
+ get nativeTime() {
+ return this.innerObject.toUnixTime() * UNIX_TIME_TO_PRTIME;
+ },
+ set nativeTime(val) {
+ this.innerObject.fromUnixTime(val / UNIX_TIME_TO_PRTIME);
+ },
+
+ get year() {
+ return this.innerObject.year;
+ },
+ set year(val) {
+ this.innerObject.year = parseInt(val, 10);
+ },
+
+ get month() {
+ return this.innerObject.month - 1;
+ },
+ set month(val) {
+ this.innerObject.month = val + 1;
+ },
+
+ get day() {
+ return this.innerObject.day;
+ },
+ set day(val) {
+ this.innerObject.day = parseInt(val, 10);
+ },
+
+ get hour() {
+ return this.innerObject.hour;
+ },
+ set hour(val) {
+ this.innerObject.hour = parseInt(val, 10);
+ },
+
+ get minute() {
+ return this.innerObject.minute;
+ },
+ set minute(val) {
+ this.innerObject.minute = parseInt(val, 10);
+ },
+
+ get second() {
+ return this.innerObject.second;
+ },
+ set second(val) {
+ this.innerObject.second = parseInt(val, 10);
+ },
+
+ get timezone() {
+ return new lazy.CalTimezone(this.innerObject.zone);
+ },
+ set timezone(rawval) {
+ unwrapSetter(
+ ICAL.Timezone,
+ rawval,
+ function (val) {
+ this.innerObject.zone = val;
+ return val;
+ },
+ this
+ );
+ },
+
+ resetTo(year, month, day, hour, minute, second, timezone) {
+ this.innerObject.fromData({
+ year,
+ month: month + 1,
+ day,
+ hour,
+ minute,
+ second,
+ });
+ this.timezone = timezone;
+ },
+
+ reset() {
+ this.innerObject.reset();
+ },
+
+ get timezoneOffset() {
+ return this.innerObject.utcOffset();
+ },
+ get isDate() {
+ return this.innerObject.isDate;
+ },
+ set isDate(val) {
+ this.innerObject.isDate = !!val;
+ },
+
+ get weekday() {
+ return this.innerObject.dayOfWeek() - 1;
+ },
+ get yearday() {
+ return this.innerObject.dayOfYear();
+ },
+
+ toString() {
+ return this.innerObject.toString();
+ },
+
+ toJSON() {
+ return this.toString();
+ },
+
+ getInTimezone: unwrap(ICAL.Timezone, function (val) {
+ return new CalDateTime(this.innerObject.convertToZone(val));
+ }),
+
+ addDuration: unwrap(ICAL.Duration, function (val) {
+ this.innerObject.addDuration(val);
+ }),
+
+ subtractDate: unwrap(ICAL.Time, function (val) {
+ return new lazy.CalDuration(this.innerObject.subtractDateTz(val));
+ }),
+
+ compare: unwrap(ICAL.Time, function (val) {
+ let a = this.innerObject;
+ let b = val;
+
+ // If either this or aOther is floating, both objects are treated
+ // as floating for the comparison.
+ if (a.zone == ICAL.Timezone.localTimezone || b.zone == ICAL.Timezone.localTimezone) {
+ a = a.convertToZone(ICAL.Timezone.localTimezone);
+ b = b.convertToZone(ICAL.Timezone.localTimezone);
+ }
+
+ if (a.isDate || b.isDate) {
+ // Calendar expects 20120101 and 20120101T010101 to be equal
+ return a.compareDateOnlyTz(b, a.zone);
+ }
+ // If both are dates or date-times, then just do the normal compare
+ return a.compare(b);
+ }),
+
+ get startOfWeek() {
+ return new CalDateTime(this.innerObject.startOfWeek());
+ },
+ get endOfWeek() {
+ return new CalDateTime(this.innerObject.endOfWeek());
+ },
+ get startOfMonth() {
+ return new CalDateTime(this.innerObject.startOfMonth());
+ },
+ get endOfMonth() {
+ return new CalDateTime(this.innerObject.endOfMonth());
+ },
+ get startOfYear() {
+ return new CalDateTime(this.innerObject.startOfYear());
+ },
+ get endOfYear() {
+ return new CalDateTime(this.innerObject.endOfYear());
+ },
+
+ get icalString() {
+ return this.innerObject.toICALString();
+ },
+ set icalString(val) {
+ let jcalString;
+ if (val.length > 10) {
+ jcalString = ICAL.design.icalendar.value["date-time"].fromICAL(val);
+ } else {
+ jcalString = ICAL.design.icalendar.value.date.fromICAL(val);
+ }
+ this.innerObject = ICAL.Time.fromString(jcalString);
+ },
+};
diff --git a/comm/calendar/base/src/CalDefaultACLManager.jsm b/comm/calendar/base/src/CalDefaultACLManager.jsm
new file mode 100644
index 0000000000..ca660f8e67
--- /dev/null
+++ b/comm/calendar/base/src/CalDefaultACLManager.jsm
@@ -0,0 +1,97 @@
+/* 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 = ["CalDefaultACLManager"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+function CalDefaultACLManager() {
+ this.mCalendarEntries = {};
+}
+
+CalDefaultACLManager.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calICalendarACLManager"]),
+ classID: Components.ID("{7463258c-6ef3-40a2-89a9-bb349596e927}"),
+
+ mCalendarEntries: null,
+
+ /* calICalendarACLManager */
+ _getCalendarEntryCached(aCalendar) {
+ let calUri = aCalendar.uri.spec;
+ if (!(calUri in this.mCalendarEntries)) {
+ this.mCalendarEntries[calUri] = new calDefaultCalendarACLEntry(this, aCalendar);
+ }
+
+ return this.mCalendarEntries[calUri];
+ },
+ getCalendarEntry(aCalendar, aListener) {
+ let entry = this._getCalendarEntryCached(aCalendar);
+ aListener.onOperationComplete(aCalendar, Cr.NS_OK, Ci.calIOperationListener.GET, null, entry);
+ },
+ getItemEntry(aItem) {
+ let calEntry = this._getCalendarEntryCached(aItem.calendar);
+ return new calDefaultItemACLEntry(calEntry);
+ },
+};
+
+function calDefaultCalendarACLEntry(aMgr, aCalendar) {
+ this.mACLManager = aMgr;
+ this.mCalendar = aCalendar;
+}
+
+calDefaultCalendarACLEntry.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calICalendarACLEntry"]),
+
+ mACLManager: null,
+
+ /* calICalendarACLCalendarEntry */
+ get aclManager() {
+ return this.mACLManager;
+ },
+
+ hasAccessControl: false,
+ userIsOwner: true,
+ userCanAddItems: true,
+ userCanDeleteItems: true,
+
+ _getIdentities() {
+ let identities = [];
+ cal.email.iterateIdentities(id => identities.push(id));
+ return identities;
+ },
+
+ getUserAddresses() {
+ let identities = this.getUserIdentities();
+ let addresses = identities.map(id => id.email);
+ return addresses;
+ },
+
+ getUserIdentities() {
+ let identity = cal.provider.getEmailIdentityOfCalendar(this.mCalendar);
+ if (identity) {
+ return [identity];
+ }
+ return this._getIdentities();
+ },
+ getOwnerIdentities() {
+ return this._getIdentities();
+ },
+
+ refresh() {},
+};
+
+function calDefaultItemACLEntry(aCalendarEntry) {
+ this.calendarEntry = aCalendarEntry;
+}
+
+calDefaultItemACLEntry.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIItemACLEntry"]),
+
+ /* calIItemACLEntry */
+ calendarEntry: null,
+ userCanModify: true,
+ userCanRespond: true,
+ userCanViewAll: true,
+ userCanViewDateAndTime: true,
+};
diff --git a/comm/calendar/base/src/CalDeletedItems.jsm b/comm/calendar/base/src/CalDeletedItems.jsm
new file mode 100644
index 0000000000..2db386b7c5
--- /dev/null
+++ b/comm/calendar/base/src/CalDeletedItems.jsm
@@ -0,0 +1,200 @@
+/* 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 = ["CalDeletedItems"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { FileUtils } = ChromeUtils.importESModule("resource://gre/modules/FileUtils.sys.mjs");
+
+/**
+ * Handles remembering deleted items.
+ *
+ * This is (currently) not a real trashcan. Only ids and time deleted is stored.
+ * Note also that the code doesn't strictly check the calendar of the item,
+ * except when a calendar id is passed to getDeletedDate.
+ */
+function CalDeletedItems() {
+ this.wrappedJSObject = this;
+
+ this.completedNotifier = {
+ handleResult() {},
+ handleError() {},
+ handleCompletion() {},
+ };
+}
+
+var calDeletedItemsClassID = Components.ID("{8e6799af-e7e9-4e6c-9a82-a2413e86d8c3}");
+var calDeletedItemsInterfaces = [Ci.calIDeletedItems, Ci.nsIObserver, Ci.calIObserver];
+CalDeletedItems.prototype = {
+ classID: calDeletedItemsClassID,
+ QueryInterface: cal.generateQI(["calIDeletedItems", "nsIObserver", "calIObserver"]),
+ classInfo: cal.generateCI({
+ classID: calDeletedItemsClassID,
+ contractID: "@mozilla.org/calendar/deleted-items-manager;1",
+ classDescription: "Database containing information about deleted items",
+ interfaces: calDeletedItemsInterfaces,
+ flags: Ci.nsIClassInfo.SINGLETON,
+ }),
+
+ DB_SCHEMA_VERSION: 1,
+ STALE_TIME: (30 * 24 * 60 * 60) / 1000 /* 30 days */,
+
+ // To make the tests more failsafe, we have an internal notifier function.
+ // As the deleted items store is just meant to be a hint, this should not
+ // be used in real code.
+ completedNotifier: null,
+
+ flush() {
+ this.ensureStatements();
+ this.stmtFlush.params.stale_time = cal.dtz.now().nativeTime - this.STALE_TIME;
+ this.stmtFlush.executeAsync(this.completedNotifier);
+ },
+
+ getDeletedDate(aId, aCalId) {
+ this.ensureStatements();
+ let stmt;
+ if (aCalId) {
+ stmt = this.stmtGetWithCal;
+ stmt.params.calId = aCalId;
+ } else {
+ stmt = this.stmtGet;
+ }
+
+ stmt.params.id = aId;
+ try {
+ if (stmt.executeStep()) {
+ let date = cal.createDateTime();
+ date.nativeTime = stmt.row.time_deleted;
+ return date.getInTimezone(cal.dtz.defaultTimezone);
+ }
+ } catch (e) {
+ cal.ERROR(e);
+ } finally {
+ stmt.reset();
+ }
+ return null;
+ },
+
+ markDeleted(aItem) {
+ this.ensureStatements();
+ this.stmtMarkDelete.params.calId = aItem.calendar.id;
+ this.stmtMarkDelete.params.id = aItem.id;
+ this.stmtMarkDelete.params.time = cal.dtz.now().nativeTime;
+ this.stmtMarkDelete.params.rid = (aItem.recurrenceId && aItem.recurrenceId.nativeTime) || "";
+ this.stmtMarkDelete.executeAsync(this.completedNotifier);
+ },
+
+ unmarkDeleted(aItem) {
+ this.ensureStatements();
+ this.stmtUnmarkDelete.params.id = aItem.id;
+ this.stmtUnmarkDelete.executeAsync(this.completedNotifier);
+ },
+
+ initDB() {
+ if (this.mDB) {
+ // Looks like we've already initialized, exit early
+ return;
+ }
+
+ let file = FileUtils.getFile("ProfD", ["calendar-data", "deleted.sqlite"]);
+ this.mDB = Services.storage.openDatabase(file);
+
+ // If this database needs changing, please start using a real schema
+ // management, i.e using PRAGMA user_version and upgrading
+ if (!this.mDB.tableExists("cal_deleted_items")) {
+ const v1_schema = "cal_id TEXT, id TEXT, time_deleted INTEGER, recurrence_id INTEGER";
+ const v1_index =
+ "CREATE INDEX idx_deleteditems ON cal_deleted_items(id,cal_id,recurrence_id)";
+
+ this.mDB.createTable("cal_deleted_items", v1_schema);
+ this.mDB.executeSimpleSQL(v1_index);
+ this.mDB.executeSimpleSQL("PRAGMA user_version = 1");
+ }
+
+ // We will not init the statements now, we can still do that the
+ // first time this interface is used. What we should do though is
+ // to clean up at shutdown
+ cal.addShutdownObserver(this.shutdown.bind(this));
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "profile-after-change") {
+ // Make sure to observe calendar changes so we know when things are
+ // deleted. We don't initialize the statements until first use.
+ cal.manager.addCalendarObserver(this);
+ }
+ },
+
+ ensureStatements() {
+ if (!this.mDB) {
+ this.initDB();
+ }
+
+ if (!this.stmtMarkDelete) {
+ let stmt =
+ "INSERT OR REPLACE INTO cal_deleted_items (cal_id, id, time_deleted, recurrence_id) VALUES(:calId, :id, :time, :rid)";
+ this.stmtMarkDelete = this.mDB.createStatement(stmt);
+ }
+ if (!this.stmtUnmarkDelete) {
+ let stmt = "DELETE FROM cal_deleted_items WHERE id = :id";
+ this.stmtUnmarkDelete = this.mDB.createStatement(stmt);
+ }
+ if (!this.stmtGetWithCal) {
+ let stmt = "SELECT time_deleted FROM cal_deleted_items WHERE cal_id = :calId AND id = :id";
+ this.stmtGetWithCal = this.mDB.createStatement(stmt);
+ }
+ if (!this.stmtGet) {
+ let stmt = "SELECT time_deleted FROM cal_deleted_items WHERE id = :id";
+ this.stmtGet = this.mDB.createStatement(stmt);
+ }
+ if (!this.stmtFlush) {
+ let stmt = "DELETE FROM cal_deleted_items WHERE time_deleted < :stale_time";
+ this.stmtFlush = this.mDB.createStatement(stmt);
+ }
+ },
+
+ shutdown() {
+ try {
+ let stmts = [
+ this.stmtMarkDelete,
+ this.stmtUnmarkDelete,
+ this.stmtGet,
+ this.stmtGetWithCal,
+ this.stmtFlush,
+ ];
+ for (let stmt of stmts) {
+ stmt.finalize();
+ }
+
+ if (this.mDB) {
+ this.mDB.asyncClose();
+ this.mDB = null;
+ }
+ } catch (e) {
+ cal.ERROR("Error closing deleted items database: " + e);
+ }
+
+ cal.manager.removeCalendarObserver(this);
+ },
+
+ // calIObserver
+ onStartBatch() {},
+ onEndBatch() {},
+ onModifyItem() {},
+ onError() {},
+ onPropertyChanged() {},
+ onPropertyDeleting() {},
+
+ onAddItem(aItem) {
+ this.unmarkDeleted(aItem);
+ },
+
+ onDeleteItem(aItem) {
+ this.markDeleted(aItem);
+ },
+
+ onLoad() {
+ this.flush();
+ },
+};
diff --git a/comm/calendar/base/src/CalDuration.jsm b/comm/calendar/base/src/CalDuration.jsm
new file mode 100644
index 0000000000..cb289bdde4
--- /dev/null
+++ b/comm/calendar/base/src/CalDuration.jsm
@@ -0,0 +1,106 @@
+/* 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 = ["CalDuration"];
+
+const { ICAL, unwrap } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm");
+
+function CalDuration(innerObject) {
+ this.innerObject = innerObject || new ICAL.Duration();
+ this.wrappedJSObject = this;
+}
+
+CalDuration.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIDuration"]),
+ classID: Components.ID("{7436f480-c6fc-4085-9655-330b1ee22288}"),
+
+ get icalDuration() {
+ return this.innerObject;
+ },
+ set icalDuration(val) {
+ this.innerObject = val;
+ },
+
+ isMutable: true,
+ makeImmutable() {
+ this.isMutable = false;
+ },
+ clone() {
+ return new CalDuration(this.innerObject.clone());
+ },
+
+ get isNegative() {
+ return this.innerObject.isNegative;
+ },
+ set isNegative(val) {
+ this.innerObject.isNegative = !!val;
+ },
+
+ get weeks() {
+ return this.innerObject.weeks;
+ },
+ set weeks(val) {
+ this.innerObject.weeks = parseInt(val, 10);
+ },
+
+ get days() {
+ return this.innerObject.days;
+ },
+ set days(val) {
+ this.innerObject.days = parseInt(val, 10);
+ },
+
+ get hours() {
+ return this.innerObject.hours;
+ },
+ set hours(val) {
+ this.innerObject.hours = parseInt(val, 10);
+ },
+
+ get minutes() {
+ return this.innerObject.minutes;
+ },
+ set minutes(val) {
+ this.innerObject.minutes = parseInt(val, 10);
+ },
+
+ get seconds() {
+ return this.innerObject.seconds;
+ },
+ set seconds(val) {
+ this.innerObject.seconds = parseInt(val, 10);
+ },
+
+ get inSeconds() {
+ return this.innerObject.toSeconds();
+ },
+ set inSeconds(val) {
+ this.innerObject.fromSeconds(val);
+ },
+
+ addDuration: unwrap(ICAL.Duration, function (val) {
+ this.innerObject.fromSeconds(this.innerObject.toSeconds() + val.toSeconds());
+ }),
+
+ compare: unwrap(ICAL.Duration, function (val) {
+ return this.innerObject.compare(val);
+ }),
+
+ reset() {
+ this.innerObject.reset();
+ },
+ normalize() {
+ this.innerObject.normalize();
+ },
+ toString() {
+ return this.innerObject.toString();
+ },
+
+ get icalString() {
+ return this.innerObject.toString();
+ },
+ set icalString(val) {
+ this.innerObject = ICAL.Duration.fromString(val);
+ },
+};
diff --git a/comm/calendar/base/src/CalEvent.jsm b/comm/calendar/base/src/CalEvent.jsm
new file mode 100644
index 0000000000..fa5225e520
--- /dev/null
+++ b/comm/calendar/base/src/CalEvent.jsm
@@ -0,0 +1,225 @@
+/* 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/. */
+
+/* import-globals-from calItemBase.js */
+
+var EXPORTED_SYMBOLS = ["CalEvent"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+Services.scriptloader.loadSubScript("resource:///components/calItemBase.js");
+
+/**
+ * Constructor for `calIEvent` objects.
+ *
+ * @class
+ * @implements {calIEvent}
+ * @param {string} [icalString] - Optional iCal string for initializing existing events.
+ */
+function CalEvent(icalString) {
+ this.initItemBase();
+
+ this.eventPromotedProps = {
+ DTSTART: true,
+ DTEND: true,
+ __proto__: this.itemBasePromotedProps,
+ };
+
+ if (icalString) {
+ this.icalString = icalString;
+ }
+}
+var calEventClassID = Components.ID("{974339d5-ab86-4491-aaaf-2b2ca177c12b}");
+var calEventInterfaces = [Ci.calIItemBase, Ci.calIEvent, Ci.calIInternalShallowCopy];
+CalEvent.prototype = {
+ __proto__: calItemBase.prototype,
+
+ classID: calEventClassID,
+ QueryInterface: cal.generateQI(["calIItemBase", "calIEvent", "calIInternalShallowCopy"]),
+ classInfo: cal.generateCI({
+ classID: calEventClassID,
+ contractID: "@mozilla.org/calendar/event;1",
+ classDescription: "Calendar Event",
+ interfaces: calEventInterfaces,
+ }),
+
+ cloneShallow(aNewParent) {
+ let cloned = new CalEvent();
+ this.cloneItemBaseInto(cloned, aNewParent);
+ return cloned;
+ },
+
+ createProxy(aRecurrenceId) {
+ cal.ASSERT(!this.mIsProxy, "Tried to create a proxy for an existing proxy!", true);
+
+ let proxy = new CalEvent();
+
+ // override proxy's DTSTART/DTEND/RECURRENCE-ID
+ // before master is set (and item might get immutable):
+ let endDate = aRecurrenceId.clone();
+ endDate.addDuration(this.duration);
+ proxy.endDate = endDate;
+ proxy.startDate = aRecurrenceId;
+
+ proxy.initializeProxy(this, aRecurrenceId);
+ proxy.mDirty = false;
+
+ return proxy;
+ },
+
+ makeImmutable() {
+ this.makeItemBaseImmutable();
+ },
+
+ isEvent() {
+ return true;
+ },
+
+ get duration() {
+ if (this.endDate && this.startDate) {
+ return this.endDate.subtractDate(this.startDate);
+ }
+ // Return a null-duration if we don't have an end date
+ return cal.createDuration();
+ },
+
+ get recurrenceStartDate() {
+ return this.startDate;
+ },
+
+ icsEventPropMap: [
+ { cal: "DTSTART", ics: "startTime" },
+ { cal: "DTEND", ics: "endTime" },
+ ],
+
+ set icalString(value) {
+ this.icalComponent = cal.icsService.parseICS(value);
+ },
+
+ get icalString() {
+ let calcomp = cal.icsService.createIcalComponent("VCALENDAR");
+ cal.item.setStaticProps(calcomp);
+ calcomp.addSubcomponent(this.icalComponent);
+ return calcomp.serializeToICS();
+ },
+
+ get icalComponent() {
+ let icalcomp = cal.icsService.createIcalComponent("VEVENT");
+ this.fillIcalComponentFromBase(icalcomp);
+ this.mapPropsToICS(icalcomp, this.icsEventPropMap);
+
+ for (let [name, value] of this.properties) {
+ try {
+ // When deleting a property of an occurrence, the property is not deleted
+ // but instead set to null, so we need to prevent adding those properties.
+ let wasReset = this.mIsProxy && value === null;
+ if (!this.eventPromotedProps[name] && !wasReset) {
+ let icalprop = cal.icsService.createIcalProperty(name);
+ icalprop.value = value;
+ let propBucket = this.mPropertyParams[name];
+ if (propBucket) {
+ for (let paramName in propBucket) {
+ try {
+ icalprop.setParameter(paramName, propBucket[paramName]);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG(
+ "Warning: Invalid event parameter value " +
+ paramName +
+ "=" +
+ propBucket[paramName]
+ );
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+ icalcomp.addProperty(icalprop);
+ }
+ } catch (e) {
+ cal.ERROR("failed to set " + name + " to " + value + ": " + e + "\n");
+ }
+ }
+ return icalcomp;
+ },
+
+ eventPromotedProps: null,
+
+ set icalComponent(event) {
+ this.modify();
+ if (event.componentType != "VEVENT") {
+ event = event.getFirstSubcomponent("VEVENT");
+ if (!event) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+
+ this.mEndDate = undefined;
+ this.setItemBaseFromICS(event);
+ this.mapPropsFromICS(event, this.icsEventPropMap);
+
+ this.importUnpromotedProperties(event, this.eventPromotedProps);
+
+ // Importing didn't really change anything
+ this.mDirty = false;
+ },
+
+ isPropertyPromoted(name) {
+ // avoid strict undefined property warning
+ return this.eventPromotedProps[name] || false;
+ },
+
+ set startDate(value) {
+ this.modify();
+
+ // We're about to change the start date of an item which probably
+ // could break the associated calIRecurrenceInfo. We're calling
+ // the appropriate method here to adjust the internal structure in
+ // order to free clients from worrying about such details.
+ if (this.parentItem == this) {
+ let rec = this.recurrenceInfo;
+ if (rec) {
+ rec.onStartDateChange(value, this.startDate);
+ }
+ }
+
+ this.setProperty("DTSTART", value);
+ },
+
+ get startDate() {
+ return this.getProperty("DTSTART");
+ },
+
+ mEndDate: undefined,
+ get endDate() {
+ let endDate = this.mEndDate;
+ if (endDate === undefined) {
+ endDate = this.getProperty("DTEND");
+ if (!endDate && this.startDate) {
+ endDate = this.startDate.clone();
+ let dur = this.getProperty("DURATION");
+ if (dur) {
+ // If there is a duration set on the event, calculate the right end time.
+ endDate.addDuration(cal.createDuration(dur));
+ } else if (endDate.isDate) {
+ // If the start time is a date-time the event ends on the same calendar
+ // date and time of day. If the start time is a date the events
+ // non-inclusive end is the end of the calendar date.
+ endDate.day += 1;
+ }
+ }
+ this.mEndDate = endDate;
+ }
+ return endDate;
+ },
+
+ set endDate(value) {
+ this.deleteProperty("DURATION"); // setting endDate once removes DURATION
+ this.setProperty("DTEND", value);
+ this.mEndDate = value;
+ },
+};
diff --git a/comm/calendar/base/src/CalFreeBusyService.jsm b/comm/calendar/base/src/CalFreeBusyService.jsm
new file mode 100644
index 0000000000..822f37acda
--- /dev/null
+++ b/comm/calendar/base/src/CalFreeBusyService.jsm
@@ -0,0 +1,89 @@
+/* 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 = ["CalFreeBusyService"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+function CalFreeBusyListener(numOperations, finalListener) {
+ this.mFinalListener = finalListener;
+ this.mNumOperations = numOperations;
+
+ this.opGroup = new cal.data.OperationGroup(() => {
+ this.notifyResult(null);
+ });
+}
+CalFreeBusyListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIGenericOperationListener"]),
+
+ mFinalListener: null,
+ mNumOperations: 0,
+ opGroup: null,
+
+ notifyResult(result) {
+ let listener = this.mFinalListener;
+ if (listener) {
+ if (!this.opGroup.isPending) {
+ this.mFinalListener = null;
+ }
+ listener.onResult(this.opGroup, result);
+ }
+ },
+
+ // calIGenericOperationListener:
+ onResult(aOperation, aResult) {
+ if (this.mFinalListener) {
+ if (!aOperation || !aOperation.isPending) {
+ --this.mNumOperations;
+ if (this.mNumOperations <= 0) {
+ this.opGroup.notifyCompleted();
+ }
+ }
+ let opStatus = aOperation ? aOperation.status : Cr.NS_OK;
+ if (Components.isSuccessCode(opStatus) && aResult && Array.isArray(aResult)) {
+ this.notifyResult(aResult);
+ } else {
+ this.notifyResult([]);
+ }
+ }
+ },
+};
+
+function CalFreeBusyService() {
+ this.wrappedJSObject = this;
+ this.mProviders = new Set();
+}
+CalFreeBusyService.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIFreeBusyProvider", "calIFreeBusyService"]),
+ classID: Components.ID("{29c56cd5-d36e-453a-acde-0083bd4fe6d3}"),
+
+ mProviders: null,
+
+ // calIFreeBusyProvider:
+ getFreeBusyIntervals(aCalId, aRangeStart, aRangeEnd, aBusyTypes, aListener) {
+ let groupListener = new CalFreeBusyListener(this.mProviders.size, aListener);
+ if (this.mProviders.size == 0) {
+ groupListener.onResult(null, []);
+ }
+ for (let provider of this.mProviders.values()) {
+ let operation = provider.getFreeBusyIntervals(
+ aCalId,
+ aRangeStart,
+ aRangeEnd,
+ aBusyTypes,
+ groupListener
+ );
+ groupListener.opGroup.add(operation);
+ }
+ return groupListener.opGroup;
+ },
+
+ // calIFreeBusyService:
+ addProvider(aProvider) {
+ this.mProviders.add(aProvider.QueryInterface(Ci.calIFreeBusyProvider));
+ },
+ removeProvider(aProvider) {
+ this.mProviders.delete(aProvider.QueryInterface(Ci.calIFreeBusyProvider));
+ },
+};
diff --git a/comm/calendar/base/src/CalICSService.jsm b/comm/calendar/base/src/CalICSService.jsm
new file mode 100644
index 0000000000..fc9d53500e
--- /dev/null
+++ b/comm/calendar/base/src/CalICSService.jsm
@@ -0,0 +1,604 @@
+/* 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 = ["CalIcalProperty", "CalICSService"];
+
+const { ICAL, unwrapSetter, unwrapSingle, wrapGetter } = ChromeUtils.import(
+ "resource:///modules/calendar/Ical.jsm"
+);
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(lazy, "CalDateTime", "resource:///modules/CalDateTime.jsm");
+ChromeUtils.defineModuleGetter(lazy, "CalDuration", "resource:///modules/CalDuration.jsm");
+ChromeUtils.defineModuleGetter(lazy, "CalTimezone", "resource:///modules/CalTimezone.jsm");
+
+function CalIcalProperty(innerObject) {
+ this.innerObject = innerObject || new ICAL.Property();
+ this.wrappedJSObject = this;
+}
+
+CalIcalProperty.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIIcalProperty"]),
+ classID: Components.ID("{423ac3f0-f612-48b3-953f-47f7f8fd705b}"),
+
+ get icalString() {
+ return this.innerObject.toICALString() + ICAL.newLineChar;
+ },
+ get icalProperty() {
+ return this.innerObject;
+ },
+ set icalProperty(val) {
+ this.innerObject = val;
+ },
+
+ get parent() {
+ return this.innerObject.parent;
+ },
+ toString() {
+ return this.innerObject.toICAL();
+ },
+
+ get value() {
+ // Unescaped value for properties of TEXT, escaped otherwise.
+ if (this.innerObject.type == "text") {
+ return this.innerObject.getValues().join(",");
+ }
+ return this.valueAsIcalString;
+ },
+ set value(val) {
+ // Unescaped value for properties of TEXT, escaped otherwise.
+ if (this.innerObject.type == "text") {
+ this.innerObject.setValue(val);
+ return;
+ }
+ this.valueAsIcalString = val;
+ },
+
+ get valueAsIcalString() {
+ let propertyStr = this.innerObject.toICALString();
+ if (propertyStr.match(/:/g).length == 1) {
+ // For property containing only one colon, e.g. `GEO:latitude;longitude`,
+ // the left hand side must be the property name, the right hand side must
+ // be property value.
+ return propertyStr.slice(propertyStr.indexOf(":") + 1);
+ }
+ // For property containing many or no colons, retrieve the property value
+ // according to its type. An example is
+ // `ATTENDEE;MEMBER="mailto:foo@example.com": mailto:bar@example.com`
+ let type = this.innerObject.type;
+ return this.innerObject
+ .getValues()
+ .map(val => {
+ if (type == "text") {
+ return ICAL.stringify.value(val, type, ICAL.design.icalendar);
+ } else if (typeof val == "number" || typeof val == "string") {
+ return val;
+ } else if ("toICALString" in val) {
+ return val.toICALString();
+ }
+ return val.toString();
+ })
+ .join(",");
+ },
+ set valueAsIcalString(val) {
+ let mockLine = this.propertyName + ":" + val;
+ let prop = ICAL.Property.fromString(mockLine, ICAL.design.icalendar);
+
+ if (this.innerObject.isMultiValue) {
+ this.innerObject.setValues(prop.getValues());
+ } else {
+ this.innerObject.setValue(prop.getFirstValue());
+ }
+ },
+
+ get valueAsDatetime() {
+ let val = this.innerObject.getFirstValue();
+ let isIcalTime =
+ val && typeof val == "object" && "icalclass" in val && val.icalclass == "icaltime";
+ return isIcalTime ? new lazy.CalDateTime(val) : null;
+ },
+ set valueAsDatetime(rawval) {
+ unwrapSetter(
+ ICAL.Time,
+ rawval,
+ function (val) {
+ if (
+ val &&
+ val.zone &&
+ val.zone != ICAL.Timezone.utcTimezone &&
+ val.zone != ICAL.Timezone.localTimezone
+ ) {
+ this.innerObject.setParameter("TZID", val.zone.tzid);
+ if (this.parent) {
+ let tzref = wrapGetter(lazy.CalTimezone, val.zone);
+ this.parent.addTimezoneReference(tzref);
+ }
+ } else {
+ this.innerObject.removeParameter("TZID");
+ }
+ this.innerObject.setValue(val);
+ },
+ this
+ );
+ },
+
+ get propertyName() {
+ return this.innerObject.name.toUpperCase();
+ },
+
+ getParameter(name) {
+ // Unfortunately getting the "VALUE" parameter won't work, since in
+ // jCal it has been translated to the value type id.
+ if (name == "VALUE") {
+ let defaultType = this.innerObject.getDefaultType();
+ if (this.innerObject.type != defaultType) {
+ // Default type doesn't match object type, so we have a VALUE
+ // parameter
+ return this.innerObject.type.toUpperCase();
+ }
+ }
+
+ return this.innerObject.getParameter(name.toLowerCase());
+ },
+ setParameter(name, value) {
+ // Similar problems for setting the value parameter. Calendar code
+ // expects setting the value parameter to just change the value type
+ // and attempt to use the previous value as the new one. To do this in
+ // ICAL.js we need to save the value, reset the type and then try to
+ // set the value again.
+ if (name == "VALUE") {
+ let oldValues;
+ let type = this.innerObject.type;
+ let designSet = this.innerObject._designSet;
+
+ let wasMultiValue = this.innerObject.isMultiValue;
+ if (wasMultiValue) {
+ oldValues = this.innerObject.getValues();
+ } else {
+ let oldValue = this.innerObject.getFirstValue();
+ oldValues = oldValue ? [oldValue] : [];
+ }
+
+ this.innerObject.resetType(value.toLowerCase());
+ try {
+ oldValues = oldValues.map(oldValue => {
+ let strvalue = ICAL.stringify.value(oldValue.toString(), type, designSet);
+ return ICAL.parse._parseValue(strvalue, value, designSet);
+ });
+ } catch (e) {
+ // If there was an error reparsing the value, then just keep it
+ // empty.
+ oldValues = null;
+ }
+
+ if (oldValues && oldValues.length) {
+ if (wasMultiValue && this.innerObject.isMultiValue) {
+ this.innerObject.setValues(oldValues);
+ } else {
+ this.innerObject.setValue(oldValues.join(","));
+ }
+ }
+ } else {
+ this.innerObject.setParameter(name.toLowerCase(), value);
+ }
+ },
+ removeParameter(name) {
+ // Again, VALUE needs special handling. Removing the value parameter is
+ // kind of like resetting it to the default type. So find out the
+ // default type and then set the value parameter to it.
+ if (name == "VALUE") {
+ let propname = this.innerObject.name.toLowerCase();
+ if (propname in ICAL.design.icalendar.property) {
+ let details = ICAL.design.icalendar.property[propname];
+ if ("defaultType" in details) {
+ this.setParameter("VALUE", details.defaultType);
+ }
+ }
+ } else {
+ this.innerObject.removeParameter(name.toLowerCase());
+ }
+ },
+
+ clearXParameters() {
+ cal.WARN(
+ "calIICSService::clearXParameters is no longer implemented, please use removeParameter"
+ );
+ },
+
+ paramIterator: null,
+ getFirstParameterName() {
+ let innerObject = this.innerObject;
+ this.paramIterator = (function* () {
+ let defaultType = innerObject.getDefaultType();
+ if (defaultType != innerObject.type) {
+ yield "VALUE";
+ }
+
+ let paramNames = Object.keys(innerObject.jCal[1] || {});
+ for (let name of paramNames) {
+ yield name.toUpperCase();
+ }
+ })();
+ return this.getNextParameterName();
+ },
+
+ getNextParameterName() {
+ if (this.paramIterator) {
+ let next = this.paramIterator.next();
+ if (next.done) {
+ this.paramIterator = null;
+ }
+
+ return next.value;
+ }
+ return this.getFirstParameterName();
+ },
+};
+
+function calIcalComponent(innerObject) {
+ this.innerObject = innerObject || new ICAL.Component();
+ this.wrappedJSObject = this;
+ this.mReferencedZones = {};
+}
+
+calIcalComponent.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIIcalComponent"]),
+ classID: Components.ID("{51ac96fd-1279-4439-a85b-6947b37f4cea}"),
+
+ clone() {
+ return new calIcalComponent(new ICAL.Component(this.innerObject.toJSON()));
+ },
+
+ get parent() {
+ return wrapGetter(calIcalComponent, this.innerObject.parent);
+ },
+
+ get icalTimezone() {
+ return this.innerObject.name == "vtimezone" ? this.innerObject : null;
+ },
+ get icalComponent() {
+ return this.innerObject;
+ },
+ set icalComponent(val) {
+ this.innerObject = val;
+ },
+
+ componentIterator: null,
+ getFirstSubcomponent(kind) {
+ if (kind == "ANY") {
+ kind = null;
+ } else if (kind) {
+ kind = kind.toLowerCase();
+ }
+ let innerObject = this.innerObject;
+ this.componentIterator = (function* () {
+ let comps = innerObject.getAllSubcomponents(kind);
+ if (comps) {
+ for (let comp of comps) {
+ yield new calIcalComponent(comp);
+ }
+ }
+ })();
+ return this.getNextSubcomponent(kind);
+ },
+ getNextSubcomponent(kind) {
+ if (this.componentIterator) {
+ let next = this.componentIterator.next();
+ if (next.done) {
+ this.componentIterator = null;
+ }
+
+ return next.value;
+ }
+ return this.getFirstSubcomponent(kind);
+ },
+
+ get componentType() {
+ return this.innerObject.name.toUpperCase();
+ },
+
+ get uid() {
+ return this.innerObject.getFirstPropertyValue("uid");
+ },
+ set uid(val) {
+ this.innerObject.updatePropertyWithValue("uid", val);
+ },
+
+ get prodid() {
+ return this.innerObject.getFirstPropertyValue("prodid");
+ },
+ set prodid(val) {
+ this.innerObject.updatePropertyWithValue("prodid", val);
+ },
+
+ get version() {
+ return this.innerObject.getFirstPropertyValue("version");
+ },
+ set version(val) {
+ this.innerObject.updatePropertyWithValue("version", val);
+ },
+
+ get method() {
+ return this.innerObject.getFirstPropertyValue("method");
+ },
+ set method(val) {
+ this.innerObject.updatePropertyWithValue("method", val);
+ },
+
+ get status() {
+ return this.innerObject.getFirstPropertyValue("status");
+ },
+ set status(val) {
+ this.innerObject.updatePropertyWithValue("status", val);
+ },
+
+ get summary() {
+ return this.innerObject.getFirstPropertyValue("summary");
+ },
+ set summary(val) {
+ this.innerObject.updatePropertyWithValue("summary", val);
+ },
+
+ get description() {
+ return this.innerObject.getFirstPropertyValue("description");
+ },
+ set description(val) {
+ this.innerObject.updatePropertyWithValue("description", val);
+ },
+
+ get location() {
+ return this.innerObject.getFirstPropertyValue("location");
+ },
+ set location(val) {
+ this.innerObject.updatePropertyWithValue("location", val);
+ },
+
+ get categories() {
+ return this.innerObject.getFirstPropertyValue("categories");
+ },
+ set categories(val) {
+ this.innerObject.updatePropertyWithValue("categories", val);
+ },
+
+ get URL() {
+ return this.innerObject.getFirstPropertyValue("url");
+ },
+ set URL(val) {
+ this.innerObject.updatePropertyWithValue("url", val);
+ },
+
+ get priority() {
+ // If there is no value for this integer property, then we must return
+ // the designated INVALID_VALUE.
+ const INVALID_VALUE = Ci.calIIcalComponent.INVALID_VALUE;
+ let prop = this.innerObject.getFirstProperty("priority");
+ let val = prop ? prop.getFirstValue() : null;
+ return val === null ? INVALID_VALUE : val;
+ },
+ set priority(val) {
+ this.innerObject.updatePropertyWithValue("priority", val);
+ },
+
+ _setTimeAttr(propName, val) {
+ let prop = this.innerObject.updatePropertyWithValue(propName, val);
+ if (
+ val &&
+ val.zone &&
+ val.zone != ICAL.Timezone.utcTimezone &&
+ val.zone != ICAL.Timezone.localTimezone
+ ) {
+ prop.setParameter("TZID", val.zone.tzid);
+ this.addTimezoneReference(wrapGetter(lazy.CalTimezone, val.zone));
+ } else {
+ prop.removeParameter("TZID");
+ }
+ },
+
+ get startTime() {
+ return wrapGetter(lazy.CalDateTime, this.innerObject.getFirstPropertyValue("dtstart"));
+ },
+ set startTime(val) {
+ unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "dtstart"), this);
+ },
+
+ get endTime() {
+ return wrapGetter(lazy.CalDateTime, this.innerObject.getFirstPropertyValue("dtend"));
+ },
+ set endTime(val) {
+ unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "dtend"), this);
+ },
+
+ get duration() {
+ return wrapGetter(lazy.CalDuration, this.innerObject.getFirstPropertyValue("duration"));
+ },
+
+ get dueTime() {
+ return wrapGetter(lazy.CalDateTime, this.innerObject.getFirstPropertyValue("due"));
+ },
+ set dueTime(val) {
+ unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "due"), this);
+ },
+
+ get stampTime() {
+ return wrapGetter(lazy.CalDateTime, this.innerObject.getFirstPropertyValue("dtstamp"));
+ },
+ set stampTime(val) {
+ unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "dtstamp"), this);
+ },
+
+ get createdTime() {
+ return wrapGetter(lazy.CalDateTime, this.innerObject.getFirstPropertyValue("created"));
+ },
+ set createdTime(val) {
+ unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "created"), this);
+ },
+
+ get completedTime() {
+ return wrapGetter(lazy.CalDateTime, this.innerObject.getFirstPropertyValue("completed"));
+ },
+ set completedTime(val) {
+ unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "completed"), this);
+ },
+
+ get lastModified() {
+ return wrapGetter(lazy.CalDateTime, this.innerObject.getFirstPropertyValue("last-modified"));
+ },
+ set lastModified(val) {
+ unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "last-modified"), this);
+ },
+
+ get recurrenceId() {
+ return wrapGetter(lazy.CalDateTime, this.innerObject.getFirstPropertyValue("recurrence-id"));
+ },
+ set recurrenceId(val) {
+ unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "recurrence-id"), this);
+ },
+
+ serializeToICS() {
+ return this.innerObject.toString() + ICAL.newLineChar;
+ },
+ toString() {
+ return this.innerObject.toString();
+ },
+
+ addSubcomponent(comp) {
+ comp.getReferencedTimezones().forEach(this.addTimezoneReference, this);
+ let jscomp = unwrapSingle(ICAL.Component, comp);
+ this.innerObject.addSubcomponent(jscomp);
+ },
+
+ propertyIterator: null,
+ getFirstProperty(kind) {
+ if (kind == "ANY") {
+ kind = null;
+ } else if (kind) {
+ kind = kind.toLowerCase();
+ }
+ let innerObject = this.innerObject;
+ this.propertyIterator = (function* () {
+ let props = innerObject.getAllProperties(kind);
+ if (!props) {
+ return;
+ }
+ for (let prop of props) {
+ let hell = prop.getValues();
+ if (hell.length > 1) {
+ // Uh oh, multiple property values. Our code expects each as one
+ // property. I hate API incompatibility!
+ for (let devil of hell) {
+ let thisprop = new ICAL.Property(prop.toJSON(), prop.parent);
+ thisprop.removeAllValues();
+ thisprop.setValue(devil);
+ yield new CalIcalProperty(thisprop);
+ }
+ } else {
+ yield new CalIcalProperty(prop);
+ }
+ }
+ })();
+
+ return this.getNextProperty(kind);
+ },
+
+ getNextProperty(kind) {
+ if (this.propertyIterator) {
+ let next = this.propertyIterator.next();
+ if (next.done) {
+ this.propertyIterator = null;
+ }
+
+ return next.value;
+ }
+ return this.getFirstProperty(kind);
+ },
+
+ _getNextParentVCalendar() {
+ let vcalendar = this; // eslint-disable-line consistent-this
+ while (vcalendar && vcalendar.componentType != "VCALENDAR") {
+ vcalendar = vcalendar.parent;
+ }
+ return vcalendar || this;
+ },
+
+ addProperty(prop) {
+ try {
+ let datetime = prop.valueAsDatetime;
+ if (datetime && datetime.timezone) {
+ this._getNextParentVCalendar().addTimezoneReference(datetime.timezone);
+ }
+ } catch (e) {
+ // If there is an issue adding the timezone reference, don't make
+ // that break adding the property.
+ }
+
+ let jsprop = unwrapSingle(ICAL.Property, prop);
+ this.innerObject.addProperty(jsprop);
+ },
+
+ addTimezoneReference(timezone) {
+ if (timezone) {
+ if (!(timezone.tzid in this.mReferencedZones) && this.componentType == "VCALENDAR") {
+ let comp = timezone.icalComponent;
+ if (comp) {
+ this.addSubcomponent(comp);
+ }
+ }
+
+ this.mReferencedZones[timezone.tzid] = timezone;
+ }
+ },
+
+ getReferencedTimezones(aCount) {
+ return Object.keys(this.mReferencedZones).map(timezone => this.mReferencedZones[timezone]);
+ },
+
+ serializeToICSStream() {
+ let data = this.innerObject.toString();
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.setUTF8Data(data, data.length);
+ return stream;
+ },
+};
+
+function CalICSService() {
+ this.wrappedJSObject = this;
+}
+
+CalICSService.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIICSService"]),
+ classID: Components.ID("{c61cb903-4408-41b3-bc22-da0b27efdfe1}"),
+
+ parseICS(serialized) {
+ let comp = ICAL.parse(serialized);
+ return new calIcalComponent(new ICAL.Component(comp));
+ },
+
+ parseICSAsync(serialized, listener) {
+ let worker = new ChromeWorker("resource:///components/calICSService-worker.js");
+ worker.onmessage = function (event) {
+ let icalComp = new calIcalComponent(new ICAL.Component(event.data));
+ listener.onParsingComplete(Cr.OK, icalComp);
+ };
+ worker.onerror = function (event) {
+ cal.ERROR(`Parsing failed; ${event.message}. ICS data:\n${serialized}`);
+ listener.onParsingComplete(Cr.NS_ERROR_FAILURE, null);
+ };
+ worker.postMessage(serialized);
+ },
+
+ createIcalComponent(kind) {
+ return new calIcalComponent(new ICAL.Component(kind.toLowerCase()));
+ },
+
+ createIcalProperty(kind) {
+ return new CalIcalProperty(new ICAL.Property(kind.toLowerCase()));
+ },
+
+ createIcalPropertyFromString(str) {
+ return new CalIcalProperty(ICAL.Property.fromString(str.trim(), ICAL.design.icalendar));
+ },
+};
diff --git a/comm/calendar/base/src/CalIcsParser.jsm b/comm/calendar/base/src/CalIcsParser.jsm
new file mode 100644
index 0000000000..67ea32ba24
--- /dev/null
+++ b/comm/calendar/base/src/CalIcsParser.jsm
@@ -0,0 +1,334 @@
+/* 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 = ["CalIcsParser"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ CalEvent: "resource:///modules/CalEvent.jsm",
+ CalTodo: "resource:///modules/CalTodo.jsm",
+ CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm",
+});
+
+function CalIcsParser() {
+ this.wrappedJSObject = this;
+ this.mItems = [];
+ this.mParentlessItems = [];
+ this.mComponents = [];
+ this.mProperties = [];
+}
+CalIcsParser.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIIcsParser"]),
+ classID: Components.ID("{6fe88047-75b6-4874-80e8-5f5800f14984}"),
+
+ processIcalComponent(rootComp, aAsyncParsing) {
+ let calComp;
+ // libical returns the vcalendar component if there is just one vcalendar.
+ // If there are multiple vcalendars, it returns an xroot component, with
+ // vcalendar children. We need to handle both cases.
+ if (rootComp) {
+ if (rootComp.componentType == "VCALENDAR") {
+ calComp = rootComp;
+ } else {
+ calComp = rootComp.getFirstSubcomponent("VCALENDAR");
+ }
+ }
+
+ if (!calComp) {
+ let message = "Parser Error. Could not find 'VCALENDAR' component.\n";
+ try {
+ // we try to also provide the parsed component - if that fails due to an error in
+ // libical, we append the error message of the caught exception, which includes
+ // already a stack trace.
+ cal.ERROR(message + rootComp + "\n" + cal.STACK(10));
+ } catch (e) {
+ cal.ERROR(message + e);
+ }
+ }
+
+ let self = this;
+ let state = new parserState(this, aAsyncParsing);
+
+ while (calComp) {
+ // Get unknown properties from the VCALENDAR
+ for (let prop of cal.iterate.icalProperty(calComp)) {
+ if (prop.propertyName != "VERSION" && prop.propertyName != "PRODID") {
+ this.mProperties.push(prop);
+ }
+ }
+
+ let isGCal = /^-\/\/Google Inc\/\/Google Calendar /.test(calComp.prodid);
+ for (let subComp of cal.iterate.icalSubcomponent(calComp)) {
+ state.submit(subComp, isGCal);
+ }
+ calComp = rootComp.getNextSubcomponent("VCALENDAR");
+ }
+
+ // eslint-disable-next-line mozilla/use-returnValue
+ state.join(() => {
+ let fakedParents = {};
+ // tag "exceptions", i.e. items with rid:
+ for (let item of state.excItems) {
+ let parent = state.uid2parent[item.id];
+
+ if (!parent) {
+ // a parentless one, fake a master and override it's occurrence
+ parent = item.isEvent() ? new lazy.CalEvent() : new lazy.CalTodo();
+ parent.id = item.id;
+ parent.setProperty("DTSTART", item.recurrenceId);
+ parent.setProperty("X-MOZ-FAKED-MASTER", "1"); // this tag might be useful in the future
+ parent.recurrenceInfo = new lazy.CalRecurrenceInfo(parent);
+ fakedParents[item.id] = true;
+ state.uid2parent[item.id] = parent;
+ state.items.push(parent);
+ }
+ if (item.id in fakedParents) {
+ let rdate = cal.createRecurrenceDate();
+ rdate.date = item.recurrenceId;
+ parent.recurrenceInfo.appendRecurrenceItem(rdate);
+ // we'll keep the parentless-API until we switch over using itip-process for import (e.g. in dnd code)
+ self.mParentlessItems.push(item);
+ }
+
+ parent.recurrenceInfo.modifyException(item, true);
+ }
+
+ if (Object.keys(state.tzErrors).length > 0) {
+ // Use an alert rather than a prompt because problems may appear in
+ // remote subscribed calendars the user cannot change.
+ if (Cc["@mozilla.org/alerts-service;1"]) {
+ let notifier = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+ let title = cal.l10n.getCalString("TimezoneErrorsAlertTitle");
+ let text = cal.l10n.getCalString("TimezoneErrorsSeeConsole");
+ try {
+ let alert = Cc["@mozilla.org/alert-notification;1"].createInstance(
+ Ci.nsIAlertNotification
+ );
+ alert.init(title, "", title, text);
+ notifier.showAlert(alert);
+ } catch (e) {
+ // The notifier may not be available, e.g. on xpcshell tests
+ }
+ }
+ }
+
+ // We are done, push the items to the parser and notify the listener
+ self.mItems = self.mItems.concat(state.items);
+ self.mComponents = self.mComponents.concat(state.extraComponents);
+
+ if (aAsyncParsing) {
+ aAsyncParsing.onParsingComplete(Cr.NS_OK, self);
+ }
+ });
+ },
+
+ parseString(aICSString, aAsyncParsing) {
+ if (aAsyncParsing) {
+ let self = this;
+
+ // We are using two types of very similar listeners here:
+ // aAsyncParsing is a calIcsParsingListener that returns the ics
+ // parser containing the processed items.
+ // The listener passed to parseICSAsync is a calICsComponentParsingListener
+ // required by the ics service, that receives the parsed root component.
+ cal.icsService.parseICSAsync(aICSString, {
+ onParsingComplete(rc, rootComp) {
+ if (Components.isSuccessCode(rc)) {
+ self.processIcalComponent(rootComp, aAsyncParsing);
+ } else {
+ cal.ERROR("Error Parsing ICS: " + rc);
+ aAsyncParsing.onParsingComplete(rc, self);
+ }
+ },
+ });
+ } else {
+ try {
+ let icalComp = cal.icsService.parseICS(aICSString);
+ this.processIcalComponent(icalComp);
+ } catch (exc) {
+ cal.ERROR(exc.message + " when parsing\n" + aICSString);
+ }
+ }
+ },
+
+ parseFromStream(aStream, aAsyncParsing) {
+ // Read in the string. Note that it isn't a real string at this point,
+ // because likely, the file is utf8. The multibyte chars show up as multiple
+ // 'chars' in this string. So call it an array of octets for now.
+
+ let stringData = NetUtil.readInputStreamToString(aStream, aStream.available(), {
+ charset: "utf-8",
+ });
+ this.parseString(stringData, aAsyncParsing);
+ },
+
+ getItems() {
+ return this.mItems.concat([]);
+ },
+
+ getParentlessItems() {
+ return this.mParentlessItems.concat([]);
+ },
+
+ getProperties() {
+ return this.mProperties.concat([]);
+ },
+
+ getComponents() {
+ return this.mComponents.concat([]);
+ },
+};
+
+/**
+ * The parser state, which helps process ical components without clogging up the
+ * event queue.
+ *
+ * @param aParser The parser that is using this state
+ */
+function parserState(aParser, aListener) {
+ this.parser = aParser;
+ this.listener = aListener;
+
+ this.extraComponents = [];
+ this.items = [];
+ this.uid2parent = {};
+ this.excItems = [];
+ this.tzErrors = {};
+}
+
+parserState.prototype = {
+ parser: null,
+ joinFunc: null,
+ threadCount: 0,
+
+ extraComponents: null,
+ items: null,
+ uid2parent: null,
+ excItems: null,
+ tzErrors: null,
+ listener: null,
+
+ /**
+ * Checks if the timezones are missing and notifies the user via error console
+ *
+ * @param item The item to check for
+ * @param date The datetime object to check with
+ */
+ checkTimezone(item, date) {
+ function isPhantomTimezone(timezone) {
+ return !timezone.icalComponent && !timezone.isUTC && !timezone.isFloating;
+ }
+
+ if (date && isPhantomTimezone(date.timezone)) {
+ let tzid = date.timezone.tzid;
+ let hid = item.hashId + "#" + tzid;
+ if (!(hid in this.tzErrors)) {
+ // For now, publish errors to console and alert user.
+ // In future, maybe make them available through an interface method
+ // so this UI code can be removed from the parser, and caller can
+ // choose whether to alert, or show user the problem items and ask
+ // for fixes, or something else.
+ let msgArgs = [tzid, item.title, cal.dtz.formatter.formatDateTime(date)];
+ let msg = cal.l10n.getCalString("unknownTimezoneInItem", msgArgs);
+
+ cal.ERROR(msg + "\n" + item.icalString);
+ this.tzErrors[hid] = true;
+ }
+ }
+ },
+
+ /**
+ * Submit processing of a subcomponent to the event queue
+ *
+ * @param subComp The component to process
+ * @param isGCal If this is a Google Calendar invitation
+ */
+ submit(subComp, isGCal) {
+ let self = this;
+ let runner = {
+ run() {
+ let item = null;
+ switch (subComp.componentType) {
+ case "VEVENT":
+ item = new lazy.CalEvent();
+ item.icalComponent = subComp;
+ if (isGCal) {
+ cal.view.fixGoogleCalendarDescription(item);
+ }
+ self.checkTimezone(item, item.startDate);
+ self.checkTimezone(item, item.endDate);
+ break;
+ case "VTODO":
+ item = new lazy.CalTodo();
+ item.icalComponent = subComp;
+ self.checkTimezone(item, item.entryDate);
+ self.checkTimezone(item, item.dueDate);
+ // completed is defined to be in UTC
+ break;
+ case "VTIMEZONE":
+ // this should already be attached to the relevant
+ // events in the calendar, so there's no need to
+ // do anything with it here.
+ break;
+ default:
+ self.extraComponents.push(subComp);
+ break;
+ }
+
+ if (item) {
+ let rid = item.recurrenceId;
+ if (rid) {
+ self.excItems.push(item);
+ } else {
+ self.items.push(item);
+ if (item.recurrenceInfo) {
+ self.uid2parent[item.id] = item;
+ }
+ }
+ }
+ self.threadCount--;
+ self.checkCompletion();
+ },
+ };
+
+ this.threadCount++;
+ if (this.listener) {
+ // If we have a listener, we are doing this asynchronously. Go ahead
+ // and use the thread manager to dispatch the above runner
+ Services.tm.currentThread.dispatch(runner, Ci.nsIEventTarget.DISPATCH_NORMAL);
+ } else {
+ // No listener means synchonous. Just run the runner instead
+ runner.run();
+ }
+ },
+
+ /**
+ * Checks if the processing of all events has completed. If a join function
+ * has been set, this function is called.
+ *
+ * @returns True, if all tasks have been completed
+ */
+ checkCompletion() {
+ if (this.joinFunc && this.threadCount == 0) {
+ this.joinFunc();
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Sets a join function that is called when all tasks have been completed
+ *
+ * @param joinFunc The join function to call
+ */
+ join(joinFunc) {
+ this.joinFunc = joinFunc;
+ this.checkCompletion();
+ },
+};
diff --git a/comm/calendar/base/src/CalIcsSerializer.jsm b/comm/calendar/base/src/CalIcsSerializer.jsm
new file mode 100644
index 0000000000..ebdf84f9e7
--- /dev/null
+++ b/comm/calendar/base/src/CalIcsSerializer.jsm
@@ -0,0 +1,77 @@
+/* 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 = ["CalIcsSerializer"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+function CalIcsSerializer() {
+ this.wrappedJSObject = this;
+ this.mItems = [];
+ this.mProperties = [];
+ this.mComponents = [];
+}
+CalIcsSerializer.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIIcsSerializer"]),
+ classID: Components.ID("{207a6682-8ff1-4203-9160-729ec28c8766}"),
+
+ addItems(aItems) {
+ if (aItems.length > 0) {
+ this.mItems = this.mItems.concat(aItems);
+ }
+ },
+
+ addProperty(aProperty) {
+ this.mProperties.push(aProperty);
+ },
+
+ addComponent(aComponent) {
+ this.mComponents.push(aComponent);
+ },
+
+ serializeToString() {
+ let calComp = this.getIcalComponent();
+ return calComp.serializeToICS();
+ },
+
+ serializeToInputStream(aStream) {
+ let calComp = this.getIcalComponent();
+ return calComp.serializeToICSStream();
+ },
+
+ serializeToStream(aStream) {
+ let str = this.serializeToString();
+
+ // Convert the javascript string to an array of bytes, using the
+ // UTF8 encoder
+ let convStream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(
+ Ci.nsIConverterOutputStream
+ );
+ convStream.init(aStream, "UTF-8");
+
+ convStream.writeString(str);
+ convStream.close();
+ },
+
+ getIcalComponent() {
+ let calComp = cal.icsService.createIcalComponent("VCALENDAR");
+ cal.item.setStaticProps(calComp);
+
+ // xxx todo: think about that the below code doesn't clone the properties/components,
+ // thus ownership is moved to returned VCALENDAR...
+
+ for (let prop of this.mProperties) {
+ calComp.addProperty(prop);
+ }
+ for (let comp of this.mComponents) {
+ calComp.addSubcomponent(comp);
+ }
+
+ for (let item of cal.iterate.items(this.mItems)) {
+ calComp.addSubcomponent(item.icalComponent);
+ }
+
+ return calComp;
+ },
+};
diff --git a/comm/calendar/base/src/CalItipItem.jsm b/comm/calendar/base/src/CalItipItem.jsm
new file mode 100644
index 0000000000..aecf8671a1
--- /dev/null
+++ b/comm/calendar/base/src/CalItipItem.jsm
@@ -0,0 +1,212 @@
+/* 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 = ["CalItipItem"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+function CalItipItem() {
+ this.wrappedJSObject = this;
+ this.mCurrentItemIndex = 0;
+}
+CalItipItem.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIItipItem"]),
+ classID: Components.ID("{f41392ab-dcad-4bad-818f-b3d1631c4d93}"),
+
+ mIsInitialized: false,
+
+ mSender: null,
+ get sender() {
+ return this.mSender;
+ },
+ set sender(aValue) {
+ this.mSender = aValue;
+ },
+
+ mIsSend: false,
+ get isSend() {
+ return this.mIsSend;
+ },
+ set isSend(aValue) {
+ this.mIsSend = aValue;
+ },
+
+ mReceivedMethod: "REQUEST",
+ get receivedMethod() {
+ return this.mReceivedMethod;
+ },
+ set receivedMethod(aMethod) {
+ this.mReceivedMethod = aMethod.toUpperCase();
+ },
+
+ mResponseMethod: "REPLY",
+ get responseMethod() {
+ if (!this.mIsInitialized) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ return this.mResponseMethod;
+ },
+ set responseMethod(aMethod) {
+ this.mResponseMethod = aMethod.toUpperCase();
+ },
+
+ mAutoResponse: null,
+ get autoResponse() {
+ return this.mAutoResponse;
+ },
+ set autoResponse(aValue) {
+ this.mAutoResponse = aValue;
+ },
+
+ mTargetCalendar: null,
+ get targetCalendar() {
+ return this.mTargetCalendar;
+ },
+ set targetCalendar(aValue) {
+ this.mTargetCalendar = aValue;
+ },
+
+ mIdentity: null,
+ get identity() {
+ return this.mIdentity;
+ },
+ set identity(aValue) {
+ this.mIdentity = aValue;
+ },
+
+ mLocalStatus: null,
+ get localStatus() {
+ return this.mLocalStatus;
+ },
+ set localStatus(aValue) {
+ this.mLocalStatus = aValue;
+ },
+
+ mItemList: {},
+
+ init(aIcalString) {
+ let parser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser);
+ parser.parseString(aIcalString);
+
+ // - User specific alarms as well as X-MOZ- properties are irrelevant w.r.t. iTIP messages,
+ // should not be sent out and should not be relevant for incoming messages
+ // - faked master items
+ // so clean them out:
+
+ function cleanItem(item) {
+ // the following changes will bump LAST-MODIFIED/DTSTAMP, we want to preserve the originals:
+ let stamp = item.stampTime;
+ let lastModified = item.lastModifiedTime;
+ item.clearAlarms();
+ item.alarmLastAck = null;
+ item.deleteProperty("RECEIVED-SEQUENCE");
+ item.deleteProperty("RECEIVED-DTSTAMP");
+ for (let [name] of item.properties) {
+ if (name != "X-MOZ-FAKED-MASTER" && name.substr(0, "X-MOZ-".length) == "X-MOZ-") {
+ item.deleteProperty(name);
+ }
+ }
+ // never publish an organizer's RECEIVED params:
+ item.getAttendees().forEach(att => {
+ att.deleteProperty("RECEIVED-SEQUENCE");
+ att.deleteProperty("RECEIVED-DTSTAMP");
+ });
+
+ // according to RfC 6638, the following items must not be exposed in client side
+ // email scheduling messages, so let's remove it if present
+ let removeSchedulingParams = aCalUser => {
+ aCalUser.deleteProperty("SCHEDULE-AGENT");
+ aCalUser.deleteProperty("SCHEDULE-FORCE-SEND");
+ aCalUser.deleteProperty("SCHEDULE-STATUS");
+ };
+ item.getAttendees().forEach(removeSchedulingParams);
+ // we're graceful here as some PUBLISHed events may violate RfC by having no organizer
+ if (item.organizer) {
+ removeSchedulingParams(item.organizer);
+ }
+
+ item.setProperty("DTSTAMP", stamp);
+ item.setProperty("LAST-MODIFIED", lastModified); // need to be last to undirty the item
+ }
+
+ this.mItemList = [];
+ for (let item of cal.iterate.items(parser.getItems())) {
+ cleanItem(item);
+ // only push non-faked master items or
+ // the overridden instances of faked master items
+ // to the list:
+ if (item == item.parentItem) {
+ if (!item.hasProperty("X-MOZ-FAKED-MASTER")) {
+ this.mItemList.push(item);
+ }
+ } else if (item.parentItem.hasProperty("X-MOZ-FAKED-MASTER")) {
+ this.mItemList.push(item);
+ }
+ }
+
+ // We set both methods now for safety's sake. It's the ItipProcessor's
+ // responsibility to properly ascertain what the correct response
+ // method is (using user feedback, prefs, etc.) for the given
+ // receivedMethod. The RFC tells us to treat items without a METHOD
+ // as if they were METHOD:REQUEST.
+ for (let prop of parser.getProperties()) {
+ if (prop.propertyName == "METHOD") {
+ this.mReceivedMethod = prop.value;
+ this.mResponseMethod = prop.value;
+ break;
+ }
+ }
+
+ this.mIsInitialized = true;
+ },
+
+ clone() {
+ let newItem = new CalItipItem();
+ newItem.mItemList = this.mItemList.map(item => item.clone());
+ newItem.mReceivedMethod = this.mReceivedMethod;
+ newItem.mResponseMethod = this.mResponseMethod;
+ newItem.mAutoResponse = this.mAutoResponse;
+ newItem.mTargetCalendar = this.mTargetCalendar;
+ newItem.mIdentity = this.mIdentity;
+ newItem.mLocalStatus = this.mLocalStatus;
+ newItem.mSender = this.mSender;
+ newItem.mIsSend = this.mIsSend;
+ newItem.mIsInitialized = this.mIsInitialized;
+ return newItem;
+ },
+
+ /**
+ * This returns both the array and the number of items. An easy way to
+ * call it is: let itemArray = itipItem.getItemList();
+ */
+ getItemList() {
+ if (!this.mIsInitialized) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ return this.mItemList;
+ },
+
+ /**
+ * Note that this code forces the user to respond to all items in the same
+ * way, which is a current limitation of the spec.
+ */
+ setAttendeeStatus(aAttendeeId, aStatus) {
+ // Append "mailto:" to the attendee if it is missing it.
+ if (!aAttendeeId.match(/^mailto:/i)) {
+ aAttendeeId = "mailto:" + aAttendeeId;
+ }
+
+ for (let item of this.mItemList) {
+ let attendee = item.getAttendeeById(aAttendeeId);
+ if (attendee) {
+ // Replies should not have the RSVP property.
+ // XXX BUG 351589: workaround for updating an attendee
+ item.removeAttendee(attendee);
+ attendee = attendee.clone();
+ attendee.rsvp = null;
+ item.addAttendee(attendee);
+ }
+ }
+ },
+};
diff --git a/comm/calendar/base/src/CalMetronome.jsm b/comm/calendar/base/src/CalMetronome.jsm
new file mode 100644
index 0000000000..b675e5bc0a
--- /dev/null
+++ b/comm/calendar/base/src/CalMetronome.jsm
@@ -0,0 +1,142 @@
+/* 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 = ["CalMetronome"];
+
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+const { EventEmitter } = ChromeUtils.importESModule("resource://gre/modules/EventEmitter.sys.mjs");
+
+const MINUTE_IN_MS = 60000;
+const HOUR_IN_MS = 3600000;
+const DAY_IN_MS = 86400000;
+
+/**
+ * Keeps calendar UI/components in sync by ticking regularly. Fires a "minute"
+ * event every minute on the minute, an "hour" event on the hour, and a "day"
+ * event at midnight. Each event also fires if longer than the time period in
+ * question has elapsed since the last event, e.g. because the computer has
+ * been asleep.
+ *
+ * It automatically corrects clock skew: if a minute event is more than one
+ * second late, the time to the next event is recalculated and should fire a
+ * few milliseconds late at worst.
+ *
+ * @implements nsIObserver
+ * @implements EventEmitter
+ */
+var CalMetronome = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ /**
+ * The time when the minute event last fired, in milliseconds since the epoch.
+ *
+ * @type integer
+ */
+ _lastFireTime: 0,
+
+ /**
+ * The last minute for which the minute event fired.
+ *
+ * @type integer (0-59)
+ */
+ _lastMinute: -1,
+
+ /**
+ * The last hour for which the hour event fired.
+ *
+ * @type integer (0-23)
+ */
+ _lastHour: -1,
+
+ /**
+ * The last day of the week for which the day event fired.
+ *
+ * @type integer (0-7)
+ */
+ _lastDay: -1,
+
+ /**
+ * The timer running everything.
+ *
+ * @type nsITimer
+ */
+ _timer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
+
+ init() {
+ let now = new Date();
+ this._lastFireTime = now.valueOf();
+ this._lastHour = now.getHours();
+ this._lastDay = now.getDay();
+
+ EventEmitter.decorate(this);
+
+ Services.obs.addObserver(this, "wake_notification");
+ Services.obs.addObserver(this, "quit-application");
+ this._startNext();
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "wake_notification") {
+ cal.LOGverbose("[CalMetronome] Observed wake_notification");
+ this.notify();
+ } else if (topic == "quit-application") {
+ this._timer.cancel();
+ Services.obs.removeObserver(this, "wake_notification");
+ Services.obs.removeObserver(this, "quit-application");
+ }
+ },
+
+ _startNext() {
+ this._timer.cancel();
+
+ let now = new Date();
+ let next = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ now.getHours(),
+ now.getMinutes() + 1,
+ 0
+ );
+ cal.LOGverbose(`[CalMetronome] Scheduling one-off event in ${next - now}ms`);
+ this._timer.initWithCallback(this, next - now, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ _startRepeating() {
+ cal.LOGverbose(`[CalMetronome] Starting repeating events`);
+ this._timer.initWithCallback(this, MINUTE_IN_MS, Ci.nsITimer.TYPE_REPEATING_SLACK);
+ },
+
+ notify() {
+ let now = new Date();
+ let elapsedSinceLastFire = now.valueOf() - this._lastFireTime;
+ this._lastFireTime = now.valueOf();
+
+ let minute = now.getMinutes();
+ if (minute != this._lastMinute || elapsedSinceLastFire > MINUTE_IN_MS) {
+ this._lastMinute = minute;
+ this.emit("minute", now);
+ }
+
+ let hour = now.getHours();
+ if (hour != this._lastHour || elapsedSinceLastFire > HOUR_IN_MS) {
+ this._lastHour = hour;
+ this.emit("hour", now);
+ }
+
+ let day = now.getDay();
+ if (day != this._lastDay || elapsedSinceLastFire > DAY_IN_MS) {
+ this._lastDay = day;
+ this.emit("day", now);
+ }
+
+ let slack = now.getSeconds();
+ if (slack >= 1 && slack < 59) {
+ this._startNext();
+ } else if (this._timer.type == Ci.nsITimer.TYPE_ONE_SHOT) {
+ this._startRepeating();
+ }
+ },
+};
+CalMetronome.init();
diff --git a/comm/calendar/base/src/CalMimeConverter.jsm b/comm/calendar/base/src/CalMimeConverter.jsm
new file mode 100644
index 0000000000..be09812817
--- /dev/null
+++ b/comm/calendar/base/src/CalMimeConverter.jsm
@@ -0,0 +1,69 @@
+/* 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 = ["CalMimeConverter"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+function CalMimeConverter() {
+ this.wrappedJSObject = this;
+}
+
+CalMimeConverter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsISimpleMimeConverter"]),
+ classID: Components.ID("{c70acb08-464e-4e55-899d-b2c84c5409fa}"),
+
+ mailChannel: null,
+ uri: null,
+
+ convertToHTML(contentType, data) {
+ let parser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser);
+ parser.parseString(data);
+ let event = null;
+ for (let item of parser.getItems()) {
+ if (item.isEvent()) {
+ if (item.hasProperty("X-MOZ-FAKED-MASTER")) {
+ // if it's a faked master, take any overridden item to get a real occurrence:
+ let exc = item.recurrenceInfo.getExceptionFor(item.startDate);
+ cal.ASSERT(exc, "unexpected!");
+ if (exc) {
+ item = exc;
+ }
+ }
+ event = item;
+ break;
+ }
+ }
+ if (!event) {
+ return "";
+ }
+
+ let itipItem = Cc["@mozilla.org/calendar/itip-item;1"].createInstance(Ci.calIItipItem);
+ itipItem.init(data);
+
+ // this.uri is the message URL that we are processing.
+ if (this.uri) {
+ try {
+ let msgUrl = this.uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ itipItem.sender = msgUrl.mimeHeaders.extractHeader("From", false);
+ } catch (exc) {
+ // msgWindow is optional in some scenarios
+ // (e.g. gloda in action, throws NS_ERROR_INVALID_POINTER then)
+ }
+ }
+
+ // msgOverlay needs to be defined irrespectively of the existence of msgWindow to not break
+ // printing of invitation emails
+ let msgOverlay = "";
+
+ if (!Services.prefs.getBoolPref("calendar.itip.newInvitationDisplay")) {
+ let dom = cal.invitation.createInvitationOverlay(event, itipItem);
+ msgOverlay = cal.xml.serializeDOM(dom);
+ }
+
+ this.mailChannel.imipItem = itipItem;
+
+ return msgOverlay;
+ },
+};
diff --git a/comm/calendar/base/src/CalPeriod.jsm b/comm/calendar/base/src/CalPeriod.jsm
new file mode 100644
index 0000000000..7c3ca3c5e3
--- /dev/null
+++ b/comm/calendar/base/src/CalPeriod.jsm
@@ -0,0 +1,87 @@
+/* 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 = ["CalPeriod"];
+
+const { ICAL, unwrapSetter, wrapGetter } = ChromeUtils.import(
+ "resource:///modules/calendar/Ical.jsm"
+);
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(lazy, "CalDateTime", "resource:///modules/CalDateTime.jsm");
+ChromeUtils.defineModuleGetter(lazy, "CalDuration", "resource:///modules/CalDuration.jsm");
+
+function CalPeriod(innerObject) {
+ this.innerObject = innerObject || new ICAL.Period({});
+ this.wrappedJSObject = this;
+}
+
+CalPeriod.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIPeriod"]),
+ classID: Components.ID("{394a281f-7299-45f7-8b1f-cce21258972f}"),
+
+ isMutable: true,
+ innerObject: null,
+
+ get icalPeriod() {
+ return this.innerObject;
+ },
+ set icalPeriod(val) {
+ this.innerObject = val;
+ },
+
+ makeImmutable() {
+ this.isMutable = false;
+ },
+ clone() {
+ return new CalPeriod(this.innerObject.clone());
+ },
+
+ get start() {
+ return wrapGetter(lazy.CalDateTime, this.innerObject.start);
+ },
+ set start(rawval) {
+ unwrapSetter(
+ ICAL.Time,
+ rawval,
+ function (val) {
+ this.innerObject.start = val;
+ },
+ this
+ );
+ },
+
+ get end() {
+ return wrapGetter(lazy.CalDateTime, this.innerObject.getEnd());
+ },
+ set end(rawval) {
+ unwrapSetter(
+ ICAL.Time,
+ rawval,
+ function (val) {
+ if (this.innerObject.duration) {
+ this.innerObject.duration = null;
+ }
+ this.innerObject.end = val;
+ },
+ this
+ );
+ },
+
+ get duration() {
+ return wrapGetter(lazy.CalDuration, this.innerObject.getDuration());
+ },
+
+ get icalString() {
+ return this.innerObject.toICALString();
+ },
+ set icalString(val) {
+ let dates = ICAL.parse._parseValue(val, "period", ICAL.design.icalendar);
+ this.innerObject = ICAL.Period.fromString(dates.join("/"));
+ },
+
+ toString() {
+ return this.innerObject.toString();
+ },
+};
diff --git a/comm/calendar/base/src/CalProtocolHandler.jsm b/comm/calendar/base/src/CalProtocolHandler.jsm
new file mode 100644
index 0000000000..09632355a0
--- /dev/null
+++ b/comm/calendar/base/src/CalProtocolHandler.jsm
@@ -0,0 +1,63 @@
+/* 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 = ["CalProtocolHandlerWebcal", "CalProtocolHandlerWebcals"];
+
+/**
+ * CalProtocolHandler.
+ *
+ * @param {string} scheme - The scheme to init for (webcal, webcals).
+ * @implements {nsIProtocolHandler}
+ */
+class CalProtocolHandlerWebcal {
+ QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]);
+
+ scheme = "webcal";
+ httpScheme = "http";
+ httpPort = 80;
+
+ newURI(aSpec, anOriginalCharset, aBaseURI) {
+ return Cc["@mozilla.org/network/standard-url-mutator;1"]
+ .createInstance(Ci.nsIStandardURLMutator)
+ .init(Ci.nsIStandardURL.URLTYPE_STANDARD, this.httpPort, aSpec, anOriginalCharset, aBaseURI)
+ .finalize()
+ .QueryInterface(Ci.nsIStandardURL);
+ }
+
+ newChannel(aUri, aLoadInfo) {
+ let uri = aUri.mutate().setScheme(this.httpScheme).finalize();
+
+ let channel;
+ if (aLoadInfo) {
+ channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
+ } else {
+ channel = Services.io.newChannelFromURI(
+ uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ }
+ channel.originalURI = aUri;
+ return channel;
+ }
+
+ allowPort(aPort, aScheme) {
+ return false; // We are not overriding any special ports.
+ }
+}
+CalProtocolHandlerWebcal.prototype.classID = Components.ID(
+ "{1153c73a-39be-46aa-9ba9-656d188865ca}"
+);
+
+class CalProtocolHandlerWebcals extends CalProtocolHandlerWebcal {
+ scheme = "webcals";
+ httpScheme = "http";
+ httpPort = 443;
+}
+CalProtocolHandlerWebcals.prototype.classID = Components.ID(
+ "{bdf71224-365d-4493-856a-a7e74026f766}"
+);
diff --git a/comm/calendar/base/src/CalReadableStreamFactory.jsm b/comm/calendar/base/src/CalReadableStreamFactory.jsm
new file mode 100644
index 0000000000..c41d20dc8f
--- /dev/null
+++ b/comm/calendar/base/src/CalReadableStreamFactory.jsm
@@ -0,0 +1,314 @@
+/* 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/. */
+
+/* global ReadableStream */
+
+const EXPORTED_SYMBOLS = ["CalReadableStreamFactory"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+/**
+ * Function used to transform each value received from a stream.
+ *
+ * @callback MapStreamFunction
+ * @param {any} value
+ * @returns {Promise<any>|any}
+ */
+
+/**
+ * A version of UnderlyingSource that accepts a CalBoundedReadableStreamController
+ * as the controller argument.
+ *
+ * @typedef {object} CalBoundedReadableStreamUnderlyingSource
+ */
+
+/**
+ * Wrapper class for a ReadableStreamDefaultController that keeps track of how
+ * many items have been added to the queue before closing. This controller also
+ * buffers items to reduce the amount of times items are added to the queue.
+ */
+class CalBoundedReadableStreamController {
+ /**
+ * @type {ReadableStreamDefaultController}
+ */
+ _controller = null;
+
+ /**
+ * @type {CalBoundedReadableStreamUnderlyingSource}
+ */
+ _src = null;
+
+ /**
+ * @type {number}
+ */
+ _maxTotalItems;
+
+ /**
+ * @type {number}
+ */
+ _maxQueuedItems;
+
+ /**
+ * @type {calIItemBase[]}
+ */
+ _buffer = [];
+
+ /**
+ * @type {boolean}
+ */
+ _closed = false;
+
+ /**
+ * The count of items enqueued so far.
+ *
+ * @type {number}
+ */
+ count = 0;
+
+ /**
+ * @param {number} maxTotalItems
+ * @param {number} maxQueuedItems
+ * @param {CalBoundedReadableStreamUnderlyingSource} src
+ */
+ constructor(maxTotalItems, maxQueuedItems, src) {
+ this._maxTotalItems = maxTotalItems;
+ this._maxQueuedItems = maxQueuedItems;
+ this._src = src;
+ }
+
+ /**
+ * Indicates whether the maximum number of items have been added to the queue
+ * after which no more will be allowed.
+ *
+ * @type {number}
+ */
+ get maxTotalItemsReached() {
+ return this._maxTotalItems && this.count >= this._maxTotalItems;
+ }
+
+ /**
+ * Indicates whether the queue is full or not.
+ *
+ * @type {boolean}
+ */
+ get queueFull() {
+ return this._buffer.length >= this._maxQueuedItems;
+ }
+
+ /**
+ * Indicates how many more items can be enqueued based on the internal count
+ * kept.
+ *
+ * @type {number}
+ */
+ get remainingItemCount() {
+ return this._maxTotalItems ? this._maxTotalItems - this.count : Infinity;
+ }
+
+ /**
+ * Provides the value of the same property from the controller.
+ *
+ * @type {number}
+ */
+ get desiredSize() {
+ return this._controller.desiredSize;
+ }
+
+ /**
+ * Called by the ReadableStream to begin queueing items. This delegates to
+ * the provided underlying source.
+ *
+ * @param {ReadableStreamDefaultController} controller
+ */
+ async start(controller) {
+ this._controller = controller;
+ if (this._src.start) {
+ await this._src.start(this);
+ }
+ }
+
+ /**
+ * Called by the ReadableStream to receive more items when the queue has not
+ * been filled.
+ */
+ async pull() {
+ if (this._src.pull) {
+ await this._src.pull(this);
+ }
+ }
+
+ /**
+ * Called by the ReadableStream when reading has been cancelled.
+ *
+ * @param {string} reason
+ */
+ async cancel(reason) {
+ this._closed = true;
+ if (this._src.cancel) {
+ await this._src.cancel(reason);
+ }
+ }
+
+ /**
+ * Called by start() of the underlying source to add items to the queue. Items
+ * will only be added if maxTotalItemsReached returns false at which point
+ * the stream is automatically closed.
+ *
+ * @param {calIItemBase[]} items
+ */
+ enqueue(items) {
+ for (let item of items) {
+ if (this.queueFull) {
+ this.flush();
+ }
+ if (this.maxTotalItemsReached) {
+ return;
+ }
+ this._buffer.push(item);
+ }
+ this.flush();
+ }
+
+ /**
+ * Flushes the internal buffer if the number of buffered items have reached
+ * the threshold.
+ *
+ * @param {boolean} [force] - If true, will flush all items regardless of the
+ * threshold.
+ */
+ flush(force) {
+ if (force || this.queueFull) {
+ if (this.maxTotalItemsReached) {
+ return;
+ }
+ let buffer = this._buffer.slice(0, this.remainingItemCount);
+ this._controller.enqueue(buffer);
+ this.count += buffer.length;
+ this._buffer = [];
+ if (this.maxTotalItemsReached) {
+ this._controller.close();
+ }
+ }
+ }
+
+ /**
+ * Puts the stream in the error state.
+ *
+ * @param {Error} err
+ */
+ error(err) {
+ this._closed = true;
+ this._controller.error(err);
+ }
+
+ /**
+ * Closes the stream preventing any further items from being added to the queue.
+ */
+ close() {
+ if (!this._closed) {
+ if (this._buffer.length) {
+ this.flush(true);
+ }
+ this._closed = true;
+ this._controller.close();
+ }
+ }
+}
+
+/**
+ * Factory object for creating ReadableStreams of calIItemBase instances. This
+ * is used by the providers to satisfy getItems() calls from their respective
+ * backing stores.
+ */
+class CalReadableStreamFactory {
+ /**
+ * The default amount of items to queue before providing via the reader.
+ */
+ static defaultQueueSize = 10;
+
+ /**
+ * Creates a generic ReadableStream using the passed object as the
+ * UnderlyingSource. Use this method instead of creating streams directly
+ * until the API is more stable.
+ *
+ * @param {UnderlyingSource} src
+ *
+ * @returns {ReadableStream}
+ */
+ static createReadableStream(src) {
+ return new ReadableStream(src);
+ }
+
+ /**
+ * Creates a ReadableStream of calIItemBase items that tracks how many
+ * have been added to the queue. If maxTotalItems or more are enqueued, the
+ * stream will close ignoring further additions.
+ *
+ * @param {number} maxTotalItems
+ * @param {number} maxQueuedItems
+ * @param {UnderlyingSource} src
+ *
+ * @returns {ReadableStream<calIItemBase>}
+ */
+ static createBoundedReadableStream(maxTotalItems, maxQueuedItems, src) {
+ return new ReadableStream(
+ new CalBoundedReadableStreamController(maxTotalItems, maxQueuedItems, src)
+ );
+ }
+
+ /**
+ * Creates a ReadableStream that will provide no actual items.
+ *
+ * @returns {ReadableStream<calIItemBase>}
+ */
+ static createEmptyReadableStream() {
+ return new ReadableStream({
+ start(controller) {
+ controller.close();
+ },
+ });
+ }
+
+ /**
+ * Creates a ReadableStream that uses the one or more provided ReadableStreams
+ * for the source of its data. Each stream is read to completion one at a time
+ * and an error occurring while reading any will cause the main stream to end
+ * with in an error state.
+ *
+ * @param {ReadableStream[]} streams
+ * @returns {ReadableStream}
+ */
+ static createCombinedReadableStream(streams) {
+ return new ReadableStream({
+ async start(controller) {
+ for (let stream of streams) {
+ for await (let chunk of cal.iterate.streamValues(stream)) {
+ controller.enqueue(chunk);
+ }
+ }
+ controller.close();
+ },
+ });
+ }
+
+ /**
+ * Creates a ReadableStream from another stream where each chunk of the source
+ * stream is passed to a MapStreamFunction before enqueuing in the final stream.
+ *
+ * @param {ReadableStream}
+ * @param {MapStreamFunction}
+ *
+ * @returns {ReadableStream}
+ */
+ static createMappedReadableStream(stream, func) {
+ return new ReadableStream({
+ async start(controller) {
+ for await (let chunk of cal.iterate.streamValues(stream)) {
+ controller.enqueue(await func(chunk));
+ }
+ controller.close();
+ },
+ });
+ }
+}
diff --git a/comm/calendar/base/src/CalRecurrenceDate.jsm b/comm/calendar/base/src/CalRecurrenceDate.jsm
new file mode 100644
index 0000000000..cd43979a38
--- /dev/null
+++ b/comm/calendar/base/src/CalRecurrenceDate.jsm
@@ -0,0 +1,122 @@
+/* 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 = ["CalRecurrenceDate"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(lazy, "CalPeriod", "resource:///modules/CalPeriod.jsm");
+
+function CalRecurrenceDate() {
+ this.wrappedJSObject = this;
+}
+
+var calRecurrenceDateClassID = Components.ID("{806b6423-3aaa-4b26-afa3-de60563e9cec}");
+var calRecurrenceDateInterfaces = [Ci.calIRecurrenceItem, Ci.calIRecurrenceDate];
+CalRecurrenceDate.prototype = {
+ isMutable: true,
+
+ mIsNegative: false,
+ mDate: null,
+
+ classID: calRecurrenceDateClassID,
+ QueryInterface: cal.generateQI(["calIRecurrenceItem", "calIRecurrenceDate"]),
+ classInfo: cal.generateCI({
+ classID: calRecurrenceDateClassID,
+ contractID: "@mozilla.org/calendar/recurrence-date;1",
+ classDescription: "The date of an occurrence of a recurring item",
+ interfaces: calRecurrenceDateInterfaces,
+ }),
+
+ makeImmutable() {
+ this.isMutable = false;
+ },
+
+ ensureMutable() {
+ if (!this.isMutable) {
+ throw Components.Exception("", Cr.NS_ERROR_OBJECT_IS_IMMUTABLE);
+ }
+ },
+
+ clone() {
+ let other = new CalRecurrenceDate();
+ other.mDate = this.mDate ? this.mDate.clone() : null;
+ other.mIsNegative = this.mIsNegative;
+ return other;
+ },
+
+ get isNegative() {
+ return this.mIsNegative;
+ },
+ set isNegative(val) {
+ this.ensureMutable();
+ this.mIsNegative = val;
+ },
+
+ get isFinite() {
+ return true;
+ },
+
+ get date() {
+ return this.mDate;
+ },
+ set date(val) {
+ this.ensureMutable();
+ this.mDate = val;
+ },
+
+ getNextOccurrence(aStartTime, aOccurrenceTime) {
+ if (this.mDate && this.mDate.compare(aStartTime) > 0) {
+ return this.mDate;
+ }
+ return null;
+ },
+
+ getOccurrences(aStartTime, aRangeStart, aRangeEnd, aMaxCount) {
+ if (
+ this.mDate &&
+ this.mDate.compare(aRangeStart) >= 0 &&
+ (!aRangeEnd || this.mDate.compare(aRangeEnd) < 0)
+ ) {
+ return [this.mDate];
+ }
+ return [];
+ },
+
+ get icalString() {
+ let comp = this.icalProperty;
+ return comp ? comp.icalString : "";
+ },
+ set icalString(val) {
+ let prop = cal.icsService.createIcalPropertyFromString(val);
+ let propName = prop.propertyName;
+ if (propName != "RDATE" && propName != "EXDATE") {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ this.icalProperty = prop;
+ },
+
+ get icalProperty() {
+ let prop = cal.icsService.createIcalProperty(this.mIsNegative ? "EXDATE" : "RDATE");
+ prop.valueAsDatetime = this.mDate;
+ return prop;
+ },
+ set icalProperty(prop) {
+ if (prop.propertyName == "RDATE") {
+ this.mIsNegative = false;
+ if (prop.getParameter("VALUE") == "PERIOD") {
+ let period = new lazy.CalPeriod();
+ period.icalString = prop.valueAsIcalString;
+ this.mDate = period.start;
+ } else {
+ this.mDate = prop.valueAsDatetime;
+ }
+ } else if (prop.propertyName == "EXDATE") {
+ this.mIsNegative = true;
+ this.mDate = prop.valueAsDatetime;
+ }
+ },
+};
diff --git a/comm/calendar/base/src/CalRecurrenceInfo.jsm b/comm/calendar/base/src/CalRecurrenceInfo.jsm
new file mode 100644
index 0000000000..f24f210a30
--- /dev/null
+++ b/comm/calendar/base/src/CalRecurrenceInfo.jsm
@@ -0,0 +1,847 @@
+/* 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 = ["CalRecurrenceInfo"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+function getRidKey(date) {
+ if (!date) {
+ return null;
+ }
+ let timezone = date.timezone;
+ if (!timezone.isUTC && !timezone.isFloating) {
+ date = date.getInTimezone(cal.dtz.UTC);
+ }
+ return date.icalString;
+}
+
+/**
+ * Constructor for `calIRecurrenceInfo` objects.
+ *
+ * @class
+ * @implements {calIRecurrenceInfo}
+ * @param {calIItemBase} [item] - Optional calendar item for which this recurrence applies.
+ */
+function CalRecurrenceInfo(item) {
+ this.wrappedJSObject = this;
+ this.mRecurrenceItems = [];
+ this.mExceptionMap = {};
+ if (item) {
+ this.item = item;
+ }
+}
+
+CalRecurrenceInfo.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIRecurrenceInfo"]),
+ classID: Components.ID("{04027036-5884-4a30-b4af-f2cad79f6edf}"),
+
+ mImmutable: false,
+ mBaseItem: null,
+ mEndDate: null,
+ mRecurrenceItems: null,
+ mPositiveRules: null,
+ mNegativeRules: null,
+ mExceptionMap: null,
+
+ /**
+ * Helpers
+ */
+ ensureBaseItem() {
+ if (!this.mBaseItem) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+ },
+ ensureMutable() {
+ if (this.mImmutable) {
+ throw Components.Exception("", Cr.NS_ERROR_OBJECT_IS_IMMUTABLE);
+ }
+ },
+ ensureSortedRecurrenceRules() {
+ if (!this.mPositiveRules || !this.mNegativeRules) {
+ this.mPositiveRules = [];
+ this.mNegativeRules = [];
+ for (let ritem of this.mRecurrenceItems) {
+ if (ritem.isNegative) {
+ this.mNegativeRules.push(ritem);
+ } else {
+ this.mPositiveRules.push(ritem);
+ }
+ }
+ }
+ },
+
+ /**
+ * Mutability bits
+ */
+ get isMutable() {
+ return !this.mImmutable;
+ },
+ makeImmutable() {
+ if (this.mImmutable) {
+ return;
+ }
+
+ for (let ritem of this.mRecurrenceItems) {
+ if (ritem.isMutable) {
+ ritem.makeImmutable();
+ }
+ }
+
+ for (let ex in this.mExceptionMap) {
+ let item = this.mExceptionMap[ex];
+ if (item.isMutable) {
+ item.makeImmutable();
+ }
+ }
+
+ this.mImmutable = true;
+ },
+
+ clone() {
+ let cloned = new CalRecurrenceInfo();
+ cloned.mBaseItem = this.mBaseItem;
+
+ let clonedItems = [];
+ for (let ritem of this.mRecurrenceItems) {
+ clonedItems.push(ritem.clone());
+ }
+ cloned.mRecurrenceItems = clonedItems;
+
+ let clonedExceptions = {};
+ for (let exitem in this.mExceptionMap) {
+ clonedExceptions[exitem] = this.mExceptionMap[exitem].cloneShallow(this.mBaseItem);
+ }
+ cloned.mExceptionMap = clonedExceptions;
+
+ return cloned;
+ },
+
+ /*
+ * calIRecurrenceInfo
+ */
+ get item() {
+ return this.mBaseItem;
+ },
+ set item(value) {
+ this.ensureMutable();
+
+ value = cal.unwrapInstance(value);
+ this.mBaseItem = value;
+ // patch exception's parentItem:
+ for (let ex in this.mExceptionMap) {
+ let exitem = this.mExceptionMap[ex];
+ exitem.parentItem = value;
+ }
+ },
+
+ get isFinite() {
+ this.ensureBaseItem();
+
+ for (let ritem of this.mRecurrenceItems) {
+ if (!ritem.isFinite) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Get the item ending date (end date for an event, due date or entry date if available for a task).
+ *
+ * @param {calIEvent | calITodo} item - The item.
+ * @returns {calIDateTime | null} The ending date or null.
+ */
+ getItemEndingDate(item) {
+ if (item.isEvent()) {
+ if (item.endDate) {
+ return item.endDate;
+ }
+ } else if (item.isTodo()) {
+ // Due date must be considered since it is used when displaying the task in agenda view.
+ if (item.dueDate) {
+ return item.dueDate;
+ } else if (item.entryDate) {
+ return item.entryDate;
+ }
+ }
+ return null;
+ },
+
+ get recurrenceEndDate() {
+ // The lowest and highest possible values of a PRTime (64-bit integer) when in javascript,
+ // which stores them as floating-point values.
+ const MIN_PRTIME = -9223372036854775000;
+ const MAX_PRTIME = 9223372036854775000;
+
+ // If this object is mutable, skip this optimisation, so that we don't have to work out every
+ // possible modification and invalidate the cached value. Immutable objects are unlikely to
+ // exist for long enough to really benefit anyway.
+ if (this.isMutable) {
+ return MAX_PRTIME;
+ }
+
+ if (this.mEndDate === null) {
+ if (this.isFinite) {
+ this.mEndDate = MIN_PRTIME;
+ let lastOccurrence = this.getPreviousOccurrence(cal.createDateTime("99991231T235959Z"));
+ if (lastOccurrence) {
+ let endingDate = this.getItemEndingDate(lastOccurrence);
+ if (endingDate) {
+ this.mEndDate = endingDate.nativeTime;
+ }
+ }
+
+ // A modified occurrence may have a new ending date positioned after last occurrence one.
+ for (let rid in this.mExceptionMap) {
+ let item = this.mExceptionMap[rid];
+
+ let endingDate = this.getItemEndingDate(item);
+ if (endingDate && this.mEndDate < endingDate.nativeTime) {
+ this.mEndDate = endingDate.nativeTime;
+ }
+ }
+ } else {
+ this.mEndDate = MAX_PRTIME;
+ }
+ }
+
+ return this.mEndDate;
+ },
+
+ getRecurrenceItems() {
+ this.ensureBaseItem();
+
+ return this.mRecurrenceItems;
+ },
+
+ setRecurrenceItems(aItems) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+
+ // XXX should we clone these?
+ this.mRecurrenceItems = aItems;
+ this.mPositiveRules = null;
+ this.mNegativeRules = null;
+ },
+
+ countRecurrenceItems() {
+ this.ensureBaseItem();
+
+ return this.mRecurrenceItems.length;
+ },
+
+ getRecurrenceItemAt(aIndex) {
+ this.ensureBaseItem();
+
+ if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ return this.mRecurrenceItems[aIndex];
+ },
+
+ appendRecurrenceItem(aItem) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+ this.ensureSortedRecurrenceRules();
+
+ aItem = cal.unwrapInstance(aItem);
+ this.mRecurrenceItems.push(aItem);
+ if (aItem.isNegative) {
+ this.mNegativeRules.push(aItem);
+ } else {
+ this.mPositiveRules.push(aItem);
+ }
+ },
+
+ deleteRecurrenceItemAt(aIndex) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+
+ if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ if (this.mRecurrenceItems[aIndex].isNegative) {
+ this.mNegativeRules = null;
+ } else {
+ this.mPositiveRules = null;
+ }
+
+ this.mRecurrenceItems.splice(aIndex, 1);
+ },
+
+ deleteRecurrenceItem(aItem) {
+ aItem = cal.unwrapInstance(aItem);
+ let pos = this.mRecurrenceItems.indexOf(aItem);
+ if (pos > -1) {
+ this.deleteRecurrenceItemAt(pos);
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ },
+
+ insertRecurrenceItemAt(aItem, aIndex) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+ this.ensureSortedRecurrenceRules();
+
+ if (aIndex < 0 || aIndex > this.mRecurrenceItems.length) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ aItem = cal.unwrapInstance(aItem);
+ if (aItem.isNegative) {
+ this.mNegativeRules.push(aItem);
+ } else {
+ this.mPositiveRules.push(aItem);
+ }
+
+ this.mRecurrenceItems.splice(aIndex, 0, aItem);
+ },
+
+ clearRecurrenceItems() {
+ this.ensureBaseItem();
+ this.ensureMutable();
+
+ this.mRecurrenceItems = [];
+ this.mPositiveRules = [];
+ this.mNegativeRules = [];
+ },
+
+ /*
+ * calculations
+ */
+ getNextOccurrence(aTime) {
+ this.ensureBaseItem();
+ this.ensureSortedRecurrenceRules();
+
+ let startDate = this.mBaseItem.recurrenceStartDate;
+ let nextOccurrences = [];
+ let invalidOccurrences;
+ let negMap = {};
+ let minOccRid;
+
+ // Go through all negative rules to create a map of occurrences that
+ // should be skipped when going through occurrences.
+ for (let ritem of this.mNegativeRules) {
+ // TODO Infinite rules (i.e EXRULE) are not taken into account,
+ // because its very performance hungry and could potentially
+ // lead to a deadlock (i.e RRULE is canceled out by an EXRULE).
+ // This is ok for now, since EXRULE is deprecated anyway.
+ if (ritem.isFinite) {
+ // Get all occurrences starting at our recurrence start date.
+ // This is fine, since there will never be an EXDATE that
+ // occurs before the event started and its illegal to EXDATE an
+ // RDATE.
+ let rdates = ritem.getOccurrences(startDate, startDate, null, 0);
+ // Map all negative dates.
+ for (let rdate of rdates) {
+ negMap[getRidKey(rdate)] = true;
+ }
+ } else {
+ cal.WARN(
+ "Item '" +
+ this.mBaseItem.title +
+ "'" +
+ (this.mBaseItem.calendar ? " (" + this.mBaseItem.calendar.name + ")" : "") +
+ " has an infinite negative rule (EXRULE)"
+ );
+ }
+ }
+
+ let bailCounter = 0;
+ do {
+ invalidOccurrences = 0;
+ // Go through all positive rules and get the next recurrence id
+ // according to that rule. If for all rules the rid is "invalid",
+ // (i.e an EXDATE removed it, or an exception moved it somewhere
+ // else), then get the respective next rid.
+ //
+ // If in a loop at least one rid is valid (i.e not an exception, not
+ // an exdate, is after aTime), then remember the lowest one.
+ for (let i = 0; i < this.mPositiveRules.length; i++) {
+ let rDateInstance = cal.wrapInstance(this.mPositiveRules[i], Ci.calIRecurrenceDate);
+ let rRuleInstance = cal.wrapInstance(this.mPositiveRules[i], Ci.calIRecurrenceRule);
+ if (rDateInstance) {
+ // RDATEs are special. there is only one date in this rule,
+ // so no need to search anything.
+ let rdate = rDateInstance.date;
+ if (!nextOccurrences[i] && rdate.compare(aTime) > 0) {
+ // The RDATE falls into range, save it.
+ nextOccurrences[i] = rdate;
+ } else {
+ // The RDATE doesn't fall into range. This rule will
+ // always be invalid, since it can't give out a date.
+ nextOccurrences[i] = null;
+ invalidOccurrences++;
+ }
+ } else if (rRuleInstance) {
+ // RRULEs must not start searching before |startDate|, since
+ // the pattern is only valid afterwards. If an occurrence
+ // was found in a previous round, we can go ahead and start
+ // searching from that occurrence.
+ let searchStart = nextOccurrences[i] || startDate;
+
+ // Search for the next occurrence after aTime. If the last
+ // round was invalid, then in this round we need to search
+ // after nextOccurrences[i] to make sure getNextOccurrence()
+ // doesn't find the same occurrence again.
+ let searchDate =
+ nextOccurrences[i] && nextOccurrences[i].compare(aTime) > 0
+ ? nextOccurrences[i]
+ : aTime;
+
+ nextOccurrences[i] = rRuleInstance.getNextOccurrence(searchStart, searchDate);
+ }
+
+ // As decided in bug 734245, an EXDATE of type DATE shall also match a DTSTART of type DATE-TIME
+ let nextKey = getRidKey(nextOccurrences[i]);
+ let isInExceptionMap =
+ nextKey && (this.mExceptionMap[nextKey.substring(0, 8)] || this.mExceptionMap[nextKey]);
+ let isInNegMap = nextKey && (negMap[nextKey.substring(0, 8)] || negMap[nextKey]);
+ if (nextKey && (isInNegMap || isInExceptionMap)) {
+ // If the found recurrence id points to either an exception
+ // (will handle later) or an EXDATE, then nextOccurrences[i]
+ // is invalid and we might need to try again next round.
+ invalidOccurrences++;
+ } else if (nextOccurrences[i]) {
+ // We have a valid recurrence id (not an exception, not an
+ // EXDATE, falls into range). We only need to save the
+ // earliest occurrence after aTime (checking for aTime is
+ // not needed, since getNextOccurrence() above returns only
+ // occurrences after aTime).
+ if (!minOccRid || minOccRid.compare(nextOccurrences[i]) > 0) {
+ minOccRid = nextOccurrences[i];
+ }
+ }
+ }
+
+ // To make sure users don't just report bugs like "the application
+ // hangs", bail out after 100 runs. If this happens, it is most
+ // likely a bug.
+ if (bailCounter++ > 100) {
+ cal.ERROR("Could not find next occurrence after 100 runs!");
+ return null;
+ }
+
+ // We counted how many positive rules found out that their next
+ // candidate is invalid. If all rules produce invalid next
+ // occurrences, a second round is needed.
+ } while (invalidOccurrences == this.mPositiveRules.length);
+
+ // Since we need to compare occurrences by date, save the rid found
+ // above also as a date. This works out because above we skipped
+ // exceptions.
+ let minOccDate = minOccRid;
+
+ // Scan exceptions for any dates earlier than the above found
+ // minOccDate, but still after aTime.
+ for (let ex in this.mExceptionMap) {
+ let exc = this.mExceptionMap[ex];
+ let start = exc.recurrenceStartDate;
+ if (start.compare(aTime) > 0 && (!minOccDate || start.compare(minOccDate) <= 0)) {
+ // This exception is earlier, save its rid (for getting the
+ // occurrence later on) and its date (for comparing to other
+ // exceptions).
+ minOccRid = exc.recurrenceId;
+ minOccDate = start;
+ }
+ }
+
+ // If we found a recurrence id any time above, then return the
+ // occurrence for it.
+ return minOccRid ? this.getOccurrenceFor(minOccRid) : null;
+ },
+
+ getPreviousOccurrence(aTime) {
+ // HACK We never know how early an RDATE might be before the actual
+ // recurrence start. Since rangeStart cannot be null for recurrence
+ // items like calIRecurrenceRule, we need to work around by supplying a
+ // very early date. Again, this might have a high performance penalty.
+ let early = cal.createDateTime();
+ early.icalString = "00000101T000000Z";
+
+ let rids = this.calculateDates(early, aTime, 0);
+ // The returned dates are sorted, so the last one is a good
+ // candidate, if it exists.
+ return rids.length > 0 ? this.getOccurrenceFor(rids[rids.length - 1].id) : null;
+ },
+
+ // internal helper function;
+ calculateDates(aRangeStart, aRangeEnd, aMaxCount) {
+ this.ensureBaseItem();
+ this.ensureSortedRecurrenceRules();
+
+ // workaround for UTC- timezones
+ let rangeStart = cal.dtz.ensureDateTime(aRangeStart);
+ let rangeEnd = cal.dtz.ensureDateTime(aRangeEnd);
+
+ // If aRangeStart falls in the middle of an occurrence, libical will
+ // not return that occurrence when we go and ask for an
+ // icalrecur_iterator_new. This actually seems fairly rational, so
+ // instead of hacking libical, I'm going to move aRangeStart back far
+ // enough to make sure we get the occurrences we might miss.
+ let searchStart = rangeStart.clone();
+ let baseDuration = this.mBaseItem.duration;
+ if (baseDuration) {
+ let duration = baseDuration.clone();
+ duration.isNegative = true;
+ searchStart.addDuration(duration);
+ }
+
+ let startDate = this.mBaseItem.recurrenceStartDate;
+ if (startDate == null) {
+ // Todo created by other apps may have a saved recurrence but
+ // start and due dates disabled. Since no recurrenceStartDate,
+ // treat as undated task.
+ return [];
+ }
+
+ let dates = [];
+
+ // toss in exceptions first. Save a map of all exceptions ids, so we
+ // don't add the wrong occurrences later on.
+ let occurrenceMap = {};
+ for (let ex in this.mExceptionMap) {
+ let item = this.mExceptionMap[ex];
+ let occDate = cal.item.checkIfInRange(item, aRangeStart, aRangeEnd, true);
+ occurrenceMap[ex] = true;
+ if (occDate) {
+ dates.push({ id: item.recurrenceId, rstart: occDate });
+ }
+ }
+
+ // DTSTART/DUE is always part of the (positive) expanded set:
+ // DTSTART always equals RECURRENCE-ID for items expanded from RRULE
+ let baseOccDate = cal.item.checkIfInRange(this.mBaseItem, aRangeStart, aRangeEnd, true);
+ let baseOccDateKey = getRidKey(baseOccDate);
+ if (baseOccDate && !occurrenceMap[baseOccDateKey]) {
+ occurrenceMap[baseOccDateKey] = true;
+ dates.push({ id: baseOccDate, rstart: baseOccDate });
+ }
+
+ // if both range start and end are specified, we ask for all of the occurrences,
+ // to make sure we catch all possible exceptions. If aRangeEnd isn't specified,
+ // then we have to ask for aMaxCount, and hope for the best.
+ let maxCount;
+ if (rangeStart && rangeEnd) {
+ maxCount = 0;
+ } else {
+ maxCount = aMaxCount;
+ }
+
+ // Apply positive rules
+ for (let ritem of this.mPositiveRules) {
+ let cur_dates = ritem.getOccurrences(startDate, searchStart, rangeEnd, maxCount);
+ if (cur_dates.length == 0) {
+ continue;
+ }
+
+ // if positive, we just add these date to the existing set,
+ // but only if they're not already there
+
+ let index = 0;
+ let len = cur_dates.length;
+
+ // skip items before rangeStart due to searchStart libical hack:
+ if (rangeStart && baseDuration) {
+ for (; index < len; ++index) {
+ let date = cur_dates[index].clone();
+ date.addDuration(baseDuration);
+ if (rangeStart.compare(date) < 0) {
+ break;
+ }
+ }
+ }
+ for (; index < len; ++index) {
+ let date = cur_dates[index];
+ let dateKey = getRidKey(date);
+ if (occurrenceMap[dateKey]) {
+ // Don't add occurrences twice (i.e exception was
+ // already added before)
+ continue;
+ }
+ dates.push({ id: date, rstart: date });
+ occurrenceMap[dateKey] = true;
+ }
+ }
+
+ dates.sort((a, b) => a.rstart.compare(b.rstart));
+
+ // Apply negative rules
+ for (let ritem of this.mNegativeRules) {
+ let cur_dates = ritem.getOccurrences(startDate, searchStart, rangeEnd, maxCount);
+ if (cur_dates.length == 0) {
+ continue;
+ }
+
+ // XXX: i'm pretty sure negative dates can't really have exceptions
+ // (like, you can't make a date "real" by defining an RECURRENCE-ID which
+ // is an EXDATE, and then giving it a real DTSTART) -- so we don't
+ // check exceptions here
+ for (let dateToRemove of cur_dates) {
+ let dateToRemoveKey = getRidKey(dateToRemove);
+ if (dateToRemove.isDate) {
+ // As decided in bug 734245, an EXDATE of type DATE shall also match a DTSTART of type DATE-TIME
+ let toRemove = [];
+ for (let occurenceKey in occurrenceMap) {
+ if (occurrenceMap[occurenceKey] && occurenceKey.substring(0, 8) == dateToRemoveKey) {
+ dates = dates.filter(date => date.id.compare(dateToRemove) != 0);
+ toRemove.push(occurenceKey);
+ }
+ }
+ for (let i = 0; i < toRemove.length; i++) {
+ delete occurrenceMap[toRemove[i]];
+ }
+ } else if (occurrenceMap[dateToRemoveKey]) {
+ // TODO PERF Theoretically we could use occurrence map
+ // to construct the array of occurrences. Right now I'm
+ // just using the occurrence map to skip the filter
+ // action if the occurrence isn't there anyway.
+ dates = dates.filter(date => date.id.compare(dateToRemove) != 0);
+ delete occurrenceMap[dateToRemoveKey];
+ }
+ }
+ }
+
+ // The list was already sorted above, chop anything over aMaxCount, if
+ // specified.
+ if (aMaxCount && dates.length > aMaxCount) {
+ dates = dates.slice(0, aMaxCount);
+ }
+
+ return dates;
+ },
+
+ getOccurrenceDates(aRangeStart, aRangeEnd, aMaxCount) {
+ let dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount);
+ dates = dates.map(date => date.rstart);
+ return dates;
+ },
+
+ getOccurrences(aRangeStart, aRangeEnd, aMaxCount) {
+ let results = [];
+ let dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount);
+ if (dates.length) {
+ let count;
+ if (aMaxCount) {
+ count = Math.min(aMaxCount, dates.length);
+ } else {
+ count = dates.length;
+ }
+
+ for (let i = 0; i < count; i++) {
+ results.push(this.getOccurrenceFor(dates[i].id));
+ }
+ }
+ return results;
+ },
+
+ getOccurrenceFor(aRecurrenceId) {
+ let proxy = this.getExceptionFor(aRecurrenceId);
+ if (!proxy) {
+ return this.item.createProxy(aRecurrenceId);
+ }
+ return proxy;
+ },
+
+ removeOccurrenceAt(aRecurrenceId) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+
+ let rdate = cal.createRecurrenceDate();
+ rdate.isNegative = true;
+ rdate.date = aRecurrenceId.clone();
+
+ this.removeExceptionFor(rdate.date);
+
+ this.appendRecurrenceItem(rdate);
+ },
+
+ restoreOccurrenceAt(aRecurrenceId) {
+ this.ensureBaseItem();
+ this.ensureMutable();
+ this.ensureSortedRecurrenceRules();
+
+ for (let i = 0; i < this.mRecurrenceItems.length; i++) {
+ let rdate = cal.wrapInstance(this.mRecurrenceItems[i], Ci.calIRecurrenceDate);
+ if (rdate) {
+ if (rdate.isNegative && rdate.date.compare(aRecurrenceId) == 0) {
+ return this.deleteRecurrenceItemAt(i);
+ }
+ }
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ },
+
+ //
+ // exceptions
+ //
+
+ //
+ // Some notes:
+ //
+ // The way I read ICAL, RECURRENCE-ID is used to specify a
+ // particular instance of a recurring event, according to the
+ // RRULEs/RDATEs/etc. specified in the base event. If one of
+ // these is to be changed ("an exception"), then it can be
+ // referenced via the UID of the original event, and a
+ // RECURRENCE-ID of the start time of the instance to change.
+ // This, to me, means that an event where one of the instances has
+ // changed to a different time has a RECURRENCE-ID of the original
+ // start time, and a DTSTART/DTEND representing the new time.
+ //
+ // ITIP, however, seems to want something different -- you're
+ // supposed to use UID/RECURRENCE-ID to select from the current
+ // set of occurrences of an event. If you change the DTSTART for
+ // an instance, you're supposed to use the old (original) DTSTART
+ // as the RECURRENCE-ID, and put the new time as the DTSTART.
+ // However, after that change, to refer to that instance in the
+ // future, you have to use the modified DTSTART as the
+ // RECURRENCE-ID. This madness is described in ITIP end of
+ // section 3.7.1.
+ //
+ // This implementation does the first approach (RECURRENCE-ID will
+ // never change even if DTSTART for that instance changes), which
+ // I think is the right thing to do for CalDAV; I don't know what
+ // we'll do for incoming ITIP events though.
+ //
+ modifyException(anItem, aTakeOverOwnership) {
+ this.ensureBaseItem();
+
+ anItem = cal.unwrapInstance(anItem);
+
+ if (
+ anItem.parentItem.calendar != this.mBaseItem.calendar &&
+ anItem.parentItem.id != this.mBaseItem.id
+ ) {
+ cal.ERROR("recurrenceInfo::addException: item parentItem != this.mBaseItem (calendar/id)!");
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ if (anItem.recurrenceId == null) {
+ cal.ERROR("recurrenceInfo::addException: item with null recurrenceId!");
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let itemtoadd;
+ if (aTakeOverOwnership && anItem.isMutable) {
+ itemtoadd = anItem;
+ itemtoadd.parentItem = this.mBaseItem;
+ } else {
+ itemtoadd = anItem.cloneShallow(this.mBaseItem);
+ }
+
+ // we're going to assume that the recurrenceId is valid here,
+ // because presumably the item came from one of our functions
+
+ let exKey = getRidKey(itemtoadd.recurrenceId);
+ this.mExceptionMap[exKey] = itemtoadd;
+ },
+
+ getExceptionFor(aRecurrenceId) {
+ this.ensureBaseItem();
+ // Interface calIRecurrenceInfo specifies result be null if not found.
+ // To avoid strict "reference to undefined property" warning, appending
+ // "|| null" gives explicit result in case where property undefined
+ // (or false, 0, null, or "", but here it should never be those values).
+ return this.mExceptionMap[getRidKey(aRecurrenceId)] || null;
+ },
+
+ removeExceptionFor(aRecurrenceId) {
+ this.ensureBaseItem();
+ delete this.mExceptionMap[getRidKey(aRecurrenceId)];
+ },
+
+ getExceptionIds() {
+ this.ensureBaseItem();
+
+ let ids = [];
+ for (let ex in this.mExceptionMap) {
+ let item = this.mExceptionMap[ex];
+ ids.push(item.recurrenceId);
+ }
+ return ids;
+ },
+
+ // changing the startdate of an item needs to take exceptions into account.
+ // in case we're about to modify a parentItem (aka 'folded' item), we need
+ // to modify the recurrenceId's of all possibly existing exceptions as well.
+ onStartDateChange(aNewStartTime, aOldStartTime) {
+ // passing null for the new starttime would indicate an error condition,
+ // since having a recurrence without a starttime is invalid.
+ cal.ASSERT(aNewStartTime, "invalid arg!", true);
+
+ // no need to check for changes if there's no previous starttime.
+ if (!aOldStartTime) {
+ return;
+ }
+
+ // convert both dates to UTC since subtractDate is not timezone aware.
+ let timeDiff = aNewStartTime
+ .getInTimezone(cal.dtz.UTC)
+ .subtractDate(aOldStartTime.getInTimezone(cal.dtz.UTC));
+
+ let rdates = {};
+
+ // take RDATE's and EXDATE's into account.
+ const kCalIRecurrenceDate = Ci.calIRecurrenceDate;
+ let ritems = this.getRecurrenceItems();
+ for (let ritem of ritems) {
+ let rDateInstance = cal.wrapInstance(ritem, kCalIRecurrenceDate);
+ let rRuleInstance = cal.wrapInstance(ritem, Ci.calIRecurrenceRule);
+ if (rDateInstance) {
+ ritem = rDateInstance;
+ let date = ritem.date;
+ date.addDuration(timeDiff);
+ if (!ritem.isNegative) {
+ rdates[getRidKey(date)] = date;
+ }
+ ritem.date = date;
+ } else if (rRuleInstance) {
+ ritem = rRuleInstance;
+ if (!ritem.isByCount) {
+ let untilDate = ritem.untilDate;
+ if (untilDate) {
+ untilDate.addDuration(timeDiff);
+ ritem.untilDate = untilDate;
+ }
+ }
+ }
+ }
+
+ let startTimezone = aNewStartTime.timezone;
+ let modifiedExceptions = [];
+ for (let exid of this.getExceptionIds()) {
+ let ex = this.getExceptionFor(exid);
+ if (ex) {
+ ex = ex.clone();
+ // track RECURRENCE-IDs in DTSTART's or RDATE's timezone,
+ // otherwise those won't match any longer w.r.t DST:
+ let rid = ex.recurrenceId;
+ let rdate = rdates[getRidKey(rid)];
+ rid = rid.getInTimezone(rdate ? rdate.timezone : startTimezone);
+ rid.addDuration(timeDiff);
+ ex.recurrenceId = rid;
+ cal.item.shiftOffset(ex, timeDiff);
+ modifiedExceptions.push(ex);
+ this.removeExceptionFor(exid);
+ }
+ }
+ for (let modifiedEx of modifiedExceptions) {
+ this.modifyException(modifiedEx, true);
+ }
+ },
+
+ onIdChange(aNewId) {
+ // patch all overridden items' id:
+ for (let ex in this.mExceptionMap) {
+ let item = this.mExceptionMap[ex];
+ item.id = aNewId;
+ }
+ },
+};
diff --git a/comm/calendar/base/src/CalRecurrenceRule.jsm b/comm/calendar/base/src/CalRecurrenceRule.jsm
new file mode 100644
index 0000000000..7d713f2ecf
--- /dev/null
+++ b/comm/calendar/base/src/CalRecurrenceRule.jsm
@@ -0,0 +1,268 @@
+/* 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 = ["CalRecurrenceRule"];
+
+const { ICAL, unwrapSetter, unwrapSingle, wrapGetter } = ChromeUtils.import(
+ "resource:///modules/calendar/Ical.jsm"
+);
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(lazy, "CalDateTime", "resource:///modules/CalDateTime.jsm");
+ChromeUtils.defineModuleGetter(lazy, "CalIcalProperty", "resource:///modules/CalICSService.jsm");
+
+function CalRecurrenceRule(innerObject) {
+ this.innerObject = innerObject || new ICAL.Recur();
+ this.wrappedJSObject = this;
+}
+
+var calRecurrenceRuleInterfaces = [Ci.calIRecurrenceRule, Ci.calIRecurrenceItem];
+var calRecurrenceRuleClassID = Components.ID("{df19281a-5389-4146-b941-798cb93a7f0d}");
+CalRecurrenceRule.prototype = {
+ QueryInterface: cal.generateQI(["calIRecurrenceRule", "calIRecurrenceItem"]),
+ classID: calRecurrenceRuleClassID,
+ classInfo: cal.generateCI({
+ contractID: "@mozilla.org/calendar/recurrence-rule;1",
+ classDescription: "Calendar Recurrence Rule",
+ classID: calRecurrenceRuleClassID,
+ interfaces: calRecurrenceRuleInterfaces,
+ }),
+
+ innerObject: null,
+
+ isMutable: true,
+ makeImmutable() {
+ this.isMutable = false;
+ },
+ ensureMutable() {
+ if (!this.isMutable) {
+ throw Components.Exception("", Cr.NS_ERROR_OBJECT_IS_IMMUTABLE);
+ }
+ },
+ clone() {
+ return new CalRecurrenceRule(new ICAL.Recur(this.innerObject));
+ },
+
+ isNegative: false, // We don't support EXRULE anymore
+ get isFinite() {
+ return this.innerObject.isFinite();
+ },
+
+ /**
+ * Tests whether the "FREQ" value for this rule is supported or not. A warning
+ * is logged if an unsupported value ("SECONDLY"|"MINUTELY") is encountered.
+ *
+ * @returns {boolean}
+ */
+ freqSupported() {
+ let { freq } = this.innerObject;
+ if (freq == "SECONDLY" || freq == "MINUTELY") {
+ cal.WARN(
+ `The frequency value "${freq}" is currently not supported. No occurrences will be generated.`
+ );
+ return false;
+ }
+ return true;
+ },
+
+ getNextOccurrence(aStartTime, aRecId) {
+ if (!this.freqSupported()) {
+ return null;
+ }
+ aStartTime = unwrapSingle(ICAL.Time, aStartTime);
+ aRecId = unwrapSingle(ICAL.Time, aRecId);
+ return wrapGetter(lazy.CalDateTime, this.innerObject.getNextOccurrence(aStartTime, aRecId));
+ },
+
+ getOccurrences(aStartTime, aRangeStart, aRangeEnd, aMaxCount) {
+ if (!this.freqSupported()) {
+ return [];
+ }
+ aStartTime = unwrapSingle(ICAL.Time, aStartTime);
+ aRangeStart = unwrapSingle(ICAL.Time, aRangeStart);
+ aRangeEnd = unwrapSingle(ICAL.Time, aRangeEnd);
+
+ if (!aMaxCount && !aRangeEnd && this.count == 0 && this.until == null) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let occurrences = [];
+ let rangeStart = aRangeStart.clone();
+ rangeStart.isDate = false;
+
+ let dtend = null;
+
+ if (aRangeEnd) {
+ dtend = aRangeEnd.clone();
+ dtend.isDate = false;
+
+ // If the start of the recurrence is past the end, we have no dates
+ if (aStartTime.compare(dtend) >= 0) {
+ return [];
+ }
+ }
+
+ let iter = this.innerObject.iterator(aStartTime);
+
+ for (let next = iter.next(); next; next = iter.next()) {
+ let dtNext = next.clone();
+ dtNext.isDate = false;
+
+ if (dtNext.compare(rangeStart) < 0) {
+ continue;
+ }
+
+ if (dtend && dtNext.compare(dtend) >= 0) {
+ break;
+ }
+
+ next = next.clone();
+
+ if (aStartTime.zone) {
+ next.zone = aStartTime.zone;
+ }
+
+ occurrences.push(new lazy.CalDateTime(next));
+
+ if (aMaxCount && occurrences.length >= aMaxCount) {
+ break;
+ }
+ }
+
+ return occurrences;
+ },
+
+ get icalString() {
+ return "RRULE:" + this.innerObject.toString() + ICAL.newLineChar;
+ },
+ set icalString(val) {
+ this.ensureMutable();
+ this.innerObject = ICAL.Recur.fromString(val.replace(/^RRULE:/i, ""));
+ },
+
+ get icalProperty() {
+ let prop = new ICAL.Property("rrule");
+ prop.setValue(this.innerObject);
+ return new lazy.CalIcalProperty(prop);
+ },
+ set icalProperty(rawval) {
+ this.ensureMutable();
+ unwrapSetter(
+ ICAL.Property,
+ rawval,
+ function (val) {
+ this.innerObject = val.getFirstValue();
+ },
+ this
+ );
+ },
+
+ get type() {
+ return this.innerObject.freq;
+ },
+ set type(val) {
+ this.ensureMutable();
+ this.innerObject.freq = val;
+ },
+
+ get interval() {
+ return this.innerObject.interval;
+ },
+ set interval(val) {
+ this.ensureMutable();
+ this.innerObject.interval = val;
+ },
+
+ get count() {
+ if (!this.isByCount) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ return this.innerObject.count || -1;
+ },
+ set count(val) {
+ this.ensureMutable();
+ this.innerObject.count = val && val > 0 ? val : null;
+ },
+
+ get untilDate() {
+ if (this.innerObject.until) {
+ return new lazy.CalDateTime(this.innerObject.until);
+ }
+ return null;
+ },
+ set untilDate(rawval) {
+ this.ensureMutable();
+ unwrapSetter(
+ ICAL.Time,
+ rawval,
+ function (val) {
+ if (
+ val.timezone != ICAL.Timezone.utcTimezone &&
+ val.timezone != ICAL.Timezone.localTimezone
+ ) {
+ val = val.convertToZone(ICAL.Timezone.utcTimezone);
+ }
+
+ this.innerObject.until = val;
+ },
+ this
+ );
+ },
+
+ get isByCount() {
+ return this.innerObject.isByCount();
+ },
+
+ get weekStart() {
+ return this.innerObject.wkst - 1;
+ },
+ set weekStart(val) {
+ this.ensureMutable();
+ this.innerObject.wkst = val + 1;
+ },
+
+ getComponent(aType) {
+ let values = this.innerObject.getComponent(aType);
+ if (aType == "BYDAY") {
+ // BYDAY values are alphanumeric: SU, MO, TU, etc..
+ for (let i = 0; i < values.length; i++) {
+ let match = /^([+-])?(5[0-3]|[1-4][0-9]|[1-9])?(SU|MO|TU|WE|TH|FR|SA)$/.exec(values[i]);
+ if (!match) {
+ cal.ERROR("Malformed BYDAY rule\n" + cal.STACK(10));
+ return [];
+ }
+ values[i] = ICAL.Recur.icalDayToNumericDay(match[3]);
+ if (match[2]) {
+ // match[2] is the week number for this value.
+ values[i] += 8 * match[2];
+ }
+ if (match[1] == "-") {
+ // Week numbers are counted back from the end of the period.
+ values[i] *= -1;
+ }
+ }
+ }
+
+ return values;
+ },
+
+ setComponent(aType, aValues) {
+ let values = aValues;
+ if (aType == "BYDAY") {
+ // BYDAY values are alphanumeric: SU, MO, TU, etc..
+ for (let i = 0; i < values.length; i++) {
+ let absValue = Math.abs(values[i]);
+ if (absValue > 7) {
+ let ordinal = Math.trunc(values[i] / 8);
+ let day = ICAL.Recur.numericDayToIcalDay(absValue % 8);
+ values[i] = ordinal + day;
+ } else {
+ values[i] = ICAL.Recur.numericDayToIcalDay(values[i]);
+ }
+ }
+ }
+ this.innerObject.setComponent(aType, values);
+ },
+};
diff --git a/comm/calendar/base/src/CalRelation.jsm b/comm/calendar/base/src/CalRelation.jsm
new file mode 100644
index 0000000000..78327d962c
--- /dev/null
+++ b/comm/calendar/base/src/CalRelation.jsm
@@ -0,0 +1,125 @@
+/* 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 = ["CalRelation"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+/**
+ * Constructor for `calIRelation` objects.
+ *
+ * @class
+ * @implements {calIRelation}
+ * @param {string} [icalString] - Optional iCal string for initializing existing relations.
+ */
+function CalRelation(icalString) {
+ this.wrappedJSObject = this;
+ this.mProperties = new Map();
+ if (icalString) {
+ this.icalString = icalString;
+ }
+}
+CalRelation.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIRelation"]),
+ classID: Components.ID("{76810fae-abad-4019-917a-08e95d5bbd68}"),
+
+ mType: null,
+ mId: null,
+
+ /**
+ * @see calIRelation
+ */
+
+ get relType() {
+ return this.mType;
+ },
+ set relType(aType) {
+ this.mType = aType;
+ },
+
+ get relId() {
+ return this.mId;
+ },
+ set relId(aRelId) {
+ this.mId = aRelId;
+ },
+
+ get icalProperty() {
+ let icalatt = cal.icsService.createIcalProperty("RELATED-TO");
+ if (this.mId) {
+ icalatt.value = this.mId;
+ }
+
+ if (this.mType) {
+ icalatt.setParameter("RELTYPE", this.mType);
+ }
+
+ for (let [key, value] of this.mProperties.entries()) {
+ try {
+ icalatt.setParameter(key, value);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG("Warning: Invalid relation property value " + key + "=" + value);
+ } else {
+ throw e;
+ }
+ }
+ }
+ return icalatt;
+ },
+
+ set icalProperty(attProp) {
+ // Reset the property bag for the parameters, it will be re-initialized
+ // from the ical property.
+ this.mProperties = new Map();
+
+ if (attProp.value) {
+ this.mId = attProp.value;
+ }
+ for (let [name, value] of cal.iterate.icalParameter(attProp)) {
+ if (name == "RELTYPE") {
+ this.mType = value;
+ continue;
+ }
+
+ this.setParameter(name, value);
+ }
+ },
+
+ get icalString() {
+ let comp = this.icalProperty;
+ return comp ? comp.icalString : "";
+ },
+ set icalString(val) {
+ let prop = cal.icsService.createIcalPropertyFromString(val);
+ if (prop.propertyName != "RELATED-TO") {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ this.icalProperty = prop;
+ },
+
+ getParameter(aName) {
+ return this.mProperties.get(aName);
+ },
+
+ setParameter(aName, aValue) {
+ return this.mProperties.set(aName, aValue);
+ },
+
+ deleteParameter(aName) {
+ return this.mProperties.delete(aName);
+ },
+
+ clone() {
+ let newRelation = new CalRelation();
+ newRelation.mId = this.mId;
+ newRelation.mType = this.mType;
+ for (let [name, value] of this.mProperties.entries()) {
+ newRelation.mProperties.set(name, value);
+ }
+ return newRelation;
+ },
+};
diff --git a/comm/calendar/base/src/CalStartupService.jsm b/comm/calendar/base/src/CalStartupService.jsm
new file mode 100644
index 0000000000..04b2a53032
--- /dev/null
+++ b/comm/calendar/base/src/CalStartupService.jsm
@@ -0,0 +1,124 @@
+/* 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 = ["CalStartupService"];
+
+/**
+ * Helper function to asynchronously call a certain method on the objects passed
+ * in 'services' in order (i.e wait until the first completes before calling the
+ * second
+ *
+ * @param method The method name to call. Usually startup/shutdown.
+ * @param services The array of service objects to call on.
+ */
+function callOrderedServices(method, services) {
+ let service = services.shift();
+ if (service) {
+ service[method]({
+ onResult() {
+ callOrderedServices(method, services);
+ },
+ });
+ }
+}
+
+function CalStartupService() {
+ this.wrappedJSObject = this;
+ this.setupObservers();
+}
+
+CalStartupService.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ classID: Components.ID("{2547331f-34c0-4a4b-b93c-b503538ba6d6}"),
+
+ // Startup Service Methods
+
+ /**
+ * Sets up the needed observers for noticing startup/shutdown
+ */
+ setupObservers() {
+ Services.obs.addObserver(this, "profile-after-change");
+ Services.obs.addObserver(this, "profile-before-change");
+ Services.obs.addObserver(this, "xpcom-shutdown");
+ },
+
+ started: false,
+
+ /**
+ * Gets the startup order of services. This is an array of service objects
+ * that should be called in order at startup.
+ *
+ * @returns The startup order as an array.
+ */
+ getStartupOrder() {
+ let self = this;
+
+ let tzService = Cc["@mozilla.org/calendar/timezone-service;1"]
+ .getService(Ci.calITimezoneService)
+ .QueryInterface(Ci.calIStartupService);
+
+ let calMgr = Cc["@mozilla.org/calendar/manager;1"]
+ .getService(Ci.calICalendarManager)
+ .QueryInterface(Ci.calIStartupService);
+
+ // Localization service
+ let locales = {
+ startup(aCompleteListener) {
+ let packaged = Services.locale.packagedLocales;
+ let fileSrc = new L10nFileSource(
+ "calendar",
+ "app",
+ packaged,
+ "resource:///chrome/{locale}/locale/{locale}/calendar/"
+ );
+ L10nRegistry.getInstance().registerSources([fileSrc]);
+ aCompleteListener.onResult(null, Cr.NS_OK);
+ },
+ shutdown(aCompleteListener) {
+ aCompleteListener.onResult(null, Cr.NS_OK);
+ },
+ };
+
+ // Notification object
+ let notify = {
+ startup(aCompleteListener) {
+ self.started = true;
+ Services.obs.notifyObservers(null, "calendar-startup-done");
+ aCompleteListener.onResult(null, Cr.NS_OK);
+ },
+ shutdown(aCompleteListener) {
+ // Argh, it would have all been so pretty! Since we just reverse
+ // the array, the shutdown notification would happen before the
+ // other shutdown calls. For lack of pretty code, I'm
+ // leaving this out! Users can still listen to xpcom-shutdown.
+ self.started = false;
+ aCompleteListener.onResult(null, Cr.NS_OK);
+ },
+ };
+
+ // We need to spin up the timezone service before the calendar manager
+ // to ensure we have the timezones initialized. Make sure "notify" is
+ // last in this array!
+ return [locales, tzService, calMgr, notify];
+ },
+
+ /**
+ * Observer notification callback
+ */
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "profile-after-change":
+ callOrderedServices("startup", this.getStartupOrder());
+ break;
+ case "profile-before-change":
+ callOrderedServices("shutdown", this.getStartupOrder().reverse());
+ break;
+ case "xpcom-shutdown":
+ Services.obs.removeObserver(this, "profile-after-change");
+ Services.obs.removeObserver(this, "profile-before-change");
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ break;
+ }
+ },
+};
diff --git a/comm/calendar/base/src/CalTimezone.jsm b/comm/calendar/base/src/CalTimezone.jsm
new file mode 100644
index 0000000000..094321fdb3
--- /dev/null
+++ b/comm/calendar/base/src/CalTimezone.jsm
@@ -0,0 +1,77 @@
+/* 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 = ["CalTimezone"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { ICAL } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "l10nBundle", () => {
+ // Prepare localized timezone display values
+ const bundleURL = "chrome://calendar/locale/timezones.properties";
+ return Services.strings.createBundle(bundleURL);
+});
+
+function CalTimezone(innerObject) {
+ this.innerObject = innerObject || new ICAL.Timezone();
+ this.wrappedJSObject = this;
+}
+
+CalTimezone.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calITimezone"]),
+ classID: Components.ID("{6702eb17-a968-4b43-b562-0d0c5f8e9eb5}"),
+
+ innerObject: null,
+
+ get provider() {
+ return cal.timezoneService;
+ },
+
+ get icalComponent() {
+ let innerComp = this.innerObject.component;
+ let comp = null;
+ if (innerComp) {
+ comp = cal.icsService.createIcalComponent("VTIMEZONE");
+ comp.icalComponent = innerComp;
+ }
+ return comp;
+ },
+
+ get tzid() {
+ return this.innerObject.tzid;
+ },
+
+ get isFloating() {
+ return this.innerObject == ICAL.Timezone.localTimezone;
+ },
+
+ get isUTC() {
+ return this.innerObject == ICAL.Timezone.utcTimezone;
+ },
+
+ get displayName() {
+ // Localization is currently only used for floating/UTC until we have a
+ // better story around timezone localization and display
+ let stringName = "pref.timezone." + this.tzid.replace(/\//g, ".");
+ let displayName = this.tzid;
+
+ try {
+ displayName = lazy.l10nBundle.GetStringFromName(stringName);
+ } catch (e) {
+ // Just use the TZID if the string is missing.
+ }
+
+ this.__defineGetter__("displayName", () => {
+ return displayName;
+ });
+ return displayName;
+ },
+
+ tostring() {
+ return this.innerObject.toString();
+ },
+};
diff --git a/comm/calendar/base/src/CalTimezoneService.jsm b/comm/calendar/base/src/CalTimezoneService.jsm
new file mode 100644
index 0000000000..7973435d7c
--- /dev/null
+++ b/comm/calendar/base/src/CalTimezoneService.jsm
@@ -0,0 +1,228 @@
+/* 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 = ["CalTimezoneService"];
+
+var { AppConstants } = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { ICAL, unwrapSingle } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm");
+
+const { CalTimezone } = ChromeUtils.import("resource:///modules/CalTimezone.jsm");
+
+const TIMEZONE_CHANGED_TOPIC = "default-timezone-changed";
+
+// CalTimezoneService acts as an implementation of both ICAL.TimezoneService and
+// the XPCOM calITimezoneService used for providing timezone objects to calendar
+// code.
+function CalTimezoneService() {
+ this.wrappedJSObject = this;
+
+ this._timezoneDatabase = Cc["@mozilla.org/calendar/timezone-database;1"].getService(
+ Ci.calITimezoneDatabase
+ );
+
+ this.mZones = new Map();
+ this.mZoneIds = [];
+
+ ICAL.TimezoneService = this.wrappedJSObject;
+}
+
+var calTimezoneServiceClassID = Components.ID("{e736f2bd-7640-4715-ab35-887dc866c587}");
+var calTimezoneServiceInterfaces = [Ci.calITimezoneService, Ci.calIStartupService];
+CalTimezoneService.prototype = {
+ mDefaultTimezone: null,
+ mVersion: null,
+ mZones: null,
+ mZoneIds: null,
+
+ classID: calTimezoneServiceClassID,
+ QueryInterface: cal.generateQI(["calITimezoneService", "calIStartupService"]),
+ classInfo: cal.generateCI({
+ classID: calTimezoneServiceClassID,
+ contractID: "@mozilla.org/calendar/timezone-service;1",
+ classDescription: "Calendar Timezone Service",
+ interfaces: calTimezoneServiceInterfaces,
+ flags: Ci.nsIClassInfo.SINGLETON,
+ }),
+
+ // ical.js TimezoneService methods
+ has(id) {
+ return this.getTimezone(id) != null;
+ },
+ get(id) {
+ return id ? unwrapSingle(ICAL.Timezone, this.getTimezone(id)) : null;
+ },
+ remove() {},
+ register() {},
+
+ // calIStartupService methods
+ startup(aCompleteListener) {
+ // Fetch list of supported canonical timezone IDs from the backing database
+ this.mZoneIds = this._timezoneDatabase.getCanonicalTimezoneIds();
+
+ // Fetch the version of the backing database
+ this.mVersion = this._timezoneDatabase.version;
+ cal.LOG("[CalTimezoneService] Timezones version " + this.version + " loaded");
+
+ // Set up zones for special values
+ const utc = new CalTimezone(ICAL.Timezone.utcTimezone);
+ this.mZones.set("UTC", utc);
+
+ const floating = new CalTimezone(ICAL.Timezone.localTimezone);
+ this.mZones.set("floating", floating);
+
+ // Initialize default timezone and, if unset, user timezone prefs
+ this._initDefaultTimezone();
+
+ // Watch for changes in system timezone or related user preferences
+ Services.prefs.addObserver("calendar.timezone.useSystemTimezone", this);
+ Services.prefs.addObserver("calendar.timezone.local", this);
+ Services.obs.addObserver(this, TIMEZONE_CHANGED_TOPIC);
+
+ // Notify the startup service that startup is complete
+ if (aCompleteListener) {
+ aCompleteListener.onResult(null, Cr.NS_OK);
+ }
+ },
+
+ shutdown(aCompleteListener) {
+ Services.obs.removeObserver(this, TIMEZONE_CHANGED_TOPIC);
+ Services.prefs.removeObserver("calendar.timezone.local", this);
+ Services.prefs.removeObserver("calendar.timezone.useSystemTimezone", this);
+ aCompleteListener.onResult(null, Cr.NS_OK);
+ },
+
+ // calITimezoneService methods
+ get UTC() {
+ return this.mZones.get("UTC");
+ },
+
+ get floating() {
+ return this.mZones.get("floating");
+ },
+
+ getTimezone(tzid) {
+ if (!tzid) {
+ cal.ERROR("Unknown timezone requested\n" + cal.STACK(10));
+ return null;
+ }
+
+ if (tzid.startsWith("/mozilla.org/")) {
+ // We know that our former tzids look like "/mozilla.org/<dtstamp>/continent/..."
+ // The ending of the mozilla prefix is the index of that slash before the
+ // continent. Therefore, we start looking for the prefix-ending slash
+ // after position 13.
+ tzid = tzid.substring(tzid.indexOf("/", 13) + 1);
+ }
+
+ // Per the IANA timezone database, "Z" is _not_ an alias for UTC, but our
+ // previous list of zones included it and Ical.js at a minimum is expecting
+ // it to be valid
+ if (tzid === "Z") {
+ return this.mZones.get("UTC");
+ }
+
+ // First check our cache of timezones
+ let timezone = this.mZones.get(tzid);
+ if (!timezone) {
+ // The requested timezone is not in the cache; ask the backing database
+ // for the timezone definition
+ const tzdef = this._timezoneDatabase.getTimezoneDefinition(tzid);
+
+ if (!tzdef) {
+ cal.ERROR(`Could not find definition for ${tzid}`);
+ return null;
+ }
+
+ timezone = new CalTimezone(
+ ICAL.Timezone.fromData({
+ tzid,
+ component: tzdef,
+ })
+ );
+
+ // Cache the resulting timezone
+ this.mZones.set(tzid, timezone);
+ }
+
+ return timezone;
+ },
+
+ get timezoneIds() {
+ return this.mZoneIds;
+ },
+
+ get version() {
+ return this.mVersion;
+ },
+
+ _initDefaultTimezone() {
+ // If the "use system timezone" preference is unset, we default to enabling
+ // it if the user's system supports it
+ let isSetSystemTimezonePref = Services.prefs.prefHasUserValue(
+ "calendar.timezone.useSystemTimezone"
+ );
+
+ if (!isSetSystemTimezonePref) {
+ let canUseSystemTimezone = AppConstants.MOZ_CAN_FOLLOW_SYSTEM_TIME;
+
+ Services.prefs.setBoolPref("calendar.timezone.useSystemTimezone", canUseSystemTimezone);
+ }
+
+ this._updateDefaultTimezone();
+ },
+
+ _updateDefaultTimezone() {
+ let prefUseSystemTimezone = Services.prefs.getBoolPref(
+ "calendar.timezone.useSystemTimezone",
+ true
+ );
+ let prefTzid = Services.prefs.getStringPref("calendar.timezone.local", null);
+
+ let tzid;
+ if (prefUseSystemTimezone || prefTzid === null || prefTzid === "floating") {
+ // If we do not have a timezone preference set, we default to using the
+ // system time; we may also do this if the user has set their preferences
+ // accordingly
+ tzid = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ } else {
+ tzid = prefTzid;
+ }
+
+ // Update default timezone and preference if necessary
+ if (!this.mDefaultTimezone || this.mDefaultTimezone.tzid != tzid) {
+ this.mDefaultTimezone = this.getTimezone(tzid);
+ cal.ASSERT(this.mDefaultTimezone, `Timezone not found: ${tzid}`);
+ Services.obs.notifyObservers(null, "defaultTimezoneChanged");
+
+ if (this.mDefaultTimezone.tzid != prefTzid) {
+ Services.prefs.setStringPref("calendar.timezone.local", this.mDefaultTimezone.tzid);
+ }
+ }
+ },
+
+ get defaultTimezone() {
+ // We expect this to be initialized when the service comes up and updated if
+ // the underlying default changes
+ return this.mDefaultTimezone;
+ },
+
+ observe(aSubject, aTopic, aData) {
+ // Update the default timezone if the system timezone has changed; we
+ // expect the update function to decide if actually making the change is
+ // appropriate based on user prefs
+ if (aTopic == TIMEZONE_CHANGED_TOPIC) {
+ this._updateDefaultTimezone();
+ } else if (
+ aTopic == "nsPref:changed" &&
+ (aData == "calendar.timezone.useSystemTimezone" || aData == "calendar.timezone.local")
+ ) {
+ // We may get a bogus second update from the timezone pref if its change
+ // is a result of the system timezone changing, but it should settle, and
+ // trying to guard against it is full of corner cases
+ this._updateDefaultTimezone();
+ }
+ },
+};
diff --git a/comm/calendar/base/src/CalTodo.jsm b/comm/calendar/base/src/CalTodo.jsm
new file mode 100644
index 0000000000..50d2f42fd1
--- /dev/null
+++ b/comm/calendar/base/src/CalTodo.jsm
@@ -0,0 +1,264 @@
+/* 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/. */
+
+/* import-globals-from calItemBase.js */
+
+var EXPORTED_SYMBOLS = ["CalTodo"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+Services.scriptloader.loadSubScript("resource:///components/calItemBase.js");
+
+/**
+ * Constructor for `calITodo` objects.
+ *
+ * @class
+ * @implements {calITodo}
+ * @param {string} [icalString] - Optional iCal string for initializing existing todos.
+ */
+function CalTodo(icalString) {
+ this.initItemBase();
+
+ this.todoPromotedProps = {
+ DTSTART: true,
+ DTEND: true,
+ DUE: true,
+ COMPLETED: true,
+ __proto__: this.itemBasePromotedProps,
+ };
+
+ if (icalString) {
+ this.icalString = icalString;
+ }
+
+ // Set a default percentComplete if the icalString didn't already set it.
+ if (!this.percentComplete) {
+ this.percentComplete = 0;
+ }
+}
+
+var calTodoClassID = Components.ID("{7af51168-6abe-4a31-984d-6f8a3989212d}");
+var calTodoInterfaces = [Ci.calIItemBase, Ci.calITodo, Ci.calIInternalShallowCopy];
+CalTodo.prototype = {
+ __proto__: calItemBase.prototype,
+
+ classID: calTodoClassID,
+ QueryInterface: cal.generateQI(["calIItemBase", "calITodo", "calIInternalShallowCopy"]),
+ classInfo: cal.generateCI({
+ classID: calTodoClassID,
+ contractID: "@mozilla.org/calendar/todo;1",
+ classDescription: "Calendar Todo",
+ interfaces: calTodoInterfaces,
+ }),
+
+ cloneShallow(aNewParent) {
+ let cloned = new CalTodo();
+ this.cloneItemBaseInto(cloned, aNewParent);
+ return cloned;
+ },
+
+ createProxy(aRecurrenceId) {
+ cal.ASSERT(!this.mIsProxy, "Tried to create a proxy for an existing proxy!", true);
+
+ let proxy = new CalTodo();
+
+ // override proxy's DTSTART/DUE/RECURRENCE-ID
+ // before master is set (and item might get immutable):
+ let duration = this.duration;
+ if (duration) {
+ let dueDate = aRecurrenceId.clone();
+ dueDate.addDuration(duration);
+ proxy.dueDate = dueDate;
+ }
+ proxy.entryDate = aRecurrenceId;
+
+ proxy.initializeProxy(this, aRecurrenceId);
+ proxy.mDirty = false;
+
+ return proxy;
+ },
+
+ makeImmutable() {
+ this.makeItemBaseImmutable();
+ },
+
+ isTodo() {
+ return true;
+ },
+
+ get isCompleted() {
+ return this.completedDate != null || this.percentComplete == 100 || this.status == "COMPLETED";
+ },
+
+ set isCompleted(completed) {
+ if (completed) {
+ if (!this.completedDate) {
+ this.completedDate = cal.dtz.jsDateToDateTime(new Date());
+ }
+ this.status = "COMPLETED";
+ this.percentComplete = 100;
+ } else {
+ this.deleteProperty("COMPLETED");
+ this.deleteProperty("STATUS");
+ this.deleteProperty("PERCENT-COMPLETE");
+ }
+ },
+
+ get duration() {
+ let dur = this.getProperty("DURATION");
+ // pick up duration if available, otherwise calculate difference
+ // between start and enddate
+ if (dur) {
+ return cal.createDuration(dur);
+ }
+ if (!this.entryDate || !this.dueDate) {
+ return null;
+ }
+ return this.dueDate.subtractDate(this.entryDate);
+ },
+
+ set duration(value) {
+ this.setProperty("DURATION", value);
+ },
+
+ get recurrenceStartDate() {
+ // DTSTART is optional for VTODOs, so it's unclear if RRULE is allowed then,
+ // so fallback to DUE if no DTSTART is present:
+ return this.entryDate || this.dueDate;
+ },
+
+ icsEventPropMap: [
+ { cal: "DTSTART", ics: "startTime" },
+ { cal: "DUE", ics: "dueTime" },
+ { cal: "COMPLETED", ics: "completedTime" },
+ ],
+
+ set icalString(value) {
+ this.icalComponent = cal.icsService.parseICS(value);
+ },
+
+ get icalString() {
+ let calcomp = cal.icsService.createIcalComponent("VCALENDAR");
+ cal.item.setStaticProps(calcomp);
+ calcomp.addSubcomponent(this.icalComponent);
+ return calcomp.serializeToICS();
+ },
+
+ get icalComponent() {
+ let icalcomp = cal.icsService.createIcalComponent("VTODO");
+ this.fillIcalComponentFromBase(icalcomp);
+ this.mapPropsToICS(icalcomp, this.icsEventPropMap);
+
+ for (let [name, value] of this.properties) {
+ try {
+ // When deleting a property of an occurrence, the property is not actually deleted
+ // but instead set to null, so we need to prevent adding those properties.
+ let wasReset = this.mIsProxy && value === null;
+ if (!this.todoPromotedProps[name] && !wasReset) {
+ let icalprop = cal.icsService.createIcalProperty(name);
+ icalprop.value = value;
+ let propBucket = this.mPropertyParams[name];
+ if (propBucket) {
+ for (let paramName in propBucket) {
+ try {
+ icalprop.setParameter(paramName, propBucket[paramName]);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ // Illegal values should be ignored, but we could log them if
+ // the user has enabled logging.
+ cal.LOG(
+ "Warning: Invalid todo parameter value " +
+ paramName +
+ "=" +
+ propBucket[paramName]
+ );
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+ icalcomp.addProperty(icalprop);
+ }
+ } catch (e) {
+ cal.ERROR("failed to set " + name + " to " + value + ": " + e + "\n");
+ }
+ }
+ return icalcomp;
+ },
+
+ todoPromotedProps: null,
+
+ set icalComponent(todo) {
+ this.modify();
+ if (todo.componentType != "VTODO") {
+ todo = todo.getFirstSubcomponent("VTODO");
+ if (!todo) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+
+ this.mDueDate = undefined;
+ this.setItemBaseFromICS(todo);
+ this.mapPropsFromICS(todo, this.icsEventPropMap);
+
+ this.importUnpromotedProperties(todo, this.todoPromotedProps);
+ // Importing didn't really change anything
+ this.mDirty = false;
+ },
+
+ isPropertyPromoted(name) {
+ // avoid strict undefined property warning
+ return this.todoPromotedProps[name] || false;
+ },
+
+ set entryDate(value) {
+ this.modify();
+
+ // We're about to change the start date of an item which probably
+ // could break the associated calIRecurrenceInfo. We're calling
+ // the appropriate method here to adjust the internal structure in
+ // order to free clients from worrying about such details.
+ if (this.parentItem == this) {
+ let rec = this.recurrenceInfo;
+ if (rec) {
+ rec.onStartDateChange(value, this.entryDate);
+ }
+ }
+
+ this.setProperty("DTSTART", value);
+ },
+
+ get entryDate() {
+ return this.getProperty("DTSTART");
+ },
+
+ mDueDate: undefined,
+ get dueDate() {
+ let dueDate = this.mDueDate;
+ if (dueDate === undefined) {
+ dueDate = this.getProperty("DUE");
+ if (!dueDate) {
+ let entryDate = this.entryDate;
+ let dur = this.getProperty("DURATION");
+ if (entryDate && dur) {
+ // If there is a duration set on the todo, calculate the right end time.
+ dueDate = entryDate.clone();
+ dueDate.addDuration(cal.createDuration(dur));
+ }
+ }
+ this.mDueDate = dueDate;
+ }
+ return dueDate;
+ },
+
+ set dueDate(value) {
+ this.deleteProperty("DURATION"); // setting dueDate once removes DURATION
+ this.setProperty("DUE", value);
+ this.mDueDate = value;
+ },
+};
+
+makeMemberAttrProperty(CalTodo, "COMPLETED", "completedDate");
+makeMemberAttrProperty(CalTodo, "PERCENT-COMPLETE", "percentComplete");
diff --git a/comm/calendar/base/src/CalTransactionManager.jsm b/comm/calendar/base/src/CalTransactionManager.jsm
new file mode 100644
index 0000000000..67f01733ec
--- /dev/null
+++ b/comm/calendar/base/src/CalTransactionManager.jsm
@@ -0,0 +1,372 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "CalTransactionManager",
+ "CalTransaction",
+ "CalBatchTransaction",
+ "CalAddTransaction",
+ "CalModifyTransaction",
+ "CalDeleteTransaction",
+];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+const OP_ADD = Ci.calIOperationListener.ADD;
+const OP_MODIFY = Ci.calIOperationListener.MODIFY;
+const OP_DELETE = Ci.calIOperationListener.DELETE;
+
+let transactionManager = null;
+
+/**
+ * CalTransactionManager is used to track user initiated operations on calendar
+ * items. These transactions can be undone or repeated when appropriate.
+ *
+ * This implementation is used instead of nsITransactionManager because better
+ * support for async transactions and access to batch transactions is needed
+ * which nsITransactionManager does not provide.
+ */
+class CalTransactionManager {
+ /**
+ * Contains transactions executed by the transaction manager than can be
+ * undone.
+ *
+ * @type {CalTransaction}
+ */
+ undoStack = [];
+
+ /**
+ * Contains transactions that have been undone by the transaction manager and
+ * can be redone again later if desired.
+ *
+ * @type {CalTransaction}
+ */
+ redoStack = [];
+
+ /**
+ * Provides a singleton instance of the CalTransactionManager.
+ *
+ * @returns {CalTransactionManager}
+ */
+ static getInstance() {
+ if (!transactionManager) {
+ transactionManager = new CalTransactionManager();
+ }
+ return transactionManager;
+ }
+
+ /**
+ * @typedef {object} ExtResponse
+ * @property {number} responseMode One of the calIItipItem.autoResponse values.
+ */
+
+ /**
+ * @typedef {"add" | "modify" | "delete"} Action
+ */
+
+ /**
+ * Adds a CalTransaction to the internal stack. The transaction will be
+ * executed and its resulting Promise returned.
+ *
+ * @param {CalTransaction} trn - The CalTransaction to add to the stack and
+ * execute.
+ */
+ async commit(trn) {
+ this.undoStack.push(trn);
+ return trn.doTransaction();
+ }
+
+ /**
+ * Creates and pushes a new CalBatchTransaction onto the internal stack.
+ * The created transaction is returned and can be used to combine multiple
+ * transactions into one.
+ *
+ * @returns {CalBatchTrasaction}
+ */
+ beginBatch() {
+ let trn = new CalBatchTransaction();
+ this.undoStack.push(trn);
+ return trn;
+ }
+
+ /**
+ * peekUndoStack provides the top transaction on the undo stack (if any)
+ * without modifying the stack.
+ *
+ * @returns {CalTransaction?}
+ */
+ peekUndoStack() {
+ return this.undoStack.at(-1);
+ }
+
+ /**
+ * Undo the transaction at the top of the undo stack.
+ *
+ * @throws - NS_ERROR_FAILURE if the undo stack is empty.
+ */
+ async undo() {
+ if (!this.undoStack.length) {
+ throw new Components.Exception(
+ "CalTransactionManager: undo stack is empty!",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+ let trn = this.undoStack.pop();
+ this.redoStack.push(trn);
+ return trn.undoTransaction();
+ }
+
+ /**
+ * Returns true if it is possible to undo the transaction at the top of the
+ * undo stack.
+ *
+ * @returns {boolean}
+ */
+ canUndo() {
+ let trn = this.peekUndoStack();
+ return Boolean(trn?.canWrite());
+ }
+
+ /**
+ * peekRedoStack provides the top transaction on the redo stack (if any)
+ * without modifying the stack.
+ *
+ * @returns {CalTransaction?}
+ */
+ peekRedoStack() {
+ return this.redoStack.at(-1);
+ }
+
+ /**
+ * Redo the transaction at the top of the redo stack.
+ *
+ * @throws - NS_ERROR_FAILURE if the redo stack is empty.
+ */
+ async redo() {
+ if (!this.redoStack.length) {
+ throw new Components.Exception(
+ "CalTransactionManager: redo stack is empty!",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+ let trn = this.redoStack.pop();
+ this.undoStack.push(trn);
+ return trn.doTransaction();
+ }
+
+ /**
+ * Returns true if it is possible to redo the transaction at the top of the
+ * redo stack.
+ *
+ * @returns {boolean}
+ */
+ canRedo() {
+ let trn = this.peekRedoStack();
+ return Boolean(trn?.canWrite());
+ }
+}
+
+/**
+ * CalTransaction represents a single, atomic user operation on one or more
+ * calendar items.
+ */
+class CalTransaction {
+ /**
+ * Indicates whether the calendar of the transaction's target item(s) can be
+ * written to.
+ *
+ * @returns {boolean}
+ */
+ canWrite() {
+ return false;
+ }
+
+ /**
+ * Executes the transaction.
+ */
+ async doTransaction() {}
+
+ /**
+ * Executes the "undo" action of the transaction.
+ */
+ async undoTransaction() {}
+}
+
+/**
+ * CalBatchTransaction is used for batch transactions where multiple transactions
+ * treated as one is desired. For example; where the user selects and deletes
+ * more than one event.
+ */
+class CalBatchTransaction extends CalTransaction {
+ /**
+ * Stores the transactions that belong to the batch.
+ *
+ * @type {CalTransaction[]}
+ */
+ transactions = [];
+
+ /**
+ * Similar to the CalTransactionManager method except the transaction will be
+ * added to the batch.
+ */
+ async commit(trn) {
+ this.transactions.push(trn);
+ return trn.doTransaction();
+ }
+
+ canWrite() {
+ return Boolean(this.transactions.length && this.transactions.every(trn => trn.canWrite()));
+ }
+
+ async doTransaction() {
+ for (let trn of this.transactions) {
+ await trn.doTransaction();
+ }
+ }
+
+ async undoTransaction() {
+ for (let trn of this.transactions.slice().reverse()) {
+ await trn.undoTransaction();
+ }
+ }
+}
+
+/**
+ * CalBaseTransaction serves as the base for add/modify/delete operations.
+ */
+class CalBaseTransaction extends CalTransaction {
+ /**
+ * @type {calICalendar}
+ */
+ calendar = null;
+
+ /**
+ * @type {calIItemBase}
+ */
+ item = null;
+
+ /**
+ * @type {calIItemBase}
+ */
+ oldItem = null;
+
+ /**
+ * @type {calICalendar}
+ */
+ oldCalendar = null;
+
+ /**
+ * @type {ExtResponse}
+ */
+ extResponse = null;
+
+ /**
+ * @private
+ * @param {calIItemBase} item
+ * @param {calICalendar} calendar
+ * @param {calIItemBase?} oldItem
+ * @param {object?} extResponse
+ */
+ constructor(item, calendar, oldItem, extResponse) {
+ super();
+ this.item = item;
+ this.calendar = calendar;
+ this.oldItem = oldItem;
+ this.extResponse = extResponse;
+ }
+
+ _dispatch(opType, item, oldItem) {
+ cal.itip.checkAndSend(opType, item, oldItem, this.extResponse);
+ }
+
+ canWrite() {
+ if (itemWritable(this.item)) {
+ return this instanceof CalModifyTransaction ? itemWritable(this.oldItem) : true;
+ }
+ return false;
+ }
+}
+
+/**
+ * CalAddTransaction handles additions.
+ */
+class CalAddTransaction extends CalBaseTransaction {
+ async doTransaction() {
+ let item = await this.calendar.addItem(this.item);
+ this._dispatch(OP_ADD, item, this.oldItem);
+ this.item = item;
+ }
+
+ async undoTransaction() {
+ await this.calendar.deleteItem(this.item);
+ this._dispatch(OP_DELETE, this.item, this.item);
+ this.oldItem = this.item;
+ }
+}
+
+/**
+ * CalModifyTransaction handles modifications.
+ */
+class CalModifyTransaction extends CalBaseTransaction {
+ async doTransaction() {
+ let item;
+ if (this.item.calendar.id == this.oldItem.calendar.id) {
+ item = await this.calendar.modifyItem(
+ cal.itip.prepareSequence(this.item, this.oldItem),
+ this.oldItem
+ );
+ this._dispatch(OP_MODIFY, item, this.oldItem);
+ } else {
+ this.oldCalendar = this.oldItem.calendar;
+ item = await this.calendar.addItem(this.item);
+ this._dispatch(OP_ADD, item, this.oldItem);
+ await this.oldItem.calendar.deleteItem(this.oldItem);
+ this._dispatch(OP_DELETE, this.oldItem, this.oldItem);
+ }
+ this.item = item;
+ }
+
+ async undoTransaction() {
+ if (this.oldItem.calendar.id == this.item.calendar.id) {
+ await this.calendar.modifyItem(cal.itip.prepareSequence(this.oldItem, this.item), this.item);
+ this._dispatch(OP_MODIFY, this.oldItem, this.oldItem);
+ } else {
+ await this.calendar.deleteItem(this.item);
+ this._dispatch(OP_DELETE, this.item, this.item);
+ await this.oldCalendar.addItem(this.oldItem);
+ this._dispatch(OP_ADD, this.oldItem, this.item);
+ }
+ }
+}
+
+/**
+ * CalDeleteTransaction handles deletions.
+ */
+class CalDeleteTransaction extends CalBaseTransaction {
+ async doTransaction() {
+ await this.calendar.deleteItem(this.item);
+ this._dispatch(OP_DELETE, this.item, this.oldItem);
+ }
+
+ async undoTransaction() {
+ await this.calendar.addItem(this.item);
+ this._dispatch(OP_ADD, this.item, this.item);
+ }
+}
+
+/**
+ * Checks whether an item's calendar can be written to.
+ *
+ * @param {calIItemBase} item
+ */
+function itemWritable(item) {
+ return (
+ item &&
+ item.calendar &&
+ cal.acl.isCalendarWritable(item.calendar) &&
+ cal.acl.userCanAddItemsToCalendar(item.calendar)
+ );
+}
diff --git a/comm/calendar/base/src/CalWeekInfoService.jsm b/comm/calendar/base/src/CalWeekInfoService.jsm
new file mode 100644
index 0000000000..a94145f44e
--- /dev/null
+++ b/comm/calendar/base/src/CalWeekInfoService.jsm
@@ -0,0 +1,113 @@
+/* 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 = ["CalWeekInfoService"];
+
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+const SUNDAY = 0;
+const THURSDAY = 4;
+
+const lazy = {};
+
+XPCOMUtils.defineLazyPreferenceGetter(lazy, "startWeekday", "calendar.week.start", SUNDAY);
+
+function CalWeekInfoService() {
+ this.wrappedJSObject = this;
+}
+CalWeekInfoService.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIWeekInfoService"]),
+ classID: Components.ID("{6877bbdd-f336-46f5-98ce-fe86d0285cc1}"),
+
+ // calIWeekInfoService:
+ getWeekTitle(aDateTime) {
+ /**
+ * This implementation is based on the ISO 8601 standard.
+ * ISO 8601 defines week one as the first week with at least 4
+ * days, and defines Monday as the first day of the week.
+ * Equivalently, the week one is the week with the first Thursday.
+ *
+ * This implementation uses the second definition, because it
+ * enables the user to set a different start-day of the week
+ * (Sunday instead of Monday is a common setting). If the first
+ * definition was used, all week-numbers could be off by one
+ * depending on the week start day. (For example, if weeks start
+ * on Sunday, a year that starts on Thursday has only 3 days
+ * [Thu-Sat] in that week, so it would be part of the last week of
+ * the previous year, but if weeks start on Monday, the year would
+ * have four days [Thu-Sun] in that week, so it would be counted
+ * as week 1.)
+ */
+
+ // The week number is the number of days since the start of week 1,
+ // divided by 7 and rounded up. Week 1 is the week containing the first
+ // Thursday of the year.
+ // Thus, the week number of any day is the same as the number of days
+ // between the Thursday of that week and the Thursday of week 1, divided
+ // by 7 and rounded up. (This takes care of days at end/start of a year
+ // which may be part of first/last week in the other year.)
+ // The Thursday of a week is the Thursday that follows the first day
+ // of the week.
+ // The week number of a day is the same as the week number of the first
+ // day of the week. (This takes care of days near the start of the year,
+ // which may be part of the week counted in the previous year.) So we
+ // need the startWeekday.
+
+ // The number of days since the start of the week.
+ // Notice that the result of the subtraction might be negative.
+ // We correct for that by adding 7, and then using the remainder operator.
+ let sinceStartOfWeek = (aDateTime.weekday - lazy.startWeekday + 7) % 7;
+
+ // The number of days to Thursday is the difference between Thursday
+ // and the start-day of the week (again corrected for negative values).
+ let startToThursday = (THURSDAY - lazy.startWeekday + 7) % 7;
+
+ // The yearday number of the Thursday this week.
+ let thisWeeksThursday = aDateTime.yearday - sinceStartOfWeek + startToThursday;
+
+ if (thisWeeksThursday < 1) {
+ // For the first few days of the year, we still are in week 52 or 53.
+ let lastYearDate = aDateTime.clone();
+ lastYearDate.year -= 1;
+ thisWeeksThursday += lastYearDate.endOfYear.yearday;
+ } else if (thisWeeksThursday > aDateTime.endOfYear.yearday) {
+ // For the last few days of the year, we already are in week 1.
+ thisWeeksThursday -= aDateTime.endOfYear.yearday;
+ }
+
+ let weekNumber = Math.ceil(thisWeeksThursday / 7);
+ return weekNumber;
+ },
+
+ /**
+ * gets the first day of a week of a passed day under consideration
+ * of the preference setting "calendar.week.start"
+ *
+ * @param aDate a date time object
+ * @returns a dateTime-object denoting the first day of the week
+ */
+ getStartOfWeek(aDate) {
+ let date = aDate.clone();
+ date.isDate = true;
+ let offset = lazy.startWeekday - aDate.weekday;
+ date.day += offset;
+ if (offset > 0) {
+ date.day -= 7;
+ }
+ return date;
+ },
+
+ /**
+ * gets the last day of a week of a passed day under consideration
+ * of the preference setting "calendar.week.start"
+ *
+ * @param aDate a date time object
+ * @returns a dateTime-object denoting the last day of the week
+ */
+ getEndOfWeek(aDate) {
+ let date = this.getStartOfWeek(aDate);
+ date.day += 6;
+ return date;
+ },
+};
diff --git a/comm/calendar/base/src/TimezoneDatabase.cpp b/comm/calendar/base/src/TimezoneDatabase.cpp
new file mode 100644
index 0000000000..d93f78e1f4
--- /dev/null
+++ b/comm/calendar/base/src/TimezoneDatabase.cpp
@@ -0,0 +1,114 @@
+/* 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/. */
+
+#include "nsPrintfCString.h"
+#include "nsString.h"
+#include "unicode/strenum.h"
+#include "unicode/timezone.h"
+#include "unicode/ucal.h"
+#include "unicode/utypes.h"
+#include "unicode/vtzone.h"
+
+#include "TimezoneDatabase.h"
+
+NS_IMPL_ISUPPORTS(TimezoneDatabase, calITimezoneDatabase)
+
+NS_IMETHODIMP
+TimezoneDatabase::GetVersion(nsACString& aVersion) {
+ UErrorCode err = U_ZERO_ERROR;
+ const char* version = icu::VTimeZone::getTZDataVersion(err);
+ if (U_FAILURE(err)) {
+ NS_WARNING(nsPrintfCString("ICU error: %s", u_errorName(err)).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ aVersion.Assign(version);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TimezoneDatabase::GetCanonicalTimezoneIds(nsTArray<nsCString>& aTimezoneIds) {
+ aTimezoneIds.Clear();
+
+ UErrorCode err = U_ZERO_ERROR;
+
+ // Because this list of IDs is not intended to be restrictive, we only request
+ // the canonical IDs to avoid providing lots of redundant options to users
+ icu::StringEnumeration* icuEnum = icu::VTimeZone::createTimeZoneIDEnumeration(
+ UCAL_ZONE_TYPE_CANONICAL, nullptr, nullptr, err);
+ if (U_FAILURE(err)) {
+ NS_WARNING(nsPrintfCString("ICU error: %s", u_errorName(err)).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ const char* value;
+ err = U_ZERO_ERROR;
+ while ((value = icuEnum->next(nullptr, err)) != nullptr && U_SUCCESS(err)) {
+ nsCString tzid(value);
+ aTimezoneIds.AppendElement(tzid);
+ }
+
+ if (U_FAILURE(err)) {
+ // If we encountered any error during enumeration of the timezones, we want
+ // to return an empty list
+ aTimezoneIds.Clear();
+
+ NS_WARNING(nsPrintfCString("ICU error: %s", u_errorName(err)).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TimezoneDatabase::GetTimezoneDefinition(const nsACString& tzid,
+ nsACString& _retval) {
+ _retval.Truncate();
+
+ NS_ConvertUTF8toUTF16 convertedTzid(tzid);
+
+ // It seems Windows can potentially build `convertedTzid` with wchar_t
+ // underlying, which makes the UnicodeString ctor ambiguous; be explicit here
+ const char16_t* convertedTzidPtr = convertedTzid.get();
+
+ icu::UnicodeString icuTzid(convertedTzidPtr,
+ static_cast<int>(convertedTzid.Length()));
+
+ auto* icuTimezone = icu::VTimeZone::createVTimeZoneByID(icuTzid);
+ if (icuTimezone == nullptr) {
+ return NS_OK;
+ }
+
+ // Work around https://unicode-org.atlassian.net/browse/ICU-22175
+ // This workaround is overly complex because there's no simple, reliable way
+ // to determine if a VTimeZone is Etc/Unknown; getID() doesn't work because
+ // the ctor doesn't set the ID field, and hasSameRules() against Etc/Unknown
+ // will return true if icuTimezone is GMT
+ if (icuTimezone->hasSameRules(icu::TimeZone::getUnknown()) &&
+ !tzid.Equals("Etc/Unknown")) {
+ icu::UnicodeString actualTzid;
+ icu::TimeZone::createTimeZone(icuTzid)->getID(actualTzid);
+
+ if (actualTzid == UNICODE_STRING("Etc/Unknown", 11)) {
+ return NS_OK;
+ }
+ }
+
+ // Extract the VTIMEZONE definition from the timezone object
+ icu::UnicodeString vtimezoneDef;
+ UErrorCode err = U_ZERO_ERROR;
+ icuTimezone->write(vtimezoneDef, err);
+ if (U_FAILURE(err)) {
+ NS_WARNING(nsPrintfCString("ICU error: %s", u_errorName(err)).get());
+
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ConvertUTF16toUTF8 convertedDef(vtimezoneDef.getTerminatedBuffer());
+
+ _retval.Assign(convertedDef);
+
+ return NS_OK;
+}
diff --git a/comm/calendar/base/src/TimezoneDatabase.h b/comm/calendar/base/src/TimezoneDatabase.h
new file mode 100644
index 0000000000..df3f782309
--- /dev/null
+++ b/comm/calendar/base/src/TimezoneDatabase.h
@@ -0,0 +1,20 @@
+/* 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/. */
+#ifndef mozilla_calTimezoneDatabase_h__
+#define mozilla_calTimezoneDatabase_h__
+
+#include "calITimezoneDatabase.h"
+
+class TimezoneDatabase final : public calITimezoneDatabase {
+ NS_DECL_ISUPPORTS
+ NS_DECL_CALITIMEZONEDATABASE
+
+ public:
+ TimezoneDatabase() = default;
+
+ private:
+ ~TimezoneDatabase() = default;
+};
+
+#endif
diff --git a/comm/calendar/base/src/calApplicationUtils.js b/comm/calendar/base/src/calApplicationUtils.js
new file mode 100644
index 0000000000..47668a099f
--- /dev/null
+++ b/comm/calendar/base/src/calApplicationUtils.js
@@ -0,0 +1,47 @@
+/* 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/. */
+
+/* exported launchBrowser */
+
+/**
+ * Launch the given url (string) in the external browser. If an event is passed,
+ * then this is only done on left click and the event propagation is stopped.
+ *
+ * @param url The URL to open, as a string
+ * @param event (optional) The event that caused the URL to open
+ */
+function launchBrowser(url, event) {
+ // Bail out if there is no url set, or an event was passed without left-click
+ if (!url || (event && event.button != 0)) {
+ return;
+ }
+
+ // 0. Prevent people from trying to launch URLs such as javascript:foo();
+ // by only allowing URLs starting with http or https or mid.
+ // XXX: We likely will want to do this using nsIURLs in the future to
+ // prevent sneaky nasty escaping issues, but this is fine for now.
+ if (!/^https?:/i.test(url) && !/^mid:/i.test(url)) {
+ console.error(
+ "launchBrowser: Invalid URL provided: " + url + " Only http(s):// and mid:// URLs are valid."
+ );
+ return;
+ }
+
+ if (/^mid:/i.test(url)) {
+ let { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+ MailUtils.openMessageByMessageId(url.slice(4));
+ return;
+ }
+
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadURI(Services.io.newURI(url));
+
+ // Make sure that any default click handlers don't do anything, we have taken
+ // care of all processing
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+}
diff --git a/comm/calendar/base/src/calCachedCalendar.js b/comm/calendar/base/src/calCachedCalendar.js
new file mode 100644
index 0000000000..3dd2d872a4
--- /dev/null
+++ b/comm/calendar/base/src/calCachedCalendar.js
@@ -0,0 +1,957 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["calCachedCalendar"];
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+var calICalendar = Ci.calICalendar;
+var cICL = Ci.calIChangeLog;
+var cIOL = Ci.calIOperationListener;
+
+var gNoOpListener = {
+ QueryInterface: ChromeUtils.generateQI(["calIOperationListener"]),
+ onGetResult(calendar, status, itemType, detail, items) {},
+
+ onOperationComplete(calendar, status, opType, id, detail) {},
+};
+
+/**
+ * Returns true if the exception passed is one that should cause the cache
+ * layer to retry the operation. This is usually a network error or other
+ * temporary error.
+ *
+ * @param result The result code to check.
+ * @returns True, if the result code means server unavailability.
+ */
+function isUnavailableCode(result) {
+ // Stolen from nserror.h
+ const NS_ERROR_MODULE_NETWORK = 6;
+ function NS_ERROR_GET_MODULE(code) {
+ return ((code >> 16) - 0x45) & 0x1fff;
+ }
+
+ if (NS_ERROR_GET_MODULE(result) == NS_ERROR_MODULE_NETWORK && !Components.isSuccessCode(result)) {
+ // This is a network error, which most likely means we should
+ // retry it some time.
+ return true;
+ }
+
+ // Other potential errors we want to retry with
+ switch (result) {
+ case Cr.NS_ERROR_NOT_AVAILABLE:
+ return true;
+ default:
+ return false;
+ }
+}
+
+function calCachedCalendarObserverHelper(home, isCachedObserver) {
+ this.home = home;
+ this.isCachedObserver = isCachedObserver;
+}
+calCachedCalendarObserverHelper.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIObserver"]),
+ isCachedObserver: false,
+
+ onStartBatch() {
+ this.home.mObservers.notify("onStartBatch", [this.home]);
+ },
+
+ onEndBatch() {
+ this.home.mObservers.notify("onEndBatch", [this.home]);
+ },
+
+ async onLoad(calendar) {
+ if (this.isCachedObserver) {
+ this.home.mObservers.notify("onLoad", [this.home]);
+ } else {
+ // start sync action after uncached calendar has been loaded.
+ // xxx todo, think about:
+ // although onAddItem et al have been called, we need to fire
+ // an additional onLoad completing the refresh call (->composite)
+ let home = this.home;
+ await home.synchronize();
+ home.mObservers.notify("onLoad", [home]);
+ }
+ },
+
+ onAddItem(aItem) {
+ if (this.isCachedObserver) {
+ this.home.mObservers.notify("onAddItem", arguments);
+ }
+ },
+
+ onModifyItem(aNewItem, aOldItem) {
+ if (this.isCachedObserver) {
+ this.home.mObservers.notify("onModifyItem", arguments);
+ }
+ },
+
+ onDeleteItem(aItem) {
+ if (this.isCachedObserver) {
+ this.home.mObservers.notify("onDeleteItem", arguments);
+ }
+ },
+
+ onError(aCalendar, aErrNo, aMessage) {
+ this.home.mObservers.notify("onError", arguments);
+ },
+
+ onPropertyChanged(aCalendar, aName, aValue, aOldValue) {
+ if (!this.isCachedObserver) {
+ this.home.mObservers.notify("onPropertyChanged", [this.home, aName, aValue, aOldValue]);
+ }
+ },
+
+ onPropertyDeleting(aCalendar, aName) {
+ if (!this.isCachedObserver) {
+ this.home.mObservers.notify("onPropertyDeleting", [this.home, aName]);
+ }
+ },
+};
+
+function calCachedCalendar(uncachedCalendar) {
+ this.wrappedJSObject = this;
+ this.mSyncQueue = [];
+ this.mObservers = new cal.data.ObserverSet(Ci.calIObserver);
+ uncachedCalendar.superCalendar = this;
+ uncachedCalendar.addObserver(new calCachedCalendarObserverHelper(this, false));
+ this.mUncachedCalendar = uncachedCalendar;
+ this.setupCachedCalendar();
+ if (this.supportsChangeLog) {
+ uncachedCalendar.offlineStorage = this.mCachedCalendar;
+ }
+ this.offlineCachedItems = {};
+ this.offlineCachedItemFlags = {};
+}
+calCachedCalendar.prototype = {
+ /* eslint-disable mozilla/use-chromeutils-generateqi */
+ QueryInterface(aIID) {
+ if (aIID.equals(Ci.calISchedulingSupport) && this.mUncachedCalendar.QueryInterface(aIID)) {
+ // check whether uncached calendar supports it:
+ return this;
+ } else if (aIID.equals(Ci.calICalendar) || aIID.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+ /* eslint-enable mozilla/use-chromeutils-generateqi */
+
+ mCachedCalendar: null,
+ mCachedObserver: null,
+ mUncachedCalendar: null,
+ mObservers: null,
+ mSuperCalendar: null,
+ offlineCachedItems: null,
+ offlineCachedItemFlags: null,
+
+ onCalendarUnregistering() {
+ if (this.mCachedCalendar) {
+ let self = this;
+ this.mCachedCalendar.removeObserver(this.mCachedObserver);
+ // TODO put changes into a different calendar and delete
+ // afterwards.
+
+ let listener = {
+ onDeleteCalendar(aCalendar, aStatus, aDetail) {
+ self.mCachedCalendar = null;
+ },
+ };
+
+ this.mCachedCalendar
+ .QueryInterface(Ci.calICalendarProvider)
+ .deleteCalendar(this.mCachedCalendar, listener);
+ }
+ },
+
+ setupCachedCalendar() {
+ try {
+ if (this.mCachedCalendar) {
+ // this is actually a resetupCachedCalendar:
+ // Although this doesn't really follow the spec, we know the
+ // storage calendar's deleteCalendar method is synchronous.
+ // TODO put changes into a different calendar and delete
+ // afterwards.
+ this.mCachedCalendar
+ .QueryInterface(Ci.calICalendarProvider)
+ .deleteCalendar(this.mCachedCalendar, null);
+ if (this.supportsChangeLog) {
+ // start with full sync:
+ this.mUncachedCalendar.resetLog();
+ }
+ } else {
+ let calType = Services.prefs.getStringPref("calendar.cache.type", "storage");
+ // While technically, the above deleteCalendar should delete the
+ // whole calendar, this is nothing more than deleting all events
+ // todos and properties. Therefore the initialization can be
+ // skipped.
+ let cachedCalendar = Cc["@mozilla.org/calendar/calendar;1?type=" + calType].createInstance(
+ Ci.calICalendar
+ );
+ switch (calType) {
+ case "memory": {
+ if (this.supportsChangeLog) {
+ // start with full sync:
+ this.mUncachedCalendar.resetLog();
+ }
+ break;
+ }
+ case "storage": {
+ let file = cal.provider.getCalendarDirectory();
+ file.append("cache.sqlite");
+ cachedCalendar.uri = Services.io.newFileURI(file);
+ cachedCalendar.id = this.id;
+ break;
+ }
+ default: {
+ throw new Error("unsupported cache calendar type: " + calType);
+ }
+ }
+ cachedCalendar.transientProperties = true;
+ // Forward the disabled property to the storage calendar so that it
+ // stops interacting with the file system. Other properties have no
+ // useful effect on the storage calendar, so don't forward them.
+ cachedCalendar.setProperty("disabled", this.getProperty("disabled"));
+ cachedCalendar.setProperty("relaxedMode", true);
+ cachedCalendar.superCalendar = this;
+ if (!this.mCachedObserver) {
+ this.mCachedObserver = new calCachedCalendarObserverHelper(this, true);
+ }
+ cachedCalendar.addObserver(this.mCachedObserver);
+ this.mCachedCalendar = cachedCalendar;
+ }
+ } catch (exc) {
+ console.error(exc);
+ }
+ },
+
+ async getOfflineAddedItems() {
+ this.offlineCachedItems = {};
+ for await (let items of cal.iterate.streamValues(
+ this.mCachedCalendar.getItems(
+ calICalendar.ITEM_FILTER_ALL_ITEMS | calICalendar.ITEM_FILTER_OFFLINE_CREATED,
+ 0,
+ null,
+ null
+ )
+ )) {
+ for (let item of items) {
+ this.offlineCachedItems[item.hashId] = item;
+ this.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_CREATED_RECORD;
+ }
+ }
+ },
+
+ async getOfflineModifiedItems() {
+ for await (let items of cal.iterate.streamValues(
+ this.mCachedCalendar.getItems(
+ calICalendar.ITEM_FILTER_OFFLINE_MODIFIED | calICalendar.ITEM_FILTER_ALL_ITEMS,
+ 0,
+ null,
+ null
+ )
+ )) {
+ for (let item of items) {
+ this.offlineCachedItems[item.hashId] = item;
+ this.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_MODIFIED_RECORD;
+ }
+ }
+ },
+
+ async getOfflineDeletedItems() {
+ for await (let items of cal.iterate.streamValues(
+ this.mCachedCalendar.getItems(
+ calICalendar.ITEM_FILTER_OFFLINE_DELETED | calICalendar.ITEM_FILTER_ALL_ITEMS,
+ 0,
+ null,
+ null
+ )
+ )) {
+ for (let item of items) {
+ this.offlineCachedItems[item.hashId] = item;
+ this.offlineCachedItemFlags[item.hashId] = cICL.OFFLINE_FLAG_DELETED_RECORD;
+ }
+ }
+ },
+
+ mPendingSync: null,
+ async synchronize() {
+ if (!this.mPendingSync) {
+ this.mPendingSync = this._doSynchronize().catch(console.error);
+ }
+ return this.mPendingSync;
+ },
+ async _doSynchronize() {
+ let clearPending = () => {
+ this.mPendingSync = null;
+ };
+
+ if (this.getProperty("disabled")) {
+ clearPending();
+ return;
+ }
+
+ if (this.offline) {
+ clearPending();
+ return;
+ }
+
+ if (this.supportsChangeLog) {
+ await new Promise((resolve, reject) => {
+ let spec = this.uri.spec;
+ cal.LOG("[calCachedCalendar] Doing changelog based sync for calendar " + spec);
+ let opListener = {
+ onResult(operation, result) {
+ if (!operation || !operation.isPending) {
+ let status = operation ? operation.status : Cr.NS_OK;
+ clearPending();
+ if (!Components.isSuccessCode(status)) {
+ reject(
+ "[calCachedCalendar] replay action failed: " +
+ (operation && operation.id ? operation.id : "<unknown>") +
+ ", uri=" +
+ spec +
+ ", result=" +
+ result +
+ ", operation=" +
+ operation
+ );
+ return;
+ }
+ cal.LOG("[calCachedCalendar] replayChangesOn finished.");
+ resolve();
+ }
+ },
+ };
+ this.mUncachedCalendar.replayChangesOn(opListener);
+ });
+ return;
+ }
+
+ cal.LOG("[calCachedCalendar] Doing full sync for calendar " + this.uri.spec);
+
+ await this.getOfflineAddedItems();
+ await this.getOfflineModifiedItems();
+ await this.getOfflineDeletedItems();
+
+ // TODO instead of deleting the calendar and creating a new
+ // one, maybe we want to do a "real" sync between the
+ // existing local calendar and the remote calendar.
+ this.setupCachedCalendar();
+
+ let modifiedTimes = {};
+ try {
+ for await (let items of cal.iterate.streamValues(
+ this.mUncachedCalendar.getItems(Ci.calICalendar.ITEM_FILTER_ALL_ITEMS, 0, null, null)
+ )) {
+ for (let item of items) {
+ // Adding items recd from the Memory Calendar
+ // These may be different than what the cache has
+ modifiedTimes[item.id] = item.lastModifiedTime;
+ this.mCachedCalendar.addItem(item);
+ }
+ }
+ } catch (e) {
+ await this.playbackOfflineItems();
+ this.mCachedObserver.onLoad(this.mCachedCalendar);
+ clearPending();
+ throw e; // Do not swallow this error.
+ }
+
+ await new Promise((resolve, reject) => {
+ cal.iterate.forEach(
+ this.offlineCachedItems,
+ item => {
+ switch (this.offlineCachedItemFlags[item.hashId]) {
+ case cICL.OFFLINE_FLAG_CREATED_RECORD:
+ // Created items are not present on the server, so its safe to adopt them
+ this.adoptOfflineItem(item.clone());
+ break;
+ case cICL.OFFLINE_FLAG_MODIFIED_RECORD:
+ // Two Cases Here:
+ if (item.id in modifiedTimes) {
+ // The item is still on the server, we just retrieved it in the listener above.
+ if (item.lastModifiedTime.compare(modifiedTimes[item.id]) < 0) {
+ // The item on the server has been modified, ask to overwrite
+ cal.WARN(
+ "[calCachedCalendar] Item '" +
+ item.title +
+ "' at the server seems to be modified recently."
+ );
+ this.promptOverwrite("modify", item, null);
+ } else {
+ // Our item is newer, just modify the item
+ this.modifyOfflineItem(item, null);
+ }
+ } else {
+ // The item has been deleted from the server, ask if it should be added again
+ cal.WARN(
+ "[calCachedCalendar] Item '" + item.title + "' has been deleted from the server"
+ );
+ if (cal.provider.promptOverwrite("modify", item, null)) {
+ this.adoptOfflineItem(item.clone());
+ }
+ }
+ break;
+ case cICL.OFFLINE_FLAG_DELETED_RECORD:
+ if (item.id in modifiedTimes) {
+ // The item seems to exist on the server...
+ if (item.lastModifiedTime.compare(modifiedTimes[item.id]) < 0) {
+ // ...and has been modified on the server. Ask to overwrite
+ cal.WARN(
+ "[calCachedCalendar] Item '" +
+ item.title +
+ "' at the server seems to be modified recently."
+ );
+ this.promptOverwrite("delete", item, null);
+ } else {
+ // ...and has not been modified. Delete it now.
+ this.deleteOfflineItem(item);
+ }
+ } else {
+ // Item has already been deleted from the server, no need to change anything.
+ }
+ break;
+ }
+ },
+ async () => {
+ this.offlineCachedItems = {};
+ this.offlineCachedItemFlags = {};
+ await this.playbackOfflineItems();
+ clearPending();
+ resolve();
+ }
+ );
+ });
+ },
+
+ onOfflineStatusChanged(aNewState) {
+ if (aNewState) {
+ // Going offline: (XXX get items before going offline?) => we may ask the user to stay online a bit longer
+ } else if (!this.getProperty("disabled") && this.getProperty("refreshInterval") != "0") {
+ // Going online (start replaying changes to the remote calendar).
+ // Don't do this if the calendar is disabled or set to manual updates only.
+ this.refresh();
+ }
+ },
+
+ // aOldItem is already in the cache
+ async promptOverwrite(aMethod, aItem, aOldItem) {
+ let overwrite = cal.provider.promptOverwrite(aMethod, aItem);
+ if (overwrite) {
+ if (aMethod == "modify") {
+ await this.modifyOfflineItem(aItem, aOldItem);
+ } else {
+ await this.deleteOfflineItem(aItem);
+ }
+ }
+ },
+
+ /*
+ * Asynchronously performs playback operations of items added, modified, or deleted offline
+ *
+ * @param aPlaybackType (optional) The starting operation type. This function will be
+ * called recursively through playback operations in the order of
+ * add, modify, delete. By default playback will start with the add
+ * operation. Valid values for this parameter are defined as
+ * OFFLINE_FLAG_XXX constants in the calIChangeLog interface.
+ */
+ async playbackOfflineItems(aPlaybackType) {
+ let self = this;
+ let storage = this.mCachedCalendar.QueryInterface(Ci.calIOfflineStorage);
+
+ let itemQueue = [];
+ let debugOp;
+ let nextCallback;
+ let uncachedOp;
+ let filter;
+
+ aPlaybackType = aPlaybackType || cICL.OFFLINE_FLAG_CREATED_RECORD;
+ switch (aPlaybackType) {
+ case cICL.OFFLINE_FLAG_CREATED_RECORD:
+ debugOp = "add";
+ nextCallback = this.playbackOfflineItems.bind(this, cICL.OFFLINE_FLAG_MODIFIED_RECORD);
+ uncachedOp = item => this.mUncachedCalendar.addItem(item);
+ filter = calICalendar.ITEM_FILTER_OFFLINE_CREATED;
+ break;
+ case cICL.OFFLINE_FLAG_MODIFIED_RECORD:
+ debugOp = "modify";
+ nextCallback = this.playbackOfflineItems.bind(this, cICL.OFFLINE_FLAG_DELETED_RECORD);
+ uncachedOp = item => this.mUncachedCalendar.modifyItem(item, item);
+ filter = calICalendar.ITEM_FILTER_OFFLINE_MODIFIED;
+ break;
+ case cICL.OFFLINE_FLAG_DELETED_RECORD:
+ debugOp = "delete";
+ uncachedOp = item => this.mUncachedCalendar.deleteItem(item);
+ filter = calICalendar.ITEM_FILTER_OFFLINE_DELETED;
+ break;
+ default:
+ cal.ERROR("[calCachedCalendar] Invalid playback type: " + aPlaybackType);
+ return;
+ }
+
+ async function popItemQueue() {
+ if (!itemQueue || itemQueue.length == 0) {
+ // no items left in the queue, move on to the next operation
+ if (nextCallback) {
+ await nextCallback();
+ }
+ } else {
+ // perform operation on the next offline item in the queue
+ let item = itemQueue.pop();
+ let error = null;
+ try {
+ await uncachedOp(item);
+ } catch (e) {
+ error = e;
+ cal.ERROR(
+ "[calCachedCalendar] Could not perform playback operation " +
+ debugOp +
+ " for item " +
+ (item.title || " (none) ") +
+ ": " +
+ e
+ );
+ }
+ if (!error) {
+ if (aPlaybackType == cICL.OFFLINE_FLAG_DELETED_RECORD) {
+ self.mCachedCalendar.deleteItem(item);
+ } else {
+ storage.resetItemOfflineFlag(item);
+ }
+ } else {
+ // If the playback action could not be performed, then there
+ // is no need for further action. The item still has the
+ // offline flag, so it will be taken care of next time.
+ cal.WARN(
+ "[calCachedCalendar] Unable to perform playback action " +
+ debugOp +
+ " to the server, will try again next time (" +
+ item.id +
+ "," +
+ error +
+ ")"
+ );
+ }
+
+ // move on to the next item in the queue
+ await popItemQueue();
+ }
+ }
+
+ itemQueue = itemQueue.concat(
+ await this.mCachedCalendar.getItemsAsArray(
+ calICalendar.ITEM_FILTER_ALL_ITEMS | filter,
+ 0,
+ null,
+ null
+ )
+ );
+
+ if (this.offline) {
+ cal.LOG("[calCachedCalendar] back to offline mode, reconciliation aborted");
+ } else {
+ cal.LOG(
+ "[calCachedCalendar] Performing playback operation " +
+ debugOp +
+ " on " +
+ itemQueue.length +
+ " items to " +
+ self.name
+ );
+ // start the first operation
+ await popItemQueue();
+ }
+ },
+
+ get superCalendar() {
+ return (this.mSuperCalendar && this.mSuperCalendar.superCalendar) || this;
+ },
+ set superCalendar(val) {
+ this.mSuperCalendar = val;
+ },
+
+ get offline() {
+ return Services.io.offline;
+ },
+ get supportsChangeLog() {
+ return cal.wrapInstance(this.mUncachedCalendar, Ci.calIChangeLog) != null;
+ },
+
+ get canRefresh() {
+ // enable triggering sync using the reload button
+ return true;
+ },
+
+ get supportsScheduling() {
+ return this.mUncachedCalendar.supportsScheduling;
+ },
+
+ getSchedulingSupport() {
+ return this.mUncachedCalendar.getSchedulingSupport();
+ },
+
+ getProperty(aName) {
+ switch (aName) {
+ case "cache.enabled":
+ if (this.mUncachedCalendar.getProperty("cache.always")) {
+ return true;
+ }
+ break;
+ }
+
+ return this.mUncachedCalendar.getProperty(aName);
+ },
+ setProperty(aName, aValue) {
+ if (aName == "disabled") {
+ // Forward the disabled property to the storage calendar so that it
+ // stops interacting with the file system. Other properties have no
+ // useful effect on the storage calendar, so don't forward them.
+ this.mCachedCalendar.setProperty(aName, aValue);
+ }
+ this.mUncachedCalendar.setProperty(aName, aValue);
+ },
+ async refresh() {
+ if (this.offline) {
+ this.downstreamRefresh();
+ } else if (this.supportsChangeLog) {
+ /* we first ensure that any remaining offline items are reconciled with the calendar server */
+ await this.playbackOfflineItems();
+ await this.downstreamRefresh();
+ } else {
+ this.downstreamRefresh();
+ }
+ },
+ async downstreamRefresh() {
+ if (this.mUncachedCalendar.canRefresh && !this.offline) {
+ this.mUncachedCalendar.refresh(); // will trigger synchronize once the calendar is loaded
+ return;
+ }
+ await this.synchronize();
+ // fire completing onLoad for this refresh call
+ this.mCachedObserver.onLoad(this.mCachedCalendar);
+ },
+
+ addObserver(aObserver) {
+ this.mObservers.add(aObserver);
+ },
+ removeObserver(aObserver) {
+ this.mObservers.delete(aObserver);
+ },
+
+ async addItem(item) {
+ return this.adoptItem(item.clone());
+ },
+
+ async adoptItem(item) {
+ return new Promise((resolve, reject) => {
+ this.doAdoptItem(item, (calendar, status, opType, id, detail) => {
+ if (!Components.isSuccessCode(status)) {
+ return reject(new Components.Exception(detail, status));
+ }
+ return resolve(detail);
+ });
+ });
+ },
+
+ /**
+ * The function form of calIOperationListener.onOperationComplete used where
+ * the whole interface is not needed.
+ *
+ * @callback OnOperationCompleteHandler
+ *
+ * @param {calICalendar} calendar
+ * @param {number} status
+ * @param {number} operationType
+ * @param {string} id
+ * @param {calIItem|Error} detail
+ */
+
+ /**
+ * Keeps track of pending callbacks injected into the uncached calendar during
+ * adopt or modify operations. This is done to ensure we remove the correct
+ * callback when multiple operations occur at once.
+ *
+ * @type {OnOperationComplateHandler[]}
+ */
+ _injectedCallbacks: [],
+
+ /**
+ * Executes the actual addition of the item using either the cached or uncached
+ * calendar depending on offline state. A separate method is used here to
+ * preserve the order of the "onAddItem" event.
+ *
+ * @param {calIItem} item
+ * @param {OnOperationCompleteHandler} handler
+ */
+ doAdoptItem(item, listener) {
+ // Forwarding add/modify/delete to the cached calendar using the calIObserver
+ // callbacks would be advantageous, because the uncached provider could implement
+ // a true push mechanism firing without being triggered from within the program.
+ // But this would mean the uncached provider fires on the passed
+ // calIOperationListener, e.g. *before* it fires on calIObservers
+ // (because that order is undefined). Firing onOperationComplete before onAddItem et al
+ // would result in this facade firing onOperationComplete even though the modification
+ // hasn't yet been performed on the cached calendar (which happens in onAddItem et al).
+ // Result is that we currently stick to firing onOperationComplete if the cached calendar
+ // has performed the modification, see below:
+
+ let onSuccess = item => listener(item.calendar, Cr.NS_OK, cIOL.ADD, item.id, item);
+ let onError = e => listener(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e);
+
+ if (this.offline) {
+ // If we are offline, don't even try to add the item
+ this.adoptOfflineItem(item).then(onSuccess, onError);
+ } else {
+ // Otherwise ask the provider to add the item now.
+
+ // Expected to be called in the context of the uncached calendar's adoptItem()
+ // so this adoptItem() call returns first. This is a needed hack to keep the
+ // cached calendar's "onAddItem" event firing before the endBatch() call of
+ // the uncached calendar.
+ let adoptItemCallback = async (calendar, status, opType, id, detail) => {
+ if (isUnavailableCode(status)) {
+ // The item couldn't be added to the (remote) location,
+ // this is like being offline. Add the item to the cached
+ // calendar instead.
+ cal.LOG(
+ "[calCachedCalendar] Calendar " + calendar.name + " is unavailable, adding item offline"
+ );
+ await this.adoptOfflineItem(item).then(onSuccess, onError);
+ } else if (Components.isSuccessCode(status)) {
+ // On success, add the item to the cache.
+ await this.mCachedCalendar.addItem(detail).then(onSuccess, onError);
+ } else {
+ // Either an error occurred or this is a successful add
+ // to a cached calendar. Forward the call to the listener
+ listener(this, status, opType, id, detail);
+ }
+ this.mUncachedCalendar.wrappedJSObject._cachedAdoptItemCallback = null;
+ this._injectedCallbacks = this._injectedCallbacks.filter(cb => cb != adoptItemCallback);
+ };
+
+ // Store the callback so we can remove the correct one later.
+ this._injectedCallbacks.push(adoptItemCallback);
+
+ this.mUncachedCalendar.wrappedJSObject._cachedAdoptItemCallback = adoptItemCallback;
+ this.mUncachedCalendar.adoptItem(item).catch(e => {
+ adoptItemCallback(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e);
+ });
+ }
+ },
+
+ /**
+ * Adds an item to the cached (storage) calendar.
+ *
+ * @param {calIItem} item
+ * @returns {calIItem}
+ */
+ async adoptOfflineItem(item) {
+ let adoptedItem = await this.mCachedCalendar.adoptItem(item);
+ await this.mCachedCalendar.QueryInterface(Ci.calIOfflineStorage).addOfflineItem(adoptedItem);
+ return adoptedItem;
+ },
+
+ async modifyItem(newItem, oldItem) {
+ return new Promise((resolve, reject) => {
+ this.doModifyItem(newItem, oldItem, (calendar, status, opType, id, detail) => {
+ if (!Components.isSuccessCode(status)) {
+ return reject(new Components.Exception(detail, status));
+ }
+ return resolve(detail);
+ });
+ });
+ },
+
+ /**
+ * Executes the actual modification of the item using either the cached or
+ * uncached calendar depending on offline state. A separate method is used here
+ * to preserve the order of the "onModifyItem" event.
+ *
+ * @param {calIItem} newItem
+ * @param {calIItem} oldItem
+ * @param {OnOperationCompleteHandler} handler
+ */
+ doModifyItem(newItem, oldItem, listener) {
+ let onSuccess = item => listener(item.calendar, Cr.NS_OK, cIOL.MODIFY, item.id, item);
+ let onError = e => listener(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e);
+
+ // Forwarding add/modify/delete to the cached calendar using the calIObserver
+ // callbacks would be advantageous, because the uncached provider could implement
+ // a true push mechanism firing without being triggered from within the program.
+ // But this would mean the uncached provider fires on the passed
+ // calIOperationListener, e.g. *before* it fires on calIObservers
+ // (because that order is undefined). Firing onOperationComplete before onAddItem et al
+ // would result in this facade firing onOperationComplete even though the modification
+ // hasn't yet been performed on the cached calendar (which happens in onAddItem et al).
+ // Result is that we currently stick to firing onOperationComplete if the cached calendar
+ // has performed the modification, see below: */
+
+ // Expected to be called in the context of the uncached calendar's modifyItem()
+ // so this modifyItem() call returns first. This is a needed hack to keep the
+ // cached calendar's "onModifyItem" event firing before the endBatch() call of
+ // the uncached calendar.
+ let modifyItemCallback = async (calendar, status, opType, id, detail) => {
+ // Returned Promise only available through wrappedJSObject.
+ if (isUnavailableCode(status)) {
+ // The item couldn't be modified at the (remote) location,
+ // this is like being offline. Add the item to the cache
+ // instead.
+ cal.LOG(
+ "[calCachedCalendar] Calendar " +
+ calendar.name +
+ " is unavailable, modifying item offline"
+ );
+ await this.modifyOfflineItem(newItem, oldItem).then(onSuccess, onError);
+ } else if (Components.isSuccessCode(status)) {
+ // On success, modify the item in the cache
+ await this.mCachedCalendar.modifyItem(detail, oldItem).then(onSuccess, onError);
+ } else {
+ // This happens on error, forward the error through the listener
+ listener(this, status, opType, id, detail);
+ }
+ this._injectedCallbacks = this._injectedCallbacks.filter(cb => cb != modifyItemCallback);
+ };
+
+ // First of all, we should find out if the item to modify is
+ // already an offline item or not.
+ if (this.offline) {
+ // If we are offline, don't even try to modify the item
+ this.modifyOfflineItem(newItem, oldItem).then(onSuccess, onError);
+ } else {
+ // Otherwise, get the item flags and further process the item.
+ this.mCachedCalendar.getItemOfflineFlag(oldItem).then(offline_flag => {
+ if (
+ offline_flag == cICL.OFFLINE_FLAG_CREATED_RECORD ||
+ offline_flag == cICL.OFFLINE_FLAG_MODIFIED_RECORD
+ ) {
+ // The item is already offline, just modify it in the cache
+ this.modifyOfflineItem(newItem, oldItem).then(onSuccess, onError);
+ } else {
+ // Not an offline item, attempt to modify using provider
+
+ // This is a needed hack to keep the cached calendar's "onModifyItem" event
+ // firing before the endBatch() call of the uncached calendar. It is called
+ // in mUncachedCalendar's modifyItem() method.
+ this.mUncachedCalendar.wrappedJSObject._cachedModifyItemCallback = modifyItemCallback;
+
+ // Store the callback so we can remove the correct one later.
+ this._injectedCallbacks.push(modifyItemCallback);
+
+ this.mUncachedCalendar.modifyItem(newItem, oldItem).catch(e => {
+ modifyItemCallback(null, e.result || Cr.NS_ERROR_FAILURE, null, null, e);
+ });
+ }
+ });
+ }
+ },
+
+ /**
+ * Modifies an item in the cached calendar.
+ *
+ * @param {calIItem} newItem
+ * @param {calIItem} oldItem
+ * @returns {calIItem}
+ */
+ async modifyOfflineItem(newItem, oldItem) {
+ let modifiedItem = await this.mCachedCalendar.modifyItem(newItem, oldItem);
+ await this.mCachedCalendar
+ .QueryInterface(Ci.calIOfflineStorage)
+ .modifyOfflineItem(modifiedItem);
+ return modifiedItem;
+ },
+
+ async deleteItem(item) {
+ // First of all, we should find out if the item to delete is
+ // already an offline item or not.
+ if (this.offline) {
+ // If we are offline, don't even try to delete the item
+ await this.deleteOfflineItem(item);
+ } else {
+ // Otherwise, get the item flags, the listener will further
+ // process the item.
+ let offline_flag = await this.mCachedCalendar.getItemOfflineFlag(item);
+ if (
+ offline_flag == cICL.OFFLINE_FLAG_CREATED_RECORD ||
+ offline_flag == cICL.OFFLINE_FLAG_MODIFIED_RECORD
+ ) {
+ // The item is already offline, just mark it deleted it in
+ // the cache
+ await this.deleteOfflineItem(item);
+ } else {
+ try {
+ // Not an offline item, attempt to delete using provider
+ await this.mUncachedCalendar.deleteItem(item);
+
+ // On success, delete the item from the cache
+ await this.mCachedCalendar.deleteItem(item);
+
+ // Also, remove any meta data associated with the item
+ try {
+ this.mCachedCalendar.QueryInterface(Ci.calISyncWriteCalendar).deleteMetaData(item.id);
+ } catch (e) {
+ cal.LOG("[calCachedCalendar] Offline storage doesn't support metadata");
+ }
+ } catch (e) {
+ if (isUnavailableCode(e.result)) {
+ // The item couldn't be deleted at the (remote) location,
+ // this is like being offline. Mark the item deleted in the
+ // cache instead.
+ cal.LOG(
+ "[calCachedCalendar] Calendar " +
+ item.calendar.name +
+ " is unavailable, deleting item offline"
+ );
+ await this.deleteOfflineItem(item);
+ }
+ }
+ }
+ }
+ },
+
+ async deleteOfflineItem(item) {
+ /* We do not delete the item from the cache, as we will need it when reconciling the cache content and the server content. */
+ return this.mCachedCalendar.QueryInterface(Ci.calIOfflineStorage).deleteOfflineItem(item);
+ },
+};
+(function () {
+ function defineForwards(proto, targetName, functions, getters, gettersAndSetters) {
+ function defineForwardGetter(attr) {
+ proto.__defineGetter__(attr, function () {
+ return this[targetName][attr];
+ });
+ }
+ function defineForwardGetterAndSetter(attr) {
+ defineForwardGetter(attr);
+ proto.__defineSetter__(attr, function (value) {
+ return (this[targetName][attr] = value);
+ });
+ }
+ function defineForwardFunction(funcName) {
+ proto[funcName] = function (...args) {
+ let obj = this[targetName];
+ return obj[funcName](...args);
+ };
+ }
+ functions.forEach(defineForwardFunction);
+ getters.forEach(defineForwardGetter);
+ gettersAndSetters.forEach(defineForwardGetterAndSetter);
+ }
+
+ defineForwards(
+ calCachedCalendar.prototype,
+ "mUncachedCalendar",
+ ["deleteProperty", "isInvitation", "getInvitedAttendee", "canNotify"],
+ ["providerID", "type", "aclManager", "aclEntry"],
+ ["id", "name", "uri", "readOnly"]
+ );
+ defineForwards(
+ calCachedCalendar.prototype,
+ "mCachedCalendar",
+ ["getItem", "getItems", "getItemsAsArray", "startBatch", "endBatch"],
+ [],
+ []
+ );
+})();
diff --git a/comm/calendar/base/src/calICSService-worker.js b/comm/calendar/base/src/calICSService-worker.js
new file mode 100644
index 0000000000..83d8c2c088
--- /dev/null
+++ b/comm/calendar/base/src/calICSService-worker.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+/**
+ * ChromeWorker for parseICSAsync method in CalICSService.jsm
+ */
+
+/* eslint-env worker */
+/* import-globals-from ../modules/Ical.jsm */
+
+// eslint-disable-next-line no-unused-vars
+importScripts("resource:///modules/calendar/Ical.jsm");
+
+ICAL.design.strict = false;
+
+self.onmessage = function (event) {
+ let comp = ICAL.parse(event.data);
+ postMessage(comp);
+ self.close();
+};
diff --git a/comm/calendar/base/src/calInternalInterfaces.idl b/comm/calendar/base/src/calInternalInterfaces.idl
new file mode 100644
index 0000000000..339b935448
--- /dev/null
+++ b/comm/calendar/base/src/calInternalInterfaces.idl
@@ -0,0 +1,29 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/** Don't use these if you're not the calendar glue code! **/
+
+#include "nsISupports.idl"
+
+interface calIItemBase;
+interface calIDateTime;
+
+[scriptable, uuid(1903648f-a0ee-4ae1-84b0-d8e8d0b10506)]
+interface calIInternalShallowCopy : nsISupports
+{
+ /**
+ * create a proxy for this item; the returned item
+ * proxy will have parentItem set to this instance.
+ *
+ * @param aRecurrenceId RECURRENCE-ID of the proxy to be created
+ */
+ calIItemBase createProxy(in calIDateTime aRecurrenceId);
+
+ // used by recurrenceInfo when cloning proxy objects to
+ // avoid an infinite loop. aNewParent is optional, and is
+ // used to set the parent of the new item; it should be null
+ // if no new parent is passed in.
+ calIItemBase cloneShallow(in calIItemBase aNewParent);
+};
diff --git a/comm/calendar/base/src/calItemBase.js b/comm/calendar/base/src/calItemBase.js
new file mode 100644
index 0000000000..d1cd1599e5
--- /dev/null
+++ b/comm/calendar/base/src/calItemBase.js
@@ -0,0 +1,1198 @@
+/* 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/. */
+
+/* exported makeMemberAttr, makeMemberAttrProperty */
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { CalAttendee } = ChromeUtils.import("resource:///modules/CalAttendee.jsm");
+var { CalRelation } = ChromeUtils.import("resource:///modules/CalRelation.jsm");
+var { CalAttachment } = ChromeUtils.import("resource:///modules/CalAttachment.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalAlarm: "resource:///modules/CalAlarm.jsm",
+ CalDateTime: "resource:///modules/CalDateTime.jsm",
+ CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gParserUtils",
+ "@mozilla.org/parserutils;1",
+ "nsIParserUtils"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gTextToHtmlConverter",
+ "@mozilla.org/txttohtmlconv;1",
+ "mozITXTToHTMLConv"
+);
+
+/**
+ * calItemBase prototype definition
+ *
+ * @implements calIItemBase
+ * @class
+ */
+function calItemBase() {
+ cal.ASSERT(false, "Inheriting objects call initItemBase()!");
+}
+
+calItemBase.prototype = {
+ mProperties: null,
+ mPropertyParams: null,
+
+ mIsProxy: false,
+ mHashId: null,
+ mImmutable: false,
+ mDirty: false,
+ mCalendar: null,
+ mParentItem: null,
+ mRecurrenceInfo: null,
+ mOrganizer: null,
+
+ mAlarms: null,
+ mAlarmLastAck: null,
+
+ mAttendees: null,
+ mAttachments: null,
+ mRelations: null,
+ mCategories: null,
+
+ mACLEntry: null,
+
+ /**
+ * Initialize the base item's attributes. Can be called from inheriting
+ * objects in their constructor.
+ */
+ initItemBase() {
+ this.wrappedJSObject = this;
+ this.mProperties = new Map();
+ this.mPropertyParams = {};
+ this.setProperty("CREATED", cal.dtz.jsDateToDateTime(new Date()));
+ },
+
+ /**
+ * @see nsISupports
+ */
+ QueryInterface: ChromeUtils.generateQI(["calIItemBase"]),
+
+ /**
+ * @see calIItemBase
+ */
+ get aclEntry() {
+ let aclEntry = this.mACLEntry;
+ let aclManager = this.calendar && this.calendar.superCalendar.aclManager;
+
+ if (!aclEntry && aclManager) {
+ this.mACLEntry = aclManager.getItemEntry(this);
+ aclEntry = this.mACLEntry;
+ }
+
+ if (!aclEntry && this.parentItem != this) {
+ // No ACL entry on this item, check the parent
+ aclEntry = this.parentItem.aclEntry;
+ }
+
+ return aclEntry;
+ },
+
+ // readonly attribute AUTF8String hashId;
+ get hashId() {
+ if (this.mHashId === null) {
+ let rid = this.recurrenceId;
+ let calendar = this.calendar;
+ // some unused delim character:
+ this.mHashId = [
+ encodeURIComponent(this.id),
+ rid ? rid.getInTimezone(cal.dtz.UTC).icalString : "",
+ calendar ? encodeURIComponent(calendar.id) : "",
+ ].join("#");
+ }
+ return this.mHashId;
+ },
+
+ // attribute AUTF8String id;
+ get id() {
+ return this.getProperty("UID");
+ },
+ set id(uid) {
+ this.mHashId = null; // recompute hashId
+ this.setProperty("UID", uid);
+ if (this.mRecurrenceInfo) {
+ this.mRecurrenceInfo.onIdChange(uid);
+ }
+ },
+
+ // attribute calIDateTime recurrenceId;
+ get recurrenceId() {
+ return this.getProperty("RECURRENCE-ID");
+ },
+ set recurrenceId(rid) {
+ this.mHashId = null; // recompute hashId
+ this.setProperty("RECURRENCE-ID", rid);
+ },
+
+ // attribute calIRecurrenceInfo recurrenceInfo;
+ get recurrenceInfo() {
+ return this.mRecurrenceInfo;
+ },
+ set recurrenceInfo(value) {
+ this.modify();
+ this.mRecurrenceInfo = cal.unwrapInstance(value);
+ },
+
+ // attribute calIItemBase parentItem;
+ get parentItem() {
+ return this.mParentItem || this;
+ },
+ set parentItem(value) {
+ if (this.mImmutable) {
+ throw Components.Exception("", Cr.NS_ERROR_OBJECT_IS_IMMUTABLE);
+ }
+ this.mParentItem = cal.unwrapInstance(value);
+ },
+
+ /**
+ * Initializes the base item to be an item proxy. Used by inheriting
+ * objects createProxy() method.
+ *
+ * XXXdbo Explain proxy a bit better, either here or in
+ * calIInternalShallowCopy.
+ *
+ * @see calIInternalShallowCopy
+ * @param aParentItem The parent item to initialize the proxy on.
+ * @param aRecurrenceId The recurrence id to initialize the proxy for.
+ */
+ initializeProxy(aParentItem, aRecurrenceId) {
+ this.mIsProxy = true;
+
+ aParentItem = cal.unwrapInstance(aParentItem);
+ this.mParentItem = aParentItem;
+ this.mCalendar = aParentItem.mCalendar;
+ this.recurrenceId = aRecurrenceId;
+
+ // Make sure organizer is unset, as the getter checks for this.
+ this.mOrganizer = undefined;
+
+ this.mImmutable = aParentItem.mImmutable;
+ },
+
+ // readonly attribute boolean isMutable;
+ get isMutable() {
+ return !this.mImmutable;
+ },
+
+ /**
+ * This function should be called by all members that modify the item. It
+ * checks if the item is immutable and throws accordingly, and sets the
+ * mDirty property.
+ */
+ modify() {
+ if (this.mImmutable) {
+ throw Components.Exception("", Cr.NS_ERROR_OBJECT_IS_IMMUTABLE);
+ }
+ this.mDirty = true;
+ },
+
+ /**
+ * Makes sure the item is not dirty. If the item is dirty, properties like
+ * LAST-MODIFIED and DTSTAMP are set to now.
+ */
+ ensureNotDirty() {
+ if (this.mDirty) {
+ let now = cal.dtz.jsDateToDateTime(new Date());
+ this.setProperty("LAST-MODIFIED", now);
+ this.setProperty("DTSTAMP", now);
+ this.mDirty = false;
+ }
+ },
+
+ /**
+ * Makes all properties of the base item immutable. Can be called by
+ * inheriting objects' makeImmutable method.
+ */
+ makeItemBaseImmutable() {
+ if (this.mImmutable) {
+ return;
+ }
+
+ // make all our components immutable
+ if (this.mRecurrenceInfo) {
+ this.mRecurrenceInfo.makeImmutable();
+ }
+
+ if (this.mOrganizer) {
+ this.mOrganizer.makeImmutable();
+ }
+ if (this.mAttendees) {
+ for (let att of this.mAttendees) {
+ att.makeImmutable();
+ }
+ }
+
+ for (let propValue of this.mProperties.values()) {
+ if (propValue?.isMutable) {
+ propValue.makeImmutable();
+ }
+ }
+
+ if (this.mAlarms) {
+ for (let alarm of this.mAlarms) {
+ alarm.makeImmutable();
+ }
+ }
+
+ if (this.mAlarmLastAck) {
+ this.mAlarmLastAck.makeImmutable();
+ }
+
+ this.ensureNotDirty();
+ this.mImmutable = true;
+ },
+
+ // boolean hasSameIds(in calIItemBase aItem);
+ hasSameIds(that) {
+ return (
+ that &&
+ this.id == that.id &&
+ (this.recurrenceId == that.recurrenceId || // both null
+ (this.recurrenceId &&
+ that.recurrenceId &&
+ this.recurrenceId.compare(that.recurrenceId) == 0))
+ );
+ },
+
+ /**
+ * Overridden by CalEvent to indicate the item is an event.
+ */
+ isEvent() {
+ return false;
+ },
+
+ /**
+ * Overridden by CalTodo to indicate the item is a todo.
+ */
+ isTodo() {
+ return false;
+ },
+
+ // calIItemBase clone();
+ clone() {
+ return this.cloneShallow(this.mParentItem);
+ },
+
+ /**
+ * Clones the base item's properties into the passed object, potentially
+ * setting a new parent item.
+ *
+ * @param m The item to clone this item into
+ * @param aNewParent (optional) The new parent item to set on m.
+ */
+ cloneItemBaseInto(cloned, aNewParent) {
+ cloned.mImmutable = false;
+ cloned.mACLEntry = this.mACLEntry;
+ cloned.mIsProxy = this.mIsProxy;
+ cloned.mParentItem = cal.unwrapInstance(aNewParent) || this.mParentItem;
+ cloned.mHashId = this.mHashId;
+ cloned.mCalendar = this.mCalendar;
+ if (this.mRecurrenceInfo) {
+ cloned.mRecurrenceInfo = cal.unwrapInstance(this.mRecurrenceInfo.clone());
+ cloned.mRecurrenceInfo.item = cloned;
+ }
+
+ let org = this.organizer;
+ if (org) {
+ org = org.clone();
+ }
+ cloned.mOrganizer = org;
+
+ cloned.mAttendees = [];
+ for (let att of this.getAttendees()) {
+ cloned.mAttendees.push(att.clone());
+ }
+
+ cloned.mProperties = new Map();
+ for (let [name, value] of this.mProperties.entries()) {
+ if (value instanceof CalDateTime || value instanceof Ci.calIDateTime) {
+ value = value.clone();
+ }
+
+ cloned.mProperties.set(name, value);
+
+ let propBucket = this.mPropertyParams[name];
+ if (propBucket) {
+ let newBucket = {};
+ for (let param in propBucket) {
+ newBucket[param] = propBucket[param];
+ }
+ cloned.mPropertyParams[name] = newBucket;
+ }
+ }
+
+ cloned.mAttachments = [];
+ for (let att of this.getAttachments()) {
+ cloned.mAttachments.push(att.clone());
+ }
+
+ cloned.mRelations = [];
+ for (let rel of this.getRelations()) {
+ cloned.mRelations.push(rel.clone());
+ }
+
+ cloned.mCategories = this.getCategories();
+
+ cloned.mAlarms = [];
+ for (let alarm of this.getAlarms()) {
+ // Clone alarms into new item, assume the alarms from the old item
+ // are valid and don't need validation.
+ cloned.mAlarms.push(alarm.clone());
+ }
+
+ let alarmLastAck = this.alarmLastAck;
+ if (alarmLastAck) {
+ alarmLastAck = alarmLastAck.clone();
+ }
+ cloned.mAlarmLastAck = alarmLastAck;
+
+ cloned.mDirty = this.mDirty;
+
+ return cloned;
+ },
+
+ // attribute calIDateTime alarmLastAck;
+ get alarmLastAck() {
+ return this.mAlarmLastAck;
+ },
+ set alarmLastAck(aValue) {
+ this.modify();
+ if (aValue && !aValue.timezone.isUTC) {
+ aValue = aValue.getInTimezone(cal.dtz.UTC);
+ }
+ this.mAlarmLastAck = aValue;
+ },
+
+ // readonly attribute calIDateTime lastModifiedTime;
+ get lastModifiedTime() {
+ this.ensureNotDirty();
+ return this.getProperty("LAST-MODIFIED");
+ },
+
+ // readonly attribute calIDateTime stampTime;
+ get stampTime() {
+ this.ensureNotDirty();
+ return this.getProperty("DTSTAMP");
+ },
+
+ // attribute AUTF8string descriptionText;
+ get descriptionText() {
+ return this.getProperty("DESCRIPTION");
+ },
+
+ set descriptionText(text) {
+ this.setProperty("DESCRIPTION", text);
+ if (text) {
+ this.setPropertyParameter("DESCRIPTION", "ALTREP", null);
+ } // else: property parameter deleted by setProperty(..., null)
+ },
+
+ // attribute AUTF8string descriptionHTML;
+ get descriptionHTML() {
+ let altrep = this.getPropertyParameter("DESCRIPTION", "ALTREP");
+ if (altrep?.startsWith("data:text/html,")) {
+ try {
+ return decodeURIComponent(altrep.slice("data:text/html,".length));
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ // Fallback: Upconvert the plaintext
+ let description = this.getProperty("DESCRIPTION");
+ if (!description) {
+ return null;
+ }
+ let mode = Ci.mozITXTToHTMLConv.kStructPhrase | Ci.mozITXTToHTMLConv.kURLs;
+ description = gTextToHtmlConverter.scanTXT(description, mode);
+ return description.replace(/\r?\n/g, "<br>");
+ },
+
+ set descriptionHTML(html) {
+ if (html) {
+ // We need to output a plaintext version of the description, even if we're
+ // using the ALTREP parameter. We use the "preformatted" option in case
+ // the HTML contains a <pre/> tag with newlines.
+ let mode =
+ Ci.nsIDocumentEncoder.OutputDropInvisibleBreak |
+ Ci.nsIDocumentEncoder.OutputLFLineBreak |
+ Ci.nsIDocumentEncoder.OutputPreformatted;
+ let text = gParserUtils.convertToPlainText(html, mode, 0);
+
+ this.setProperty("DESCRIPTION", text);
+
+ // If the text is non-empty, create a standard ALTREP representation of
+ // the description as HTML.
+ // N.B. There's logic in nsMsgCompose for determining if HTML is
+ // convertible to plaintext without losing formatting. We could test if we
+ // could leave this part off if we generalized that logic.
+ if (text) {
+ this.setPropertyParameter(
+ "DESCRIPTION",
+ "ALTREP",
+ "data:text/html," + encodeURIComponent(html)
+ );
+ }
+ } else {
+ this.deleteProperty("DESCRIPTION");
+ }
+ },
+
+ // Each inner array has two elements: a string and a nsIVariant.
+ // readonly attribute Array<Array<jsval> > properties;
+ get properties() {
+ let properties = this.mProperties;
+ if (this.mIsProxy) {
+ let parentProperties = this.mParentItem.wrappedJSObject.mProperties;
+ let thisProperties = this.mProperties;
+ properties = new Map(
+ (function* () {
+ yield* parentProperties;
+ yield* thisProperties;
+ })()
+ );
+ }
+
+ return [...properties.entries()];
+ },
+
+ // nsIVariant getProperty(in AString name);
+ getProperty(aName) {
+ let name = aName.toUpperCase();
+ if (this.mProperties.has(name)) {
+ return this.mProperties.get(name);
+ }
+ return this.mIsProxy ? this.mParentItem.getProperty(name) : null;
+ },
+
+ // boolean hasProperty(in AString name);
+ hasProperty(aName) {
+ return this.getProperty(aName) != null;
+ },
+
+ // void setProperty(in AString name, in nsIVariant value);
+ setProperty(aName, aValue) {
+ this.modify();
+ aName = aName.toUpperCase();
+ if (aValue || !isNaN(parseInt(aValue, 10))) {
+ this.mProperties.set(aName, aValue);
+ if (!(aName in this.mPropertyParams)) {
+ this.mPropertyParams[aName] = {};
+ }
+ } else {
+ this.deleteProperty(aName);
+ }
+ if (aName == "LAST-MODIFIED") {
+ // setting LAST-MODIFIED cleans/undirties the item, we use this for preserving DTSTAMP
+ this.mDirty = false;
+ }
+ },
+
+ // void deleteProperty(in AString name);
+ deleteProperty(aName) {
+ this.modify();
+ aName = aName.toUpperCase();
+ if (this.mIsProxy) {
+ // deleting a proxy's property will mark the bag's item as null, so we could
+ // distinguish it when enumerating/getting properties from the undefined ones.
+ this.mProperties.set(aName, null);
+ } else {
+ this.mProperties.delete(aName);
+ }
+ delete this.mPropertyParams[aName];
+ },
+
+ // AString getPropertyParameter(in AString aPropertyName,
+ // in AString aParameterName);
+ getPropertyParameter(aPropName, aParamName) {
+ let propName = aPropName.toUpperCase();
+ let paramName = aParamName.toUpperCase();
+ if (propName in this.mPropertyParams) {
+ if (paramName in this.mPropertyParams[propName]) {
+ // If the property is not in mPropertyParams, then this just means
+ // there are no properties set.
+ return this.mPropertyParams[propName][paramName];
+ }
+ return null;
+ }
+ return this.mIsProxy ? this.mParentItem.getPropertyParameter(propName, paramName) : null;
+ },
+
+ // boolean hasPropertyParameter(in AString aPropertyName,
+ // in AString aParameterName);
+ hasPropertyParameter(aPropName, aParamName) {
+ return this.getPropertyParameter(aPropName, aParamName) != null;
+ },
+
+ // void setPropertyParameter(in AString aPropertyName,
+ // in AString aParameterName,
+ // in AUTF8String aParameterValue);
+ setPropertyParameter(aPropName, aParamName, aParamValue) {
+ let propName = aPropName.toUpperCase();
+ let paramName = aParamName.toUpperCase();
+ this.modify();
+ if (!(propName in this.mPropertyParams)) {
+ if (this.hasProperty(propName)) {
+ this.mPropertyParams[propName] = {};
+ } else {
+ throw new Error("Property " + aPropName + " not set");
+ }
+ }
+ if (aParamValue || !isNaN(parseInt(aParamValue, 10))) {
+ this.mPropertyParams[propName][paramName] = aParamValue;
+ } else {
+ delete this.mPropertyParams[propName][paramName];
+ }
+ return aParamValue;
+ },
+
+ // Array<AString> getParameterNames(in AString aPropertyName);
+ getParameterNames(aPropName) {
+ let propName = aPropName.toUpperCase();
+ if (!(propName in this.mPropertyParams)) {
+ if (this.mIsProxy) {
+ return this.mParentItem.getParameterNames(aPropName);
+ }
+ throw new Error("Property " + aPropName + " not set");
+ }
+ return Object.keys(this.mPropertyParams[propName]);
+ },
+
+ // Array<calIAttendee> getAttendees();
+ getAttendees() {
+ if (!this.mAttendees && this.mIsProxy) {
+ this.mAttendees = this.mParentItem.getAttendees();
+ }
+ if (this.mAttendees) {
+ return Array.from(this.mAttendees); // clone
+ }
+ return [];
+ },
+
+ // calIAttendee getAttendeeById(in AUTF8String id);
+ getAttendeeById(id) {
+ let attendees = this.getAttendees();
+ let lowerCaseId = id.toLowerCase();
+ for (let attendee of attendees) {
+ // This match must be case insensitive to deal with differing
+ // cases of things like MAILTO:
+ if (attendee.id.toLowerCase() == lowerCaseId) {
+ return attendee;
+ }
+ }
+ return null;
+ },
+
+ // void removeAttendee(in calIAttendee attendee);
+ removeAttendee(attendee) {
+ this.modify();
+ let found = false,
+ newAttendees = [];
+ let attendees = this.getAttendees();
+ let attIdLowerCase = attendee.id.toLowerCase();
+
+ for (let i = 0; i < attendees.length; i++) {
+ if (attendees[i].id.toLowerCase() == attIdLowerCase) {
+ found = true;
+ } else {
+ newAttendees.push(attendees[i]);
+ }
+ }
+ if (found) {
+ this.mAttendees = newAttendees;
+ }
+ },
+
+ // void removeAllAttendees();
+ removeAllAttendees() {
+ this.modify();
+ this.mAttendees = [];
+ },
+
+ // void addAttendee(in calIAttendee attendee);
+ addAttendee(attendee) {
+ if (!attendee.id) {
+ cal.LOG("Tried to add invalid attended");
+ return;
+ }
+ // the duplicate check is migration code for bug 1204255
+ let exists = this.getAttendeeById(attendee.id);
+ if (exists) {
+ cal.LOG(
+ "Ignoring attendee duplicate for item " + this.id + " (" + this.title + "): " + exists.id
+ );
+ if (
+ exists.participationStatus == "NEEDS-ACTION" ||
+ attendee.participationStatus == "DECLINED"
+ ) {
+ this.removeAttendee(exists);
+ } else {
+ attendee = null;
+ }
+ }
+ if (attendee) {
+ if (attendee.commonName) {
+ // migration code for bug 1209399 to remove leading/training double quotes in
+ let commonName = attendee.commonName.replace(/^["]*([^"]*)["]*$/, "$1");
+ if (commonName.length == 0) {
+ commonName = null;
+ }
+ if (commonName != attendee.commonName) {
+ if (attendee.isMutable) {
+ attendee.commonName = commonName;
+ } else {
+ cal.LOG(
+ "Failed to cleanup malformed commonName for immutable attendee " +
+ attendee.toString() +
+ "\n" +
+ cal.STACK(20)
+ );
+ }
+ }
+ }
+ this.modify();
+ this.mAttendees = this.getAttendees();
+ this.mAttendees.push(attendee);
+ }
+ },
+
+ // Array<calIAttachment> getAttachments();
+ getAttachments() {
+ if (!this.mAttachments && this.mIsProxy) {
+ this.mAttachments = this.mParentItem.getAttachments();
+ }
+ if (this.mAttachments) {
+ return this.mAttachments.concat([]); // clone
+ }
+ return [];
+ },
+
+ // void removeAttachment(in calIAttachment attachment);
+ removeAttachment(aAttachment) {
+ this.modify();
+ for (let attIndex in this.mAttachments) {
+ if (cal.data.compareObjects(this.mAttachments[attIndex], aAttachment, Ci.calIAttachment)) {
+ this.modify();
+ this.mAttachments.splice(attIndex, 1);
+ break;
+ }
+ }
+ },
+
+ // void addAttachment(in calIAttachment attachment);
+ addAttachment(attachment) {
+ this.modify();
+ this.mAttachments = this.getAttachments();
+ if (!this.mAttachments.some(x => x.hashId == attachment.hashId)) {
+ this.mAttachments.push(attachment);
+ }
+ },
+
+ // void removeAllAttachments();
+ removeAllAttachments() {
+ this.modify();
+ this.mAttachments = [];
+ },
+
+ // Array<calIRelation> getRelations();
+ getRelations() {
+ if (!this.mRelations && this.mIsProxy) {
+ this.mRelations = this.mParentItem.getRelations();
+ }
+ if (this.mRelations) {
+ return this.mRelations.concat([]);
+ }
+ return [];
+ },
+
+ // void removeRelation(in calIRelation relation);
+ removeRelation(aRelation) {
+ this.modify();
+ for (let attIndex in this.mRelations) {
+ // Could we have the same item as parent and as child ?
+ if (
+ this.mRelations[attIndex].relId == aRelation.relId &&
+ this.mRelations[attIndex].relType == aRelation.relType
+ ) {
+ this.modify();
+ this.mRelations.splice(attIndex, 1);
+ break;
+ }
+ }
+ },
+
+ // void addRelation(in calIRelation relation);
+ addRelation(aRelation) {
+ this.modify();
+ this.mRelations = this.getRelations();
+ this.mRelations.push(aRelation);
+ // XXX ensure that the relation isn't already there?
+ },
+
+ // void removeAllRelations();
+ removeAllRelations() {
+ this.modify();
+ this.mRelations = [];
+ },
+
+ // attribute calICalendar calendar;
+ get calendar() {
+ if (!this.mCalendar && this.parentItem != this) {
+ return this.parentItem.calendar;
+ }
+ return this.mCalendar;
+ },
+ set calendar(calendar) {
+ if (this.mImmutable) {
+ throw Components.Exception("", Cr.NS_ERROR_OBJECT_IS_IMMUTABLE);
+ }
+ this.mHashId = null; // recompute hashId
+ this.mCalendar = calendar;
+ },
+
+ // attribute calIAttendee organizer;
+ get organizer() {
+ if (this.mIsProxy && this.mOrganizer === undefined) {
+ return this.mParentItem.organizer;
+ }
+ return this.mOrganizer;
+ },
+ set organizer(organizer) {
+ this.modify();
+ this.mOrganizer = organizer;
+ },
+
+ // Array<AString> getCategories();
+ getCategories() {
+ if (!this.mCategories && this.mIsProxy) {
+ this.mCategories = this.mParentItem.getCategories();
+ }
+ if (this.mCategories) {
+ return this.mCategories.concat([]); // clone
+ }
+ return [];
+ },
+
+ // void setCategories(in Array<AString> aCategories);
+ setCategories(aCategories) {
+ this.modify();
+ this.mCategories = aCategories.concat([]);
+ },
+
+ // attribute AUTF8String icalString;
+ get icalString() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ set icalString(str) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ /**
+ * The map of promoted properties is a list of those properties that are
+ * represented directly by getters/setters.
+ * All of these property names must be in upper case isPropertyPromoted to
+ * function correctly. The has/get/set/deleteProperty interfaces
+ * are case-insensitive, but these are not.
+ */
+ itemBasePromotedProps: {
+ CREATED: true,
+ UID: true,
+ "LAST-MODIFIED": true,
+ SUMMARY: true,
+ PRIORITY: true,
+ STATUS: true,
+ DTSTAMP: true,
+ RRULE: true,
+ EXDATE: true,
+ RDATE: true,
+ ATTENDEE: true,
+ ATTACH: true,
+ CATEGORIES: true,
+ ORGANIZER: true,
+ "RECURRENCE-ID": true,
+ "X-MOZ-LASTACK": true,
+ "RELATED-TO": true,
+ },
+
+ /**
+ * A map of properties that need translation between the ical component
+ * property and their ICS counterpart.
+ */
+ icsBasePropMap: [
+ { cal: "CREATED", ics: "createdTime" },
+ { cal: "LAST-MODIFIED", ics: "lastModified" },
+ { cal: "DTSTAMP", ics: "stampTime" },
+ { cal: "UID", ics: "uid" },
+ { cal: "SUMMARY", ics: "summary" },
+ { cal: "PRIORITY", ics: "priority" },
+ { cal: "STATUS", ics: "status" },
+ { cal: "RECURRENCE-ID", ics: "recurrenceId" },
+ ],
+
+ /**
+ * Walks through the propmap and sets all properties on this item from the
+ * given icalcomp.
+ *
+ * @param icalcomp The calIIcalComponent to read from.
+ * @param propmap The property map to walk through.
+ */
+ mapPropsFromICS(icalcomp, propmap) {
+ for (let i = 0; i < propmap.length; i++) {
+ let prop = propmap[i];
+ let val = icalcomp[prop.ics];
+ if (val != null && val != Ci.calIIcalComponent.INVALID_VALUE) {
+ this.setProperty(prop.cal, val);
+ }
+ }
+ },
+
+ /**
+ * Walks through the propmap and sets all properties on the given icalcomp
+ * from the properties set on this item.
+ * given icalcomp.
+ *
+ * @param icalcomp The calIIcalComponent to write to.
+ * @param propmap The property map to walk through.
+ */
+ mapPropsToICS(icalcomp, propmap) {
+ for (let i = 0; i < propmap.length; i++) {
+ let prop = propmap[i];
+ let val = this.getProperty(prop.cal);
+ if (val != null && val != Ci.calIIcalComponent.INVALID_VALUE) {
+ icalcomp[prop.ics] = val;
+ }
+ }
+ },
+
+ /**
+ * Reads an ical component and sets up the base item's properties to match
+ * it.
+ *
+ * @param icalcomp The ical component to read.
+ */
+ setItemBaseFromICS(icalcomp) {
+ this.modify();
+
+ // re-initializing from scratch -- no light proxy anymore:
+ this.mIsProxy = false;
+ this.mProperties = new Map();
+ this.mPropertyParams = {};
+
+ this.mapPropsFromICS(icalcomp, this.icsBasePropMap);
+
+ this.mAttendees = []; // don't inherit anything from parent
+ for (let attprop of cal.iterate.icalProperty(icalcomp, "ATTENDEE")) {
+ let att = new CalAttendee();
+ att.icalProperty = attprop;
+ this.addAttendee(att);
+ }
+
+ this.mAttachments = []; // don't inherit anything from parent
+ for (let attprop of cal.iterate.icalProperty(icalcomp, "ATTACH")) {
+ let att = new CalAttachment();
+ att.icalProperty = attprop;
+ this.addAttachment(att);
+ }
+
+ this.mRelations = []; // don't inherit anything from parent
+ for (let relprop of cal.iterate.icalProperty(icalcomp, "RELATED-TO")) {
+ let rel = new CalRelation();
+ rel.icalProperty = relprop;
+ this.addRelation(rel);
+ }
+
+ let org = null;
+ let orgprop = icalcomp.getFirstProperty("ORGANIZER");
+ if (orgprop) {
+ org = new CalAttendee();
+ org.icalProperty = orgprop;
+ org.isOrganizer = true;
+ }
+ this.mOrganizer = org;
+
+ this.mCategories = [];
+ for (let catprop of cal.iterate.icalProperty(icalcomp, "CATEGORIES")) {
+ this.mCategories.push(catprop.value);
+ }
+
+ // find recurrence properties
+ let rec = null;
+ if (!this.recurrenceId) {
+ for (let recprop of cal.iterate.icalProperty(icalcomp)) {
+ let ritem = null;
+ switch (recprop.propertyName) {
+ case "RRULE":
+ case "EXRULE":
+ ritem = cal.createRecurrenceRule();
+ break;
+ case "RDATE":
+ case "EXDATE":
+ ritem = cal.createRecurrenceDate();
+ break;
+ default:
+ continue;
+ }
+ ritem.icalProperty = recprop;
+
+ if (!rec) {
+ rec = new CalRecurrenceInfo(this);
+ }
+ rec.appendRecurrenceItem(ritem);
+ }
+ }
+ this.mRecurrenceInfo = rec;
+
+ this.mAlarms = []; // don't inherit anything from parent
+ for (let alarmComp of cal.iterate.icalSubcomponent(icalcomp, "VALARM")) {
+ let alarm = new CalAlarm();
+ try {
+ alarm.icalComponent = alarmComp;
+ this.addAlarm(alarm, true);
+ } catch (e) {
+ cal.ERROR(
+ "Invalid alarm for item: " +
+ this.id +
+ " (" +
+ alarmComp.serializeToICS() +
+ ")" +
+ " exception: " +
+ e
+ );
+ }
+ }
+
+ let lastAck = icalcomp.getFirstProperty("X-MOZ-LASTACK");
+ this.mAlarmLastAck = null;
+ if (lastAck) {
+ this.mAlarmLastAck = cal.createDateTime(lastAck.value);
+ }
+
+ this.mDirty = false;
+ },
+
+ /**
+ * Import all properties not in the promoted map into this item's extended
+ * properties bag.
+ *
+ * @param icalcomp The ical component to read.
+ * @param promoted The map of promoted properties.
+ */
+ importUnpromotedProperties(icalcomp, promoted) {
+ for (let prop of cal.iterate.icalProperty(icalcomp)) {
+ let propName = prop.propertyName;
+ if (!promoted[propName]) {
+ this.setProperty(propName, prop.value);
+ for (let [paramName, paramValue] of cal.iterate.icalParameter(prop)) {
+ if (!(propName in this.mPropertyParams)) {
+ this.mPropertyParams[propName] = {};
+ }
+ this.mPropertyParams[propName][paramName] = paramValue;
+ }
+ }
+ }
+ },
+
+ // boolean isPropertyPromoted(in AString name);
+ isPropertyPromoted(name) {
+ return this.itemBasePromotedProps[name.toUpperCase()];
+ },
+
+ // attribute calIIcalComponent icalComponent;
+ get icalComponent() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ set icalComponent(val) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ // attribute PRUint32 generation;
+ get generation() {
+ let gen = this.getProperty("X-MOZ-GENERATION");
+ return gen ? parseInt(gen, 10) : 0;
+ },
+ set generation(aValue) {
+ this.setProperty("X-MOZ-GENERATION", String(aValue));
+ },
+
+ /**
+ * Fills the passed ical component with the base item's properties.
+ *
+ * @param icalcomp The ical component to write to.
+ */
+ fillIcalComponentFromBase(icalcomp) {
+ this.ensureNotDirty();
+
+ this.mapPropsToICS(icalcomp, this.icsBasePropMap);
+
+ let org = this.organizer;
+ if (org) {
+ icalcomp.addProperty(org.icalProperty);
+ }
+
+ for (let attendee of this.getAttendees()) {
+ icalcomp.addProperty(attendee.icalProperty);
+ }
+
+ for (let attachment of this.getAttachments()) {
+ icalcomp.addProperty(attachment.icalProperty);
+ }
+
+ for (let relation of this.getRelations()) {
+ icalcomp.addProperty(relation.icalProperty);
+ }
+
+ if (this.mRecurrenceInfo) {
+ for (let ritem of this.mRecurrenceInfo.getRecurrenceItems()) {
+ icalcomp.addProperty(ritem.icalProperty);
+ }
+ }
+
+ for (let cat of this.getCategories()) {
+ let catprop = cal.icsService.createIcalProperty("CATEGORIES");
+ catprop.value = cat;
+ icalcomp.addProperty(catprop);
+ }
+
+ if (this.mAlarms) {
+ for (let alarm of this.mAlarms) {
+ icalcomp.addSubcomponent(alarm.icalComponent);
+ }
+ }
+
+ let alarmLastAck = this.alarmLastAck;
+ if (alarmLastAck) {
+ let lastAck = cal.icsService.createIcalProperty("X-MOZ-LASTACK");
+ // - should we further ensure that those are UTC or rely on calAlarmService doing so?
+ lastAck.value = alarmLastAck.icalString;
+ icalcomp.addProperty(lastAck);
+ }
+ },
+
+ // Array<calIAlarm> getAlarms();
+ getAlarms() {
+ if (!this.mAlarms && this.mIsProxy) {
+ this.mAlarms = this.mParentItem.getAlarms();
+ }
+ if (this.mAlarms) {
+ return this.mAlarms.concat([]); // clone
+ }
+ return [];
+ },
+
+ /**
+ * Adds an alarm. The second parameter is for internal use only, i.e not
+ * provided on the interface.
+ *
+ * @see calIItemBase
+ * @param aDoNotValidate Don't serialize the component to check for
+ * errors.
+ */
+ addAlarm(aAlarm, aDoNotValidate) {
+ if (!aDoNotValidate) {
+ try {
+ // Trigger the icalComponent getter to make sure the alarm is valid.
+ aAlarm.icalComponent; // eslint-disable-line no-unused-expressions
+ } catch (e) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+
+ this.modify();
+ this.mAlarms = this.getAlarms();
+ this.mAlarms.push(aAlarm);
+ },
+
+ // void deleteAlarm(in calIAlarm aAlarm);
+ deleteAlarm(aAlarm) {
+ this.modify();
+ this.mAlarms = this.getAlarms();
+ for (let i = 0; i < this.mAlarms.length; i++) {
+ if (cal.data.compareObjects(this.mAlarms[i], aAlarm, Ci.calIAlarm)) {
+ this.mAlarms.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ // void clearAlarms();
+ clearAlarms() {
+ this.modify();
+ this.mAlarms = [];
+ },
+
+ // Array<calIItemBase> getOccurrencesBetween(in calIDateTime aStartDate, in calIDateTime aEndDate);
+ getOccurrencesBetween(aStartDate, aEndDate) {
+ if (this.recurrenceInfo) {
+ return this.recurrenceInfo.getOccurrences(aStartDate, aEndDate, 0);
+ }
+
+ if (cal.item.checkIfInRange(this, aStartDate, aEndDate)) {
+ return [this];
+ }
+
+ return [];
+ },
+};
+
+makeMemberAttrProperty(calItemBase, "CREATED", "creationDate");
+makeMemberAttrProperty(calItemBase, "SUMMARY", "title");
+makeMemberAttrProperty(calItemBase, "PRIORITY", "priority");
+makeMemberAttrProperty(calItemBase, "CLASS", "privacy");
+makeMemberAttrProperty(calItemBase, "STATUS", "status");
+makeMemberAttrProperty(calItemBase, "ALARMTIME", "alarmTime");
+
+/**
+ * Adds a member attribute to the given prototype.
+ *
+ * @param {Function} ctor - The constructor function of the prototype.
+ * @param {string} varname - The variable name to get/set.
+ * @param {string} attr - The attribute name to be used.
+ * @param {*} dflt - The default value in case none is set.
+ */
+function makeMemberAttr(ctor, varname, attr, dflt) {
+ let getter = function () {
+ return varname in this ? this[varname] : dflt;
+ };
+ let setter = function (value) {
+ this.modify();
+ this[varname] = value;
+ return value;
+ };
+
+ ctor.prototype.__defineGetter__(attr, getter);
+ ctor.prototype.__defineSetter__(attr, setter);
+}
+
+/**
+ * Adds a member attribute to the given prototype, using `getProperty` and
+ * `setProperty` for access.
+ *
+ * Default values are not handled here, but instead are set in constructors,
+ * which makes it possible to e.g. iterate through `mProperties` when cloning
+ * an object.
+ *
+ * @param {Function} ctor - The constructor function of the prototype.
+ * @param {string} name - The property name to get/set.
+ * @param {string} attr - The attribute name to be used.
+ */
+function makeMemberAttrProperty(ctor, name, attr) {
+ let getter = function () {
+ return this.getProperty(name);
+ };
+ let setter = function (value) {
+ this.modify();
+ return this.setProperty(name, value);
+ };
+ ctor.prototype.__defineGetter__(attr, getter);
+ ctor.prototype.__defineSetter__(attr, setter);
+}
diff --git a/comm/calendar/base/src/components.conf b/comm/calendar/base/src/components.conf
new file mode 100644
index 0000000000..b28918ca2d
--- /dev/null
+++ b/comm/calendar/base/src/components.conf
@@ -0,0 +1,208 @@
+# -*- 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": "{7463258c-6ef3-40a2-89a9-bb349596e927}",
+ "contract_ids": ["@mozilla.org/calendar/acl-manager;1?type=default"],
+ "jsm": "resource:///modules/CalDefaultACLManager.jsm",
+ "constructor": "CalDefaultACLManager",
+ },
+ {
+ "cid": "{fcbadec9-aab7-49e2-a20a-8e93f41d68d1}",
+ "interfaces": ["calITimezoneDatabase"],
+ "contract_ids": ["@mozilla.org/calendar/timezone-database;1"],
+ "type": "TimezoneDatabase",
+ "headers": ["/comm/calendar/base/src/TimezoneDatabase.h"],
+ },
+ {
+ "cid": "{e736f2bd-7640-4715-ab35-887dc866c587}",
+ "contract_ids": ["@mozilla.org/calendar/timezone-service;1"],
+ "jsm": "resource:///modules/CalTimezoneService.jsm",
+ "constructor": "CalTimezoneService",
+ },
+ {
+ "cid": "{b8db7c7f-c168-4e11-becb-f26c1c4f5f8f}",
+ "contract_ids": ["@mozilla.org/calendar/alarm;1"],
+ "jsm": "resource:///modules/CalAlarm.jsm",
+ "constructor": "CalAlarm",
+ },
+ {
+ "cid": "{4b7ae030-ed79-11d9-8cd6-0800200c9a66}",
+ "contract_ids": ["@mozilla.org/calendar/alarm-monitor;1"],
+ "jsm": "resource:///modules/CalAlarmMonitor.jsm",
+ "constructor": "CalAlarmMonitor",
+ },
+ {
+ "cid": "{7a9200dd-6a64-4fff-a798-c5802186e2cc}",
+ "contract_ids": ["@mozilla.org/calendar/alarm-service;1"],
+ "jsm": "resource:///modules/CalAlarmService.jsm",
+ "constructor": "CalAlarmService",
+ },
+ {
+ "cid": "{5f76b352-ab75-4c2b-82c9-9206dbbf8571}",
+ "contract_ids": ["@mozilla.org/calendar/attachment;1"],
+ "jsm": "resource:///modules/CalAttachment.jsm",
+ "constructor": "CalAttachment",
+ },
+ {
+ "cid": "{5c8dcaa3-170c-4a73-8142-d531156f664d}",
+ "contract_ids": ["@mozilla.org/calendar/attendee;1"],
+ "jsm": "resource:///modules/CalAttendee.jsm",
+ "constructor": "CalAttendee",
+ },
+ {
+ "cid": "{f42585e7-e736-4600-985d-9624c1c51992}",
+ "contract_ids": ["@mozilla.org/calendar/manager;1"],
+ "jsm": "resource:///modules/CalCalendarManager.jsm",
+ "constructor": "CalCalendarManager",
+ },
+ {
+ "cid": "{36783242-ec94-4d8a-9248-d2679edd55b9}",
+ "contract_ids": ["@mozilla.org/calendar/datetime;1"],
+ "jsm": "resource:///modules/CalDateTime.jsm",
+ "constructor": "CalDateTime",
+ },
+ {
+ "cid": "{8e6799af-e7e9-4e6c-9a82-a2413e86d8c3}",
+ "contract_ids": ["@mozilla.org/calendar/deleted-items-manager;1"],
+ "jsm": "resource:///modules/CalDeletedItems.jsm",
+ "constructor": "CalDeletedItems",
+ "categories": {"profile-after-change": "deleted-items-manager"},
+ },
+ {
+ "cid": "{7436f480-c6fc-4085-9655-330b1ee22288}",
+ "contract_ids": ["@mozilla.org/calendar/duration;1"],
+ "jsm": "resource:///modules/CalDuration.jsm",
+ "constructor": "CalDuration",
+ },
+ {
+ "cid": "{974339d5-ab86-4491-aaaf-2b2ca177c12b}",
+ "contract_ids": ["@mozilla.org/calendar/event;1"],
+ "jsm": "resource:///modules/CalEvent.jsm",
+ "constructor": "CalEvent",
+ },
+ {
+ "cid": "{29c56cd5-d36e-453a-acde-0083bd4fe6d3}",
+ "contract_ids": ["@mozilla.org/calendar/freebusy-service;1"],
+ "jsm": "resource:///modules/CalFreeBusyService.jsm",
+ "constructor": "CalFreeBusyService",
+ },
+ {
+ "cid": "{6fe88047-75b6-4874-80e8-5f5800f14984}",
+ "contract_ids": ["@mozilla.org/calendar/ics-parser;1"],
+ "jsm": "resource:///modules/CalIcsParser.jsm",
+ "constructor": "CalIcsParser",
+ },
+ {
+ "cid": "{207a6682-8ff1-4203-9160-729ec28c8766}",
+ "contract_ids": ["@mozilla.org/calendar/ics-serializer;1"],
+ "jsm": "resource:///modules/CalIcsSerializer.jsm",
+ "constructor": "CalIcsSerializer",
+ },
+ {
+ "cid": "{c61cb903-4408-41b3-bc22-da0b27efdfe1}",
+ "contract_ids": ["@mozilla.org/calendar/ics-service;1"],
+ "jsm": "resource:///modules/CalICSService.jsm",
+ "constructor": "CalICSService",
+ },
+ {
+ "cid": "{f41392ab-dcad-4bad-818f-b3d1631c4d93}",
+ "contract_ids": ["@mozilla.org/calendar/itip-item;1"],
+ "jsm": "resource:///modules/CalItipItem.jsm",
+ "constructor": "CalItipItem",
+ },
+ {
+ "cid": "{394a281f-7299-45f7-8b1f-cce21258972f}",
+ "contract_ids": ["@mozilla.org/calendar/period;1"],
+ "jsm": "resource:///modules/CalPeriod.jsm",
+ "constructor": "CalPeriod",
+ },
+ {
+ "cid": "{1153c73a-39be-46aa-9ba9-656d188865ca}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=webcal"],
+ "jsm": "resource:///modules/CalProtocolHandler.jsm",
+ "constructor": "CalProtocolHandlerWebcal",
+ "protocol_config": {
+ "scheme": "webcal",
+ "flags": [
+ "URI_STD",
+ "ALLOWS_PROXY",
+ "ALLOWS_PROXY_HTTP",
+ "URI_LOADABLE_BY_ANYONE",
+ "URI_IS_POTENTIALLY_TRUSTWORTHY",
+ ],
+ "default_port": 80,
+ },
+ },
+ {
+ "cid": "{bdf71224-365d-4493-856a-a7e74026f766}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=webcals"],
+ "jsm": "resource:///modules/CalProtocolHandler.jsm",
+ "constructor": "CalProtocolHandlerWebcals",
+ "protocol_config": {
+ "scheme": "webcals",
+ "flags": [
+ "URI_STD",
+ "ALLOWS_PROXY",
+ "ALLOWS_PROXY_HTTP",
+ "URI_LOADABLE_BY_ANYONE",
+ "URI_IS_POTENTIALLY_TRUSTWORTHY",
+ ],
+ "default_port": 443,
+ },
+ },
+ {
+ "cid": "{806b6423-3aaa-4b26-afa3-de60563e9cec}",
+ "contract_ids": ["@mozilla.org/calendar/recurrence-date;1"],
+ "jsm": "resource:///modules/CalRecurrenceDate.jsm",
+ "constructor": "CalRecurrenceDate",
+ },
+ {
+ "cid": "{04027036-5884-4a30-b4af-f2cad79f6edf}",
+ "contract_ids": ["@mozilla.org/calendar/recurrence-info;1"],
+ "jsm": "resource:///modules/CalRecurrenceInfo.jsm",
+ "constructor": "CalRecurrenceInfo",
+ },
+ {
+ "cid": "{df19281a-5389-4146-b941-798cb93a7f0d}",
+ "contract_ids": ["@mozilla.org/calendar/recurrence-rule;1"],
+ "jsm": "resource:///modules/CalRecurrenceRule.jsm",
+ "constructor": "CalRecurrenceRule",
+ },
+ {
+ "cid": "{76810fae-abad-4019-917a-08e95d5bbd68}",
+ "contract_ids": ["@mozilla.org/calendar/relation;1"],
+ "jsm": "resource:///modules/CalRelation.jsm",
+ "constructor": "CalRelation",
+ },
+ {
+ "cid": "{7af51168-6abe-4a31-984d-6f8a3989212d}",
+ "contract_ids": ["@mozilla.org/calendar/todo;1"],
+ "jsm": "resource:///modules/CalTodo.jsm",
+ "constructor": "CalTodo",
+ },
+ {
+ "cid": "{6877bbdd-f336-46f5-98ce-fe86d0285cc1}",
+ "contract_ids": ["@mozilla.org/calendar/weekinfo-service;1"],
+ "jsm": "resource:///modules/CalWeekInfoService.jsm",
+ "constructor": "CalWeekInfoService",
+ },
+ {
+ "cid": "{2547331f-34c0-4a4b-b93c-b503538ba6d6}",
+ "contract_ids": ["@mozilla.org/calendar/startup-service;1"],
+ "jsm": "resource:///modules/CalStartupService.jsm",
+ "constructor": "CalStartupService",
+ "categories": {"profile-after-change": "calendar-startup-service"},
+ },
+ {
+ "cid": "{c70acb08-464e-4e55-899d-b2c84c5409fa}",
+ "contract_ids": ["@mozilla.org/calendar/mime-converter;1"],
+ "jsm": "resource:///modules/CalMimeConverter.jsm",
+ "constructor": "CalMimeConverter",
+ "categories": {"simple-mime-converters": "text/calendar"},
+ },
+]
diff --git a/comm/calendar/base/src/moz.build b/comm/calendar/base/src/moz.build
new file mode 100644
index 0000000000..350262f182
--- /dev/null
+++ b/comm/calendar/base/src/moz.build
@@ -0,0 +1,71 @@
+# 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/.
+
+XPIDL_SOURCES += [
+ "calInternalInterfaces.idl",
+]
+
+XPIDL_MODULE = "calbaseinternal"
+
+EXTRA_JS_MODULES += [
+ "CalAlarm.jsm",
+ "CalAlarmMonitor.jsm",
+ "CalAlarmService.jsm",
+ "CalAttachment.jsm",
+ "CalAttendee.jsm",
+ "CalCalendarManager.jsm",
+ "CalDateTime.jsm",
+ "CalDefaultACLManager.jsm",
+ "CalDeletedItems.jsm",
+ "CalDuration.jsm",
+ "CalEvent.jsm",
+ "CalFreeBusyService.jsm",
+ "CalIcsParser.jsm",
+ "CalIcsSerializer.jsm",
+ "CalICSService.jsm",
+ "CalItipItem.jsm",
+ "CalMetronome.jsm",
+ "CalMimeConverter.jsm",
+ "CalPeriod.jsm",
+ "CalProtocolHandler.jsm",
+ "CalReadableStreamFactory.jsm",
+ "CalRecurrenceDate.jsm",
+ "CalRecurrenceInfo.jsm",
+ "CalRecurrenceRule.jsm",
+ "CalRelation.jsm",
+ "CalStartupService.jsm",
+ "CalTimezone.jsm",
+ "CalTimezoneService.jsm",
+ "CalTodo.jsm",
+ "CalTransactionManager.jsm",
+ "CalWeekInfoService.jsm",
+]
+
+EXPORTS += [
+ "TimezoneDatabase.h",
+]
+
+UNIFIED_SOURCES += [
+ "TimezoneDatabase.cpp",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+# These files go in components so they can be packaged correctly.
+FINAL_TARGET_FILES.components += [
+ "calCachedCalendar.js",
+ "calICSService-worker.js",
+ "calItemBase.js",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("Calendar", "Internal Components")
+
+with Files("calAlarm*"):
+ BUG_COMPONENT = ("Calendar", "Alarms")
+
+FINAL_LIBRARY = "xul"