diff options
Diffstat (limited to 'comm/calendar/base/src/CalAlarm.jsm')
-rw-r--r-- | comm/calendar/base/src/CalAlarm.jsm | 693 |
1 files changed, 693 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]"; + }, +}; |