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