diff options
Diffstat (limited to 'calendar/components/icaljs/calICSService.js')
-rw-r--r-- | calendar/components/icaljs/calICSService.js | 520 |
1 files changed, 520 insertions, 0 deletions
diff --git a/calendar/components/icaljs/calICSService.js b/calendar/components/icaljs/calICSService.js new file mode 100644 index 000000000..b4d07f1e3 --- /dev/null +++ b/calendar/components/icaljs/calICSService.js @@ -0,0 +1,520 @@ +/* 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/. */ + +Components.utils.import("resource://calendar/modules/ical.js"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://calendar/modules/calUtils.jsm"); + +function calIcalProperty(innerObject) { + this.innerObject = innerObject || new ICAL.Property(); + this.wrappedJSObject = this; +} + +var calIcalPropertyInterfaces = [Components.interfaces.calIIcalProperty]; +var calIcalPropertyClassID = Components.ID("{423ac3f0-f612-48b3-953f-47f7f8fd705b}"); +calIcalProperty.prototype = { + QueryInterface: XPCOMUtils.generateQI(calIcalPropertyInterfaces), + classID: calIcalPropertyClassID, + classInfo: XPCOMUtils.generateCI({ + contractID: "@mozilla.org/calendar/ical-property;1", + classDescription: "Wrapper for a libical property", + classID: calIcalPropertyClassID, + interfaces: calIcalPropertyInterfaces + }), + + 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: function() { 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 val; + } + this.valueAsIcalString = val; + return val; + }, + + get valueAsIcalString() { + 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(); + } else { + 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()); + } + return val; + }, + + get valueAsDatetime() { + let val = this.innerObject.getFirstValue(); + let isIcalTime = val && (typeof val == "object") && + ("icalclass" in val) && val.icalclass == "icaltime"; + return (isIcalTime ? new 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(calICALJSTimezone, val.zone); + this.parent.addTimezoneReference(tzref); + } + } else { + this.innerObject.removeParameter("TZID"); + } + this.innerObject.setValue(val); + }, this); + }, + + get propertyName() { return this.innerObject.name.toUpperCase(); }, + + getParameter: function(name) { + // Unfortuantely 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: function(name, value) { + // Similar problems for setting the value parameter. Lightning 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: function(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: function() { + cal.WARN("calIICSService::clearXParameters is no longer implemented, " + + "please use removeParameter"); + }, + + paramIterator: null, + getFirstParameterName: function() { + 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: function() { + if (this.paramIterator) { + let next = this.paramIterator.next(); + if (next.done) { + this.paramIterator = null; + } + + return next.value; + } else { + return this.getFirstParameterName(); + } + } +}; + +function calIcalComponent(innerObject) { + this.innerObject = innerObject || new ICAL.Component(); + this.wrappedJSObject = this; + this.mReferencedZones = {}; +} + +var calIcalComponentInterfaces = [Components.interfaces.calIIcalComponent]; +var calIcalComponentClassID = Components.ID("{51ac96fd-1279-4439-a85b-6947b37f4cea}"); +calIcalComponent.prototype = { + QueryInterface: XPCOMUtils.generateQI(calIcalComponentInterfaces), + classID: calIcalComponentClassID, + classInfo: XPCOMUtils.generateCI({ + contractID: "@mozilla.org/calendar/ical-component;1", + classDescription: "Wrapper for a icaljs component", + classID: calIcalComponentClassID, + interfaces: calIcalComponentInterfaces + }), + + clone: function() { 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: function(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: function(kind) { + if (this.componentIterator) { + let next = this.componentIterator.next(); + if (next.done) { + this.componentIterator = null; + } + + return next.value; + } else { + 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 = Components.interfaces.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: function(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(calICALJSTimezone, val.zone)); + } else { + prop.removeParameter("TZID"); + } + }, + + get startTime() { return wrapGetter(calDateTime, this.innerObject.getFirstPropertyValue("dtstart")); }, + set startTime(val) { unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "dtstart"), this); }, + + get endTime() { return wrapGetter(calDateTime, this.innerObject.getFirstPropertyValue("dtend")); }, + set endTime(val) { unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "dtend"), this); }, + + get duration() { return wrapGetter(calDuration, this.innerObject.getFirstPropertyValue("duration")); }, + + get dueTime() { return wrapGetter(calDateTime, this.innerObject.getFirstPropertyValue("due")); }, + set dueTime(val) { unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "due"), this); }, + + get stampTime() { return wrapGetter(calDateTime, this.innerObject.getFirstPropertyValue("dtstamp")); }, + set stampTime(val) { unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "dtstamp"), this); }, + + get createdTime() { return wrapGetter(calDateTime, this.innerObject.getFirstPropertyValue("created")); }, + set createdTime(val) { unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "created"), this); }, + + get completedTime() { return wrapGetter(calDateTime, this.innerObject.getFirstPropertyValue("completed")); }, + set completedTime(val) { unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "completed"), this); }, + + get lastModified() { return wrapGetter(calDateTime, this.innerObject.getFirstPropertyValue("last-modified")); }, + set lastModified(val) { unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "last-modified"), this); }, + + get recurrenceId() { return wrapGetter(calDateTime, this.innerObject.getFirstPropertyValue("recurrence-id")); }, + set recurrenceId(val) { unwrapSetter(ICAL.Time, val, this._setTimeAttr.bind(this, "recurrence-id"), this); }, + + serializeToICS: function() { return this.innerObject.toString() + ICAL.newLineChar; }, + toString: function() { return this.innerObject.toString(); }, + + addSubcomponent: function(comp) { + comp.getReferencedTimezones({}).forEach(this.addTimezoneReference, this); + let jscomp = unwrapSingle(ICAL.Component, comp); + this.innerObject.addSubcomponent(jscomp); + }, + + propertyIterator: null, + getFirstProperty: function(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: function(kind) { + if (this.propertyIterator) { + let next = this.propertyIterator.next(); + if (next.done) { + this.propertyIterator = null; + } + + return next.value; + } else { + return this.getFirstProperty(kind); + } + }, + + _getNextParentVCalendar: function() { + let vcalendar = this; // eslint-disable-line consistent-this + while (vcalendar && vcalendar.componentType != "VCALENDAR") { + vcalendar = vcalendar.parent; + } + return vcalendar || this; + }, + + addProperty: function(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: function(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: function(aCount) { + let vals = Object.keys(this.mReferencedZones).map(timezone => this.mReferencedZones[timezone]); + aCount.value = vals.length; + return vals; + }, + + serializeToICSStream: function() { + let unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); + unicodeConverter.charset = "UTF-8"; + return unicodeConverter.convertToInputStream(this.innerObject.toString()); + } +}; + +function calICSService() { + this.wrappedJSObject = this; +} + +var calICSServiceInterfaces = [Components.interfaces.calIICSService]; +var calICSServiceClassID = Components.ID("{c61cb903-4408-41b3-bc22-da0b27efdfe1}"); +calICSService.prototype = { + QueryInterface: XPCOMUtils.generateQI(calICSServiceInterfaces), + classID: calICSServiceClassID, + classInfo: XPCOMUtils.generateCI({ + contractID: "@mozilla.org/calendar/ics-service;1", + classDescription: "ICS component and property service", + classID: calICSServiceClassID, + interfaces: [Components.interfaces.calIICSService] + }), + + parseICS: function(serialized, tzProvider) { + // TODO ical.js doesn't support tz providers, but this is usually null + // or our timezone service anyway. + let comp = ICAL.parse(serialized); + return new calIcalComponent(new ICAL.Component(comp)); + }, + + parseICSAsync: function(serialized, tzProvider, listener) { + // There are way too many error checking messages here, but I had so + // much pain with this method that I don't want it to break again. + try { + let worker = new ChromeWorker("resource://calendar/calendar-js/calICSService-worker.js"); + worker.onmessage = function(event) { + let rc = Components.results.NS_ERROR_FAILURE; + let icalComp = null; + try { + rc = event.data.rc; + icalComp = new calIcalComponent(new ICAL.Component(event.data.data)); + if (!Components.isSuccessCode(rc)) { + cal.ERROR("[calICSService] Error in parser worker: " + data); + } + } catch (e) { + cal.ERROR("[calICSService] Exception parsing item: " + e); + } + + listener.onParsingComplete(rc, icalComp); + }; + worker.onerror = function(event) { + cal.ERROR("[calICSService] Error in parser worker: " + event.message); + listener.onParsingComplete(Components.results.NS_ERROR_FAILURE, null); + }; + worker.postMessage(serialized); + } catch (e) { + // If an error occurs above, the calling code will hang. Catch the exception just in case + cal.ERROR("[calICSService] Error starting parsing worker: " + e); + listener.onParsingComplete(Components.results.NS_ERROR_FAILURE, null); + } + }, + + createIcalComponent: function(kind) { + return new calIcalComponent(new ICAL.Component(kind.toLowerCase())); + }, + + createIcalProperty: function(kind) { + return new calIcalProperty(new ICAL.Property(kind.toLowerCase())); + }, + + createIcalPropertyFromString: function(str) { + return new calIcalProperty(ICAL.Property.fromString(str.trim(), ICAL.design.icalendar)); + } +}; |