summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/src/CalICSService.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/calendar/base/src/CalICSService.jsm604
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));
+ },
+};