diff options
Diffstat (limited to '')
-rw-r--r-- | comm/calendar/base/src/calItemBase.js | 1198 |
1 files changed, 1198 insertions, 0 deletions
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); +} |