Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
1063 lines
27 KiB
JavaScript
Executable file
1063 lines
27 KiB
JavaScript
Executable file
define(function(require, exports, module) {
|
|
'use strict';
|
|
|
|
var Caldav = require('ext/caldav');
|
|
var ICAL = require('ext/ical');
|
|
var IcalRecurExpansion = require('./ical_recur_expansion');
|
|
var Presets = require('common/presets');
|
|
var Responder = require('common/responder');
|
|
var debug = require('common/debug')('service/caldav');
|
|
var extend = require('common/extend');
|
|
var uuid = require('ext/uuid');
|
|
|
|
/* TODO: ugly hack to enable system XHR fix upstream in Caldav lib */
|
|
var xhrOpts = {
|
|
/** system is required for cross domain XHR */
|
|
mozSystem: true,
|
|
/** mozAnon is required to avoid system level popups on 401 status */
|
|
mozAnon: true,
|
|
/** enables use of mozilla only streaming api's when available */
|
|
useMozChunkedText: true
|
|
};
|
|
|
|
Caldav.Xhr.prototype.globalXhrOptions = xhrOpts;
|
|
|
|
function Service(service) {
|
|
Responder.call(this);
|
|
|
|
this.service = service;
|
|
this._initEvents();
|
|
}
|
|
module.exports = Service;
|
|
|
|
Service.prototype = {
|
|
__proto__: Responder.prototype,
|
|
|
|
/**
|
|
* See: http://tools.ietf.org/html/rfc5545#section-3.7.3
|
|
*/
|
|
icalProductId: '-//Mozilla//FirefoxOS',
|
|
|
|
/**
|
|
* See: http://tools.ietf.org/html/rfc5545#section-3.7.4
|
|
*/
|
|
icalVersion: '2.0',
|
|
|
|
_initEvents: function() {
|
|
var events = [
|
|
'noop',
|
|
'getAccount',
|
|
'findCalendars',
|
|
'getCalendar',
|
|
'streamEvents',
|
|
'expandComponents',
|
|
'expandRecurringEvent',
|
|
'deleteEvent',
|
|
'updateEvent',
|
|
'createEvent'
|
|
];
|
|
|
|
debug('Will listen for worker events...');
|
|
events.forEach(function(e) {
|
|
this.service.on(e, this);
|
|
}, this);
|
|
},
|
|
|
|
handleEvent: function(e) {
|
|
debug('Worker will fulfill', e.type, 'request...');
|
|
this[e.type].apply(this, e.data);
|
|
},
|
|
|
|
/**
|
|
* Builds an Caldav connection from an account model object.
|
|
*/
|
|
_createConnection: function(account) {
|
|
var params = extend({}, account);
|
|
var preset = Presets[account.preset];
|
|
|
|
if (
|
|
preset &&
|
|
preset.authenticationType &&
|
|
preset.apiCredentials
|
|
) {
|
|
switch (preset.authenticationType) {
|
|
case 'oauth2':
|
|
params.httpHandler = 'oauth2';
|
|
// shallow copy the apiCredentials on the preset
|
|
params.apiCredentials = extend({}, preset.apiCredentials);
|
|
// the url in this case will always be tokenUrl
|
|
params.apiCredentials.url = preset.apiCredentials.tokenUrl;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var connection = new Caldav.Connection(params);
|
|
return connection;
|
|
},
|
|
|
|
_requestHome: function(connection, url) {
|
|
return new Caldav.Request.CalendarHome(
|
|
connection,
|
|
{ url: url }
|
|
);
|
|
},
|
|
|
|
_requestCalendars: function(connection, url) {
|
|
var Finder = Caldav.Request.Resources;
|
|
var Resource = Caldav.Resources.Calendar;
|
|
|
|
var req = new Finder(connection, { url: url });
|
|
|
|
req.addResource('calendar', Resource);
|
|
req.prop(['ical', 'calendar-color']);
|
|
req.prop(['caldav', 'calendar-description']);
|
|
req.prop('current-user-privilege-set');
|
|
req.prop('displayname');
|
|
req.prop('resourcetype');
|
|
req.prop(['calserver', 'getctag']);
|
|
|
|
return req;
|
|
},
|
|
|
|
_requestEvents: function(connection, cal, options) {
|
|
var Resource = Caldav.Resources.Calendar;
|
|
var remoteCal = new Resource(connection, cal);
|
|
var query = remoteCal.createQuery();
|
|
|
|
query.prop('getetag');
|
|
|
|
// only return VEVENT & VTIMEZONE
|
|
var filterQuery = query.filter.setComp('VCALENDAR');
|
|
var filterEvent = filterQuery.comp('VEVENT');
|
|
|
|
if (options && options.startDate) {
|
|
// convert startDate to unix ical time.
|
|
var icalDate = new ICAL.Time();
|
|
|
|
// ical uses seconds not milliseconds
|
|
icalDate.fromUnixTime(options.startDate.valueOf() / 1000);
|
|
filterEvent.setTimeRange({ start: icalDate.toICALString() });
|
|
}
|
|
|
|
// include only the VEVENT/VTIMEZONE in the data
|
|
query.data.setComp('VCALENDAR');
|
|
|
|
return query;
|
|
},
|
|
|
|
noop: function(callback) {
|
|
callback({ ready: true });
|
|
},
|
|
|
|
getAccount: function(account, callback) {
|
|
var url = account.entrypoint;
|
|
debug('Fetching account from:', url);
|
|
|
|
var connection = this._createConnection(account);
|
|
var request = this._requestHome(connection, url);
|
|
debug('Will issue calendar home request.');
|
|
return request.send(function(err, data) {
|
|
if (err) {
|
|
debug('Error sending home request:', err);
|
|
return callback(err);
|
|
}
|
|
|
|
debug('Received data:', data);
|
|
var result = {};
|
|
|
|
if (data.url) {
|
|
result.calendarHome = data.url;
|
|
}
|
|
|
|
if (connection.oauth) {
|
|
result.oauth = connection.oauth;
|
|
}
|
|
|
|
if (connection.user) {
|
|
result.user = connection.user;
|
|
}
|
|
|
|
callback(null, result);
|
|
});
|
|
},
|
|
|
|
_formatCalendar: function(cal) {
|
|
var result = Object.create(null);
|
|
|
|
result.id = cal.url;
|
|
result.url = cal.url;
|
|
result.name = cal.name;
|
|
result.color = cal.color;
|
|
result.description = cal.description;
|
|
result.syncToken = cal.ctag;
|
|
result.privilegeSet = cal.privilegeSet;
|
|
|
|
return result;
|
|
},
|
|
|
|
findCalendars: function(account, callback) {
|
|
var self = this;
|
|
var url = account.calendarHome;
|
|
var connection = this._createConnection(account);
|
|
|
|
var request = this._requestCalendars(
|
|
connection,
|
|
url
|
|
);
|
|
|
|
request.send(function(err, data) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var calendars = data.calendar;
|
|
var results = {};
|
|
var key;
|
|
var item;
|
|
|
|
for (key in calendars) {
|
|
if (calendars.hasOwnProperty(key)) {
|
|
item = calendars[key];
|
|
var formattedCal = self._formatCalendar(
|
|
item
|
|
);
|
|
|
|
// If privilegeSet is not present we will assume full permissions.
|
|
// Its highly unlikey that it is missing however.
|
|
if (('privilegeSet' in formattedCal) &&
|
|
(formattedCal.privilegeSet.indexOf('read') === -1)) {
|
|
|
|
// skip calendars without read permissions
|
|
continue;
|
|
}
|
|
|
|
results[key] = formattedCal;
|
|
}
|
|
}
|
|
callback(null, results);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Formats an alarm trigger
|
|
* Returns the relative time for that trigger
|
|
*
|
|
* @param {ICAL.Property} trigger property.
|
|
* @param {ICAL.Date} start date.
|
|
*/
|
|
_formatTrigger: function(trigger, startDate) {
|
|
var alarmTrigger;
|
|
if (trigger.type == 'duration') {
|
|
alarmTrigger = trigger.getFirstValue().toSeconds();
|
|
} else {
|
|
// Type is date-time
|
|
alarmTrigger = trigger
|
|
.getFirstValue()
|
|
.subtractDate(startDate)
|
|
.toSeconds();
|
|
}
|
|
|
|
return alarmTrigger;
|
|
},
|
|
|
|
/**
|
|
* Formats an already parsed ICAL.Event instance.
|
|
* Expects event to already contain exceptions, etc..
|
|
*
|
|
* @param {String} etag etag.
|
|
* @param {String} url caldav url.
|
|
* @param {String} ical raw ical string.
|
|
* @param {ICAL.Event} event ical event.
|
|
*/
|
|
_formatEvent: function(etag, url, ical, event) {
|
|
var self = this;
|
|
var exceptions = null;
|
|
var key;
|
|
|
|
if (event.exceptions) {
|
|
exceptions = [];
|
|
for (key in event.exceptions) {
|
|
exceptions.push(this._formatEvent(
|
|
etag,
|
|
url,
|
|
ical,
|
|
event.exceptions[key]
|
|
));
|
|
}
|
|
|
|
if (!exceptions.length) {
|
|
exceptions = null;
|
|
}
|
|
}
|
|
|
|
var rid = event.recurrenceId;
|
|
|
|
if (rid) {
|
|
rid = this.formatICALTime(rid);
|
|
}
|
|
|
|
var resultAlarms = [];
|
|
var alarms = event.component.getAllSubcomponents('valarm');
|
|
alarms.forEach(function(instance) {
|
|
var action = instance.getFirstPropertyValue('action');
|
|
if (action && action === 'DISPLAY') {
|
|
var triggers = instance.getAllProperties('trigger');
|
|
var i = 0;
|
|
var len = triggers.length;
|
|
|
|
for (; i < len; i++) {
|
|
|
|
var trigger = triggers[i];
|
|
|
|
resultAlarms.push({
|
|
action: action,
|
|
trigger: self._formatTrigger(trigger, event.startDate)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
var result = {
|
|
alarms: resultAlarms,
|
|
syncToken: etag,
|
|
url: url,
|
|
id: event.uid,
|
|
title: event.summary,
|
|
recurrenceId: rid,
|
|
isRecurring: event.isRecurring(),
|
|
description: event.description,
|
|
location: event.location,
|
|
start: this.formatICALTime(event.startDate),
|
|
end: this.formatICALTime(event.endDate),
|
|
exceptions: exceptions
|
|
};
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Find and parse the display alarms for an event.
|
|
*
|
|
* @param {Object} details details for specific instance.
|
|
*/
|
|
_displayAlarms: function(details) {
|
|
var event = details.item;
|
|
var comp = event.component;
|
|
var alarms = comp.getAllSubcomponents('valarm');
|
|
var result = [];
|
|
|
|
var self = this;
|
|
alarms.forEach(function(instance) {
|
|
var action = instance.getFirstPropertyValue('action');
|
|
if (action && action === 'DISPLAY') {
|
|
// lets just assume we might have multiple triggers
|
|
var triggers = instance.getAllProperties('trigger');
|
|
var i = 0;
|
|
var len = triggers.length;
|
|
|
|
for (; i < len; i++) {
|
|
result.push({
|
|
action: action,
|
|
trigger: self._formatTrigger(triggers[i], event.startDate)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Takes an ICAL.Time object and converts it
|
|
* into the storage format familiar to the calendar app.
|
|
*
|
|
* var time = new ICAL.Time({
|
|
* year: 2012,
|
|
* month: 1,
|
|
* day: 1,
|
|
* zone: 'PST'
|
|
* });
|
|
*
|
|
* // time is converted to a MS
|
|
* // then its UTC offset is added
|
|
* // so the time is at UTC (offset 0) then the
|
|
* // offset is associated with that time.
|
|
*
|
|
* var output = {
|
|
* utc: ms,
|
|
* offset: (+|-)ms,
|
|
* // zone can mostly be ignored except
|
|
* // in the case where the event is "floating"
|
|
* // in time and we need to convert the utc value
|
|
* // to the current local time.
|
|
* tzid: ''
|
|
* };
|
|
*/
|
|
formatICALTime: function(time) {
|
|
var zone = time.zone;
|
|
var offset = time.utcOffset() * 1000;
|
|
var utc = time.toUnixTime() * 1000;
|
|
|
|
utc += offset;
|
|
|
|
var result = {
|
|
tzid: zone.tzid,
|
|
// from seconds to ms
|
|
offset: offset,
|
|
// from seconds to ms
|
|
utc: utc
|
|
};
|
|
|
|
if (time.isDate) {
|
|
result.isDate = true;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Formats a given time/date into a ICAL.Time instance.
|
|
* Suitable for converting the output of formatICALTime back
|
|
* into a similar representation of the original.
|
|
*
|
|
* Once a time instance goes through this method it should _not_
|
|
* be modified as the DST information is lost (offset is preserved).
|
|
*
|
|
* @param {ICAL.Time|Object} time formatted ical time
|
|
* or output of formatICALTime.
|
|
*/
|
|
formatInputTime: function(time) {
|
|
if (time instanceof ICAL.Time) {
|
|
return time;
|
|
}
|
|
|
|
var utc = time.utc;
|
|
var tzid = time.tzid;
|
|
var offset = time.offset;
|
|
var result;
|
|
|
|
if (tzid === ICAL.Timezone.localTimezone.tzid) {
|
|
result = new ICAL.Time();
|
|
result.fromUnixTime(utc / 1000);
|
|
result.zone = ICAL.Timezone.localTimezone;
|
|
} else {
|
|
result = new ICAL.Time();
|
|
result.fromUnixTime((utc - offset) / 1000);
|
|
result.zone = ICAL.Timezone.utcTimezone;
|
|
}
|
|
|
|
if (time.isDate) {
|
|
result.isDate = true;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Parse an ical data/string into primary
|
|
* event and exceptions.
|
|
*
|
|
* It is assumed there is only one primary event
|
|
* (does not have a RECURRENCE-ID) in the ical content.
|
|
*
|
|
* @param {Object|String|ICAL.Event} ical vcalendar chunk (and exceptions).
|
|
* @param {Function} callback node style callback [err, primary event].
|
|
*/
|
|
parseEvent: function(ical, callback) {
|
|
if (ical instanceof ICAL.Event) {
|
|
callback(null, ical);
|
|
return;
|
|
}
|
|
|
|
var parser = new ICAL.ComponentParser();
|
|
var primaryEvent;
|
|
var exceptions = [];
|
|
|
|
parser.ontimezone = function(zone) {
|
|
var id = zone.tzid;
|
|
|
|
if (!ICAL.TimezoneService.has(id)) {
|
|
ICAL.TimezoneService.register(id, zone);
|
|
}
|
|
};
|
|
|
|
parser.onevent = function(item) {
|
|
if (item.isRecurrenceException()) {
|
|
exceptions.push(item);
|
|
} else {
|
|
primaryEvent = item;
|
|
}
|
|
};
|
|
|
|
parser.oncomplete = function() {
|
|
if (!primaryEvent) {
|
|
//TODO: in the error handling pass we need to define
|
|
// path to log this information so we can determine
|
|
// the cause of failures.
|
|
callback(new Error('ical parse error'));
|
|
return;
|
|
}
|
|
exceptions.forEach(primaryEvent.relateException, primaryEvent);
|
|
callback(null, primaryEvent);
|
|
};
|
|
|
|
//XXX: Right now ICAL.js is all sync so we
|
|
// can catch the errors this way in the future
|
|
// onerror will replace this.
|
|
try {
|
|
parser.process(ical);
|
|
} catch (e) {
|
|
callback(e);
|
|
}
|
|
},
|
|
|
|
_defaultMaxDate: function() {
|
|
var now = new Date();
|
|
|
|
return new ICAL.Time({
|
|
year: now.getFullYear(),
|
|
// three months in advance
|
|
// +1 because js months are zero based
|
|
month: now.getMonth() + 6,
|
|
day: now.getDate()
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Expands a list recurring events by their component.
|
|
*
|
|
* It is expected for this function to receive an array
|
|
* of items each structured as a icalComponent.
|
|
*
|
|
* [
|
|
* { ical: '...', lastRecurrenceId: '..', iterator: '...' },
|
|
* ...
|
|
* ]
|
|
*
|
|
* @param {Array.<icalComponent>} components list of icalComponents.
|
|
* @param {Calendar.Responder} stream to emit events.
|
|
* @param {Object} options list of options.
|
|
* @param {Object} options.maxDate maximum date to expand to.
|
|
* @param {Function} callback only sends an error if fatal.
|
|
*/
|
|
expandComponents: function(components, options, stream, callback) {
|
|
var pending = components.length;
|
|
|
|
function next() {
|
|
if (!(--pending)) {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
|
|
components.forEach(function(component) {
|
|
var ical = component.ical;
|
|
var localOpts = {
|
|
maxDate: options.maxDate,
|
|
iterator: component.iterator
|
|
};
|
|
|
|
if (component.lastRecurrenceId) {
|
|
localOpts.minDate = component.lastRecurrenceId;
|
|
}
|
|
|
|
// expand each component
|
|
this.expandRecurringEvent(ical, localOpts, stream,
|
|
function(err, iter, lastRecurId, uid) {
|
|
|
|
if (err) {
|
|
stream.emit('error', err);
|
|
next();
|
|
return;
|
|
}
|
|
|
|
stream.emit('component', {
|
|
eventId: uid,
|
|
lastRecurrenceId: lastRecurId,
|
|
ical: ical,
|
|
iterator: iter
|
|
});
|
|
|
|
next();
|
|
});
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* Options:
|
|
*
|
|
* - iterator: (ICAL.RecurExpander) optional recurrence expander
|
|
* used to resume the iterator state for existing events.
|
|
*
|
|
* - maxDate: if instance ends after this date stop expansion.
|
|
*
|
|
*
|
|
* Returns:
|
|
*
|
|
* [
|
|
* {
|
|
* start: { offset: inMS, utc: ms },
|
|
* endDate: // same format as start,
|
|
* recurrenceId: // id of specific recurrence.
|
|
* uid: // uid of event
|
|
* isException: // true when is exception to usual rule.
|
|
* },
|
|
* //...
|
|
* ]
|
|
*/
|
|
expandRecurringEvent: function(component, options, stream, callback) {
|
|
var self = this;
|
|
var maxDate;
|
|
var minDate = null;
|
|
var now;
|
|
|
|
if (options.minDate) {
|
|
minDate = this.formatInputTime(options.minDate);
|
|
}
|
|
|
|
if (options.maxDate) {
|
|
maxDate = this.formatInputTime(options.maxDate);
|
|
}
|
|
|
|
if (!('now' in options)) {
|
|
options.now = ICAL.Time.now();
|
|
}
|
|
|
|
now = options.now;
|
|
|
|
// convert to rich ical event
|
|
this.parseEvent(component, function(err, event) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var iter = IcalRecurExpansion.forEach(
|
|
event,
|
|
options.iterator,
|
|
occuranceHandler,
|
|
minDate,
|
|
maxDate
|
|
);
|
|
|
|
function occuranceHandler(next) {
|
|
var details = event.getOccurrenceDetails(next);
|
|
var inFuture = details.endDate.compare(now);
|
|
|
|
debug('alarm time',
|
|
event.summary,
|
|
'will add ' + String(inFuture),
|
|
'start:', details.startDate.toJSDate().toString(),
|
|
'end:', details.endDate.toJSDate().toString(),
|
|
'now:', now.toJSDate().toString());
|
|
|
|
var occurrence = {
|
|
start: self.formatICALTime(details.startDate),
|
|
end: self.formatICALTime(details.endDate),
|
|
recurrenceId: self.formatICALTime(next),
|
|
eventId: details.item.uid,
|
|
isException: details.item.isRecurrenceException()
|
|
};
|
|
|
|
// only set alarms for those dates in the future...
|
|
if (inFuture >= 0) {
|
|
var alarms = self._displayAlarms(details);
|
|
if (alarms) {
|
|
occurrence.alarms = alarms;
|
|
}
|
|
}
|
|
|
|
stream.emit('occurrence', occurrence);
|
|
}
|
|
|
|
var lastRecurrence;
|
|
|
|
if (iter.complete) {
|
|
// when the iterator is complete
|
|
// last recurrence is false.
|
|
// We use this to signify the end
|
|
// of the iteration cycle.
|
|
lastRecurrence = false;
|
|
} else {
|
|
// its very important all times used
|
|
// for comparison are based on the recurrence id
|
|
// and not the start date as those can change
|
|
// with exceptions...
|
|
lastRecurrence = self.formatICALTime(
|
|
iter.last
|
|
);
|
|
}
|
|
|
|
callback(
|
|
null,
|
|
iter.toJSON(),
|
|
lastRecurrence,
|
|
event.uid
|
|
);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Handle a single caldav event response.
|
|
*
|
|
* @param {String} url location of event.
|
|
* @param {Object} response caldav response object.
|
|
* @param {Calendar.Responder} responder event emitter.
|
|
* @param {Function} callback node style callback fired after event parsing.
|
|
*/
|
|
_handleCaldavEvent: function(url, response, stream, callback) {
|
|
var self = this;
|
|
var etag = response.getetag;
|
|
var event = response['calendar-data'];
|
|
|
|
if (event.status != 200) {
|
|
callback(new Error('non 200 status code "' + url + '"'));
|
|
return;
|
|
}
|
|
|
|
// process event
|
|
var ical = event.value;
|
|
this.parseEvent(ical, function(err, event) {
|
|
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var result = self._formatEvent(etag.value, url, ical, event);
|
|
stream.emit('event', result);
|
|
|
|
var options = {
|
|
maxDate: self._defaultMaxDate(),
|
|
now: ICAL.Time.now()
|
|
};
|
|
|
|
self.expandRecurringEvent(event, options, stream,
|
|
function(err, iter, lastRecurrenceId) {
|
|
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
if (!event.isRecurring()) {
|
|
stream.emit('component', {
|
|
eventId: result.id,
|
|
isRecurring: false,
|
|
ical: ical
|
|
});
|
|
} else {
|
|
stream.emit('component', {
|
|
eventId: result.id,
|
|
lastRecurrenceId: lastRecurrenceId,
|
|
ical: ical,
|
|
iterator: iter
|
|
});
|
|
}
|
|
|
|
callback(null);
|
|
});
|
|
});
|
|
},
|
|
|
|
streamEvents: function(account, calendar, options, stream, callback) {
|
|
var self = this;
|
|
var hasCompleted = false;
|
|
var connection = this._createConnection(account);
|
|
|
|
var cache = options.cached;
|
|
|
|
// we don't need to pass this around anywhere.
|
|
//delete options.cache;
|
|
|
|
var request = this._requestEvents(connection, calendar, options);
|
|
var pending = 0;
|
|
|
|
function next(err) {
|
|
if (err) {
|
|
try {
|
|
stream.emit('error', err);
|
|
} catch (e) {
|
|
console.error('Failed to transport err:', err.toString(), err.stack);
|
|
}
|
|
}
|
|
|
|
if (!(--pending) && hasCompleted) {
|
|
callback(null);
|
|
}
|
|
}
|
|
|
|
function handleResponse(url, data) {
|
|
if (!data || !data['calendar-data']) {
|
|
// throw some error;
|
|
console.error('Could not sync: ', url);
|
|
return;
|
|
}
|
|
var etag = data.getetag.value;
|
|
if (url in cache) {
|
|
// don't need to track this for missing events.
|
|
if (etag !== cache[url].syncToken) {
|
|
pending++;
|
|
self._handleCaldavEvent(url, data, stream, next);
|
|
}
|
|
|
|
delete cache[url];
|
|
} else {
|
|
pending++;
|
|
self._handleCaldavEvent(url, data, stream, next);
|
|
}
|
|
}
|
|
|
|
request.sax.on('DAV:/response', handleResponse);
|
|
|
|
request.send(function(err) {
|
|
hasCompleted = true;
|
|
// when the request is completed stop listening
|
|
// for sax events.
|
|
request.sax.removeEventListener(
|
|
'DAV:/response', handleResponse
|
|
);
|
|
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
if (!pending) {
|
|
var missing = [];
|
|
|
|
for (var url in cache) {
|
|
missing.push(cache[url].id);
|
|
}
|
|
|
|
// send missing events
|
|
stream.emit('missingEvents', missing);
|
|
|
|
// notify the requester that we have completed.
|
|
callback();
|
|
}
|
|
});
|
|
},
|
|
|
|
_assetRequest: function(connection, url) {
|
|
return new Caldav.Request.Asset(connection, url);
|
|
},
|
|
|
|
deleteEvent: function(account, calendar, event, callback) {
|
|
var connection = this._createConnection(account);
|
|
|
|
var req = this._assetRequest(connection, event.url);
|
|
|
|
req.delete({}, function(err) {
|
|
callback(err);
|
|
});
|
|
},
|
|
|
|
addAlarms: function(component, alarms, account) {
|
|
alarms = alarms || [];
|
|
|
|
for (var i = 0; i < alarms.length; i++) {
|
|
|
|
var valarm = new ICAL.Component('valarm');
|
|
|
|
// valarm details
|
|
valarm.addPropertyWithValue('action', alarms[i].action);
|
|
valarm.addPropertyWithValue('description', 'This is an event reminder');
|
|
var trigger = valarm.addPropertyWithValue('trigger',
|
|
ICAL.Duration.fromSeconds(
|
|
alarms[i].trigger
|
|
)
|
|
);
|
|
trigger.setParameter('relative', 'START');
|
|
component.addSubcomponent(valarm);
|
|
|
|
// Check if we need to mirror the VALARM onto email
|
|
if (this.mirrorAlarms(account)) {
|
|
valarm = new ICAL.Component('valarm');
|
|
valarm.addPropertyWithValue('action', 'EMAIL');
|
|
valarm.addPropertyWithValue('description',
|
|
'This is an event reminder');
|
|
valarm.addPropertyWithValue('ATTENDEE', account.user);
|
|
trigger = valarm.addPropertyWithValue('trigger',
|
|
ICAL.Duration.fromSeconds(
|
|
alarms[i].trigger
|
|
)
|
|
);
|
|
trigger.setParameter('relative', 'START');
|
|
component.addSubcomponent(valarm);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update absolute alarm times when the startDate changes.
|
|
*
|
|
* @param {ICAL.Time} originalDate of the event.
|
|
* @param {ICAL.Event} event to update.
|
|
*/
|
|
adjustAbsoluteAlarms: function(originalDate, event) {
|
|
var alarms = event.component.getAllSubcomponents('valarm');
|
|
|
|
alarms.forEach(function(alarm) {
|
|
var trigger = alarm.getFirstProperty('trigger');
|
|
var value = trigger.getValues()[0].clone();
|
|
|
|
// absolute time
|
|
if (value instanceof ICAL.Time) {
|
|
// find absolute time difference
|
|
var diff = value.subtractDateTz(originalDate);
|
|
trigger.setValue(diff);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Yahoo needs us to mirror all alarms as EMAIL alarms
|
|
*/
|
|
mirrorAlarms: function(account) {
|
|
return account && account.domain === 'https://caldav.calendar.yahoo.com';
|
|
},
|
|
|
|
createEvent: function(account, calendar, event, callback) {
|
|
var connection = this._createConnection(account);
|
|
var vcalendar = new ICAL.Component('vcalendar');
|
|
var icalEvent = new ICAL.Event();
|
|
|
|
// vcalendar details
|
|
vcalendar.addPropertyWithValue('prodid', this.icalProductId);
|
|
vcalendar.addPropertyWithValue('version', this.icalVersion);
|
|
|
|
// text fields
|
|
icalEvent.uid = uuid();
|
|
icalEvent.summary = event.title;
|
|
icalEvent.description = event.description;
|
|
icalEvent.location = event.location;
|
|
icalEvent.sequence = 1;
|
|
|
|
// time fields
|
|
icalEvent.startDate = this.formatInputTime(event.start);
|
|
icalEvent.endDate = this.formatInputTime(event.end);
|
|
|
|
// alarms
|
|
this.addAlarms(icalEvent.component, event.alarms, account);
|
|
|
|
vcalendar.addSubcomponent(icalEvent.component);
|
|
|
|
var url = calendar.url + icalEvent.uid + '.ics';
|
|
var req = this._assetRequest(connection, url);
|
|
|
|
event.id = icalEvent.uid;
|
|
event.url = url;
|
|
event.icalComponent = vcalendar.toString();
|
|
|
|
req.put({}, event.icalComponent, function(err, data, xhr) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var token = xhr.getResponseHeader('Etag');
|
|
event.syncToken = token;
|
|
// TODO: error handling
|
|
callback(err, event);
|
|
});
|
|
|
|
},
|
|
|
|
/**
|
|
* Updates a single caldav event.
|
|
* Will handle updating both primary events and exceptions.
|
|
*
|
|
* @param {Object} account full account details.
|
|
* @param {Object} calendar full calendar details.
|
|
* @param {Object} eventDetails details to update the event.
|
|
* unmodified parsed ical component. (VCALENDAR).
|
|
*/
|
|
updateEvent: function(account, calendar, eventDetails, callback) {
|
|
var connection = this._createConnection(account);
|
|
|
|
var icalComponent = eventDetails.icalComponent;
|
|
var event = eventDetails.event;
|
|
|
|
var self = this;
|
|
var req = this._assetRequest(connection, event.url);
|
|
|
|
// parse event
|
|
this.parseEvent(icalComponent, function(err, icalEvent) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var target = icalEvent;
|
|
var vcalendar = icalEvent.component.parent;
|
|
var originalStartDate = target.startDate;
|
|
|
|
// find correct event
|
|
if (event.recurrenceId) {
|
|
var rid = self.formatInputTime(event.recurrenceId);
|
|
rid = rid.toString();
|
|
if (icalEvent.exceptions[rid]) {
|
|
target = icalEvent.exceptions[rid];
|
|
}
|
|
}
|
|
|
|
// vcalendar pieces
|
|
vcalendar.updatePropertyWithValue('prodid', self.icalProductId);
|
|
|
|
// text fields
|
|
target.summary = event.title;
|
|
target.description = event.description;
|
|
target.location = event.location;
|
|
|
|
if (!target.sequence) {
|
|
target.sequence = 0;
|
|
}
|
|
|
|
target.sequence = parseInt(target.sequence, 10) + 1;
|
|
|
|
// time fields
|
|
target.startDate = self.formatInputTime(
|
|
event.start
|
|
);
|
|
|
|
target.endDate = self.formatInputTime(
|
|
event.end
|
|
);
|
|
|
|
// adjust absolute alarm time ( we do this before adding/changing our
|
|
// new alarm times )
|
|
self.adjustAbsoluteAlarms(originalStartDate, target);
|
|
|
|
// We generally want to remove all 'DISPLAY' alarms
|
|
// UNLESS we are dealing with a YAHOO account
|
|
// Then we overwrite all alarms
|
|
var alarms = target.component.getAllSubcomponents('valarm');
|
|
alarms.forEach(function(alarm) {
|
|
var action = alarm.getFirstPropertyValue('action');
|
|
if (action === 'DISPLAY' || self.mirrorAlarms(account)) {
|
|
target.component.removeSubcomponent(alarm);
|
|
}
|
|
});
|
|
self.addAlarms(target.component, event.alarms, account);
|
|
|
|
var vcal = target.component.parent.toString();
|
|
event.icalComponent = vcal;
|
|
|
|
req.put({}, vcal, function(err, data, xhr) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var token = xhr.getResponseHeader('Etag');
|
|
event.syncToken = token;
|
|
// TODO: error handling
|
|
callback(err, event);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
});
|