Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
712 lines
16 KiB
JavaScript
712 lines
16 KiB
JavaScript
define(['require','exports','module','./abstract','common/error','common/calc','./caldav_pull_events','common/error','common/error','./local','common/error','core','common/is_offline','event_mutations','common/next_tick'],function(require, exports, module) {
|
|
'use strict';
|
|
|
|
var Abstract = require('./abstract');
|
|
var Authentication = require('common/error').Authentication;
|
|
var Calc = require('common/calc');
|
|
var CaldavPullEvents = require('./caldav_pull_events');
|
|
var CalendarError = require('common/error');
|
|
var InvalidServer = require('common/error').InvalidServer;
|
|
var Local = require('./local');
|
|
var ServerFailure = require('common/error').ServerFailure;
|
|
var core = require('core');
|
|
var isOffline = require('common/is_offline');
|
|
var mutations = require('event_mutations');
|
|
var nextTick = require('common/next_tick');
|
|
|
|
var CALDAV_ERROR_MAP = {
|
|
'caldav-authentication': Authentication,
|
|
'caldav-invalid-entrypoint': InvalidServer,
|
|
'caldav-server-failure': ServerFailure
|
|
};
|
|
|
|
function mapError(error, detail) {
|
|
console.error('Error with name:', error.name);
|
|
var calError = CALDAV_ERROR_MAP[error.name];
|
|
if (!calError) {
|
|
calError = new CalendarError(error.name, detail);
|
|
} else {
|
|
calError = new calError(detail);
|
|
}
|
|
|
|
return calError;
|
|
}
|
|
|
|
function CaldavProvider() {
|
|
Abstract.apply(this, arguments);
|
|
|
|
var storeFactory = core.storeFactory;
|
|
this.service = core.serviceController;
|
|
this.accounts = storeFactory.get('Account');
|
|
this.busytimes = storeFactory.get('Busytime');
|
|
this.events = storeFactory.get('Event');
|
|
this.icalComponents = storeFactory.get('IcalComponent');
|
|
}
|
|
module.exports = CaldavProvider;
|
|
|
|
CaldavProvider.prototype = {
|
|
__proto__: Abstract.prototype,
|
|
role: 'caldav',
|
|
useUrl: true,
|
|
useCredentials: true,
|
|
canSync: true,
|
|
canExpandRecurringEvents: true,
|
|
|
|
// allow us to test the offline behavior
|
|
isOffline: isOffline,
|
|
|
|
/**
|
|
* Number of dates in the past to sync.
|
|
* This is usually from the first sync date.
|
|
*/
|
|
daysToSyncInPast: 30,
|
|
|
|
canCreateEvent: true,
|
|
canUpdateEvent: true,
|
|
canDeleteEvent: true,
|
|
|
|
hasAccountSettings: true,
|
|
|
|
/**
|
|
* Error handling can be complex- this is the centralized location where
|
|
* methods can send their error state and some context (an account).
|
|
*
|
|
* this._handleServiceError(
|
|
* err,
|
|
* { account: account, calendar: calendar }
|
|
* );
|
|
*
|
|
* @param {Object} rawErr from service.
|
|
* @param {Object} detail for the error.
|
|
*/
|
|
_handleServiceError: function(rawErr, detail) {
|
|
var calendarErr = mapError(rawErr, detail);
|
|
|
|
// when we receive a permanent error we should mark the account with an
|
|
// error.
|
|
if (
|
|
calendarErr instanceof Authentication ||
|
|
calendarErr instanceof InvalidServer
|
|
) {
|
|
// there must always be an account
|
|
if (detail.account) {
|
|
// but we only mark it with a permanent error if its persisted.
|
|
if (detail.account._id) {
|
|
this.accounts.markWithError(detail.account, calendarErr);
|
|
}
|
|
} else {
|
|
console.error('Permanent server error without an account!');
|
|
}
|
|
}
|
|
|
|
return calendarErr;
|
|
},
|
|
|
|
/**
|
|
* Determines the capabilities of a specific calendar.
|
|
*
|
|
* The .remote property should contain a .privilegeSet array
|
|
* with the caldav specific names of privileges.
|
|
* In the case where .privilegeSet is missing all privileges are granted.
|
|
*
|
|
* (see http://tools.ietf.org/html/rfc3744#section-5.4).
|
|
*
|
|
* - write-content: (PUT) can edit/add events
|
|
* - unbind: (DELETE) remove events
|
|
*
|
|
*
|
|
* There are aggregate values (write for example) but
|
|
* the spec states the specific permissions must also be expanded
|
|
* so even if they have full write permissions we only check
|
|
* for write-content.
|
|
*
|
|
* @param {Object} calendar object with caldav remote details.
|
|
* @return {Object} object with three properties
|
|
* (canUpdate, canDelete, canCreate).
|
|
*/
|
|
calendarCapabilities: function(calendar) {
|
|
var remote = calendar.remote;
|
|
|
|
if (!remote.privilegeSet) {
|
|
return {
|
|
canUpdateEvent: true,
|
|
canDeleteEvent: true,
|
|
canCreateEvent: true
|
|
};
|
|
}
|
|
|
|
var privilegeSet = remote.privilegeSet;
|
|
var canWriteConent = privilegeSet.indexOf('write-content') !== -1;
|
|
|
|
return {
|
|
canUpdateEvent: canWriteConent,
|
|
canCreateEvent: canWriteConent,
|
|
canDeleteEvent: privilegeSet.indexOf('unbind') !== -1
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Returns the capabilities of a single event.
|
|
*
|
|
* @param {Object} event local object.
|
|
* @param {Function} callback [err, caps].
|
|
*/
|
|
eventCapabilities: function(event, callback) {
|
|
if (event.remote.isRecurring) {
|
|
// XXX: for now recurring events cannot be edited
|
|
nextTick(function() {
|
|
callback(null, {
|
|
canUpdate: false,
|
|
canDelete: false,
|
|
canCreate: false
|
|
});
|
|
});
|
|
|
|
} else {
|
|
var calendarStore = core.storeFactory.get('Calendar');
|
|
|
|
calendarStore.get(event.calendarId, function(err, calendar) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
var caps = this.calendarCapabilities(
|
|
calendar
|
|
);
|
|
|
|
callback(null, {
|
|
canCreate: caps.canCreateEvent,
|
|
canUpdate: caps.canUpdateEvent,
|
|
canDelete: caps.canDeleteEvent
|
|
});
|
|
}.bind(this));
|
|
}
|
|
},
|
|
|
|
getAccount: function(account, callback) {
|
|
if (this.bailWhenOffline(callback)) {
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
this.service.request(
|
|
'caldav',
|
|
'getAccount',
|
|
account,
|
|
function(err, data) {
|
|
if (err) {
|
|
return callback(
|
|
self._handleServiceError(err, { account: account })
|
|
);
|
|
}
|
|
callback(null, data);
|
|
}
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Hook to format remote data if needed.
|
|
*/
|
|
formatRemoteCalendar: function(calendar) {
|
|
if (!calendar.color) {
|
|
calendar.color = this.defaultColor;
|
|
}
|
|
|
|
return calendar;
|
|
},
|
|
|
|
findCalendars: function(account, callback) {
|
|
if (this.bailWhenOffline(callback)) {
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
function formatCalendars(err, data) {
|
|
if (err) {
|
|
return callback(self._handleServiceError(err, {
|
|
account: account
|
|
}));
|
|
}
|
|
|
|
// format calendars if needed
|
|
if (data) {
|
|
for (var key in data) {
|
|
data[key] = self.formatRemoteCalendar(data[key]);
|
|
}
|
|
}
|
|
|
|
callback(err, data);
|
|
}
|
|
|
|
this.service.request(
|
|
'caldav',
|
|
'findCalendars',
|
|
account.toJSON(),
|
|
formatCalendars
|
|
);
|
|
},
|
|
|
|
_syncEvents: function(account, calendar, cached, callback) {
|
|
|
|
var startDate;
|
|
// calculate the first date we want to sync
|
|
if (!calendar.firstEventSyncDate) {
|
|
startDate = Calc.createDay(new Date());
|
|
|
|
// will be persisted if sync is successful (clone is required)
|
|
calendar.firstEventSyncDate = new Date(
|
|
startDate.valueOf()
|
|
);
|
|
} else {
|
|
startDate = new Date(calendar.firstEventSyncDate.valueOf());
|
|
}
|
|
|
|
// start date - the amount of days is the sync range
|
|
startDate.setDate(startDate.getDate() - this.daysToSyncInPast);
|
|
|
|
var options = {
|
|
startDate: startDate,
|
|
cached: cached
|
|
};
|
|
|
|
var stream = this.service.stream(
|
|
'caldav',
|
|
'streamEvents',
|
|
account.toJSON(),
|
|
calendar.remote,
|
|
options
|
|
);
|
|
|
|
var pull = new CaldavPullEvents(stream, {
|
|
account: account,
|
|
calendar: calendar
|
|
});
|
|
|
|
var calendarStore = core.storeFactory.get('Calendar');
|
|
var syncStart = new Date();
|
|
|
|
var self = this;
|
|
stream.request(function(err) {
|
|
if (err) {
|
|
return callback(
|
|
self._handleServiceError(err, {
|
|
account: account,
|
|
calendar: calendar
|
|
})
|
|
);
|
|
}
|
|
|
|
var trans = pull.commit(function(commitErr) {
|
|
if (commitErr) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
callback(null);
|
|
});
|
|
|
|
/**
|
|
* Successfully synchronizing a calendar indicates we can remove this
|
|
* error.
|
|
*/
|
|
calendar.error = undefined;
|
|
|
|
calendar.lastEventSyncToken = calendar.remote.syncToken;
|
|
calendar.lastEventSyncDate = syncStart;
|
|
|
|
calendarStore.persist(calendar, trans);
|
|
|
|
});
|
|
|
|
return pull;
|
|
},
|
|
|
|
/**
|
|
* Builds list of event urls & sync tokens.
|
|
*
|
|
* @param {Calendar.Model.Calendar} calender model instance.
|
|
* @param {Function} callback node style [err, results].
|
|
*/
|
|
_cachedEventsFor: function(calendar, callback) {
|
|
var store = core.storeFactory.get('Event');
|
|
|
|
store.eventsForCalendar(calendar._id, function(err, results) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var list = Object.create(null);
|
|
|
|
var i = 0;
|
|
var len = results.length;
|
|
var item;
|
|
|
|
for (; i < len; i++) {
|
|
item = results[i];
|
|
list[item.remote.url] = {
|
|
syncToken: item.remote.syncToken,
|
|
id: item._id
|
|
};
|
|
}
|
|
|
|
callback(null, list);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Sync remote and local events for a calendar.
|
|
*/
|
|
syncEvents: function(account, calendar, callback) {
|
|
var self = this;
|
|
|
|
if (this.bailWhenOffline(callback)) {
|
|
return;
|
|
}
|
|
|
|
if (!calendar._id) {
|
|
throw new Error('calendar must be assigned an _id');
|
|
}
|
|
|
|
// Don't attempt to sync when provider cannot
|
|
// or we have matching tokens
|
|
if ((calendar.lastEventSyncToken &&
|
|
calendar.lastEventSyncToken === calendar.remote.syncToken)) {
|
|
return nextTick(callback);
|
|
}
|
|
|
|
this._cachedEventsFor(calendar, function(err, results) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
self._syncEvents(
|
|
account,
|
|
calendar,
|
|
results,
|
|
callback
|
|
);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* See abstract for contract details...
|
|
*
|
|
* Finds all ical components that have not been expanded
|
|
* beyond the given point and expands / persists them.
|
|
*
|
|
* @param {Date} maxDate maximum date to expand to.
|
|
* @param {Function} callback [err, didExpand].
|
|
*/
|
|
ensureRecurrencesExpanded: function(maxDate, callback) {
|
|
var self = this;
|
|
this.icalComponents.findRecurrencesBefore(maxDate,
|
|
function(err, results) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
if (!results.length) {
|
|
callback(null, false);
|
|
return;
|
|
}
|
|
|
|
// CaldavPullRequest is based on a calendar/account combination
|
|
// so we must group all of the outstanding components into
|
|
// their calendars before we can begin expanding them.
|
|
var groups = Object.create(null);
|
|
results.forEach(function(comp) {
|
|
var calendarId = comp.calendarId;
|
|
if (!(calendarId in groups)) {
|
|
groups[calendarId] = [];
|
|
}
|
|
|
|
groups[calendarId].push(comp);
|
|
});
|
|
|
|
var pullGroups = [];
|
|
var pending = 0;
|
|
var options = {
|
|
maxDate: Calc.dateToTransport(maxDate)
|
|
};
|
|
|
|
function next(err, pull) {
|
|
pullGroups.push(pull);
|
|
if (!(--pending)) {
|
|
var trans = core.db.transaction(
|
|
['icalComponents', 'alarms', 'busytimes'],
|
|
'readwrite'
|
|
);
|
|
|
|
trans.oncomplete = function() {
|
|
callback(null, true);
|
|
};
|
|
|
|
trans.onerror = function(event) {
|
|
callback(event.result.error.name);
|
|
};
|
|
|
|
pullGroups.forEach(function(pull) {
|
|
pull.commit(trans);
|
|
});
|
|
}
|
|
}
|
|
|
|
for (var calendarId in groups) {
|
|
pending++;
|
|
self._expandComponents(
|
|
calendarId,
|
|
groups[calendarId],
|
|
options,
|
|
next
|
|
);
|
|
}
|
|
|
|
});
|
|
},
|
|
|
|
_expandComponents: function(calendarId, comps, options, callback) {
|
|
var calStore = core.storeFactory.get('Calendar');
|
|
|
|
calStore.ownersOf(calendarId, function(err, owners) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
var calendar = owners.calendar;
|
|
var account = owners.account;
|
|
|
|
var stream = this.service.stream(
|
|
'caldav',
|
|
'expandComponents',
|
|
comps,
|
|
options
|
|
);
|
|
|
|
var pull = new CaldavPullEvents(
|
|
stream,
|
|
{
|
|
account: account,
|
|
calendar: calendar,
|
|
stores: [
|
|
'busytimes', 'alarms', 'icalComponents'
|
|
]
|
|
}
|
|
);
|
|
|
|
stream.request(function(err) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
callback(null, pull);
|
|
});
|
|
|
|
}.bind(this));
|
|
},
|
|
|
|
createEvent: function(event, busytime, callback) {
|
|
if (typeof(busytime) === 'function') {
|
|
callback = busytime;
|
|
busytime = null;
|
|
}
|
|
|
|
|
|
if (this.bailWhenOffline(callback)) {
|
|
return;
|
|
}
|
|
|
|
this.events.ownersOf(event, fetchOwners);
|
|
|
|
var self = this;
|
|
var calendar;
|
|
var account;
|
|
function fetchOwners(err, owners) {
|
|
calendar = owners.calendar;
|
|
account = owners.account;
|
|
|
|
self.service.request(
|
|
'caldav',
|
|
'createEvent',
|
|
account,
|
|
calendar.remote,
|
|
event.remote,
|
|
handleRequest
|
|
);
|
|
}
|
|
|
|
function handleRequest(err, remote) {
|
|
if (err) {
|
|
return callback(self._handleServiceError(err, {
|
|
account: account,
|
|
calendar: calendar
|
|
}));
|
|
}
|
|
|
|
var event = {
|
|
_id: calendar._id + '-' + remote.id,
|
|
calendarId: calendar._id
|
|
};
|
|
|
|
var component = {
|
|
eventId: event._id,
|
|
ical: remote.icalComponent
|
|
};
|
|
|
|
delete remote.icalComponent;
|
|
event.remote = remote;
|
|
|
|
var create = mutations.create({
|
|
event: event,
|
|
icalComponent: component
|
|
});
|
|
|
|
create.commit(function(err) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
callback(null, create.busytime, create.event);
|
|
});
|
|
}
|
|
},
|
|
|
|
updateEvent: function(event, busytime, callback) {
|
|
if (typeof(busytime) === 'function') {
|
|
callback = busytime;
|
|
busytime = null;
|
|
}
|
|
|
|
if (this.bailWhenOffline(callback)) {
|
|
return;
|
|
}
|
|
|
|
this.events.ownersOf(event, fetchOwners);
|
|
|
|
var self = this;
|
|
var calendar;
|
|
var account;
|
|
|
|
function fetchOwners(err, owners) {
|
|
calendar = owners.calendar;
|
|
account = owners.account;
|
|
|
|
self.icalComponents.get(
|
|
event._id, fetchComponent
|
|
);
|
|
}
|
|
|
|
function fetchComponent(err, ical) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
var details = {
|
|
event: event.remote,
|
|
icalComponent: ical.ical
|
|
};
|
|
|
|
self.service.request(
|
|
'caldav',
|
|
'updateEvent',
|
|
account,
|
|
calendar.remote,
|
|
details,
|
|
handleUpdate
|
|
);
|
|
}
|
|
|
|
function handleUpdate(err, remote) {
|
|
if (err) {
|
|
callback(self._handleServiceError(err, {
|
|
account: account,
|
|
calendar: calendar
|
|
}));
|
|
return;
|
|
}
|
|
|
|
var component = {
|
|
eventId: event._id,
|
|
ical: remote.icalComponent
|
|
};
|
|
|
|
delete remote.icalComponent;
|
|
event.remote = remote;
|
|
|
|
var update = mutations.update({
|
|
event: event,
|
|
icalComponent: component
|
|
});
|
|
|
|
update.commit(function(err) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
callback(null, update.busytime, update.event);
|
|
});
|
|
}
|
|
},
|
|
|
|
deleteEvent: function(event, busytime, callback) {
|
|
if (typeof(busytime) === 'function') {
|
|
callback = busytime;
|
|
busytime = null;
|
|
}
|
|
|
|
if (this.bailWhenOffline(callback)) {
|
|
return;
|
|
}
|
|
|
|
this.events.ownersOf(event, fetchOwners);
|
|
|
|
var calendar;
|
|
var account;
|
|
var self = this;
|
|
function fetchOwners(err, owners) {
|
|
calendar = owners.calendar;
|
|
account = owners.account;
|
|
|
|
self.service.request(
|
|
'caldav',
|
|
'deleteEvent',
|
|
account,
|
|
calendar.remote,
|
|
event.remote,
|
|
handleRequest
|
|
);
|
|
|
|
}
|
|
|
|
function handleRequest(err) {
|
|
if (err) {
|
|
callback(self._handleServiceError(err, {
|
|
account: account,
|
|
calendar: calendar
|
|
}));
|
|
return;
|
|
}
|
|
Local.prototype.deleteEvent.call(self, event, busytime, callback);
|
|
}
|
|
},
|
|
|
|
bailWhenOffline: function(callback) {
|
|
if (!this.offlineMessage && 'mozL10n' in window.navigator) {
|
|
this.offlineMessage = window.navigator.mozL10n.get('error-offline');
|
|
}
|
|
|
|
var ret = this.isOffline() && callback;
|
|
if (ret) {
|
|
var error = new Error();
|
|
error.name = 'offline';
|
|
error.message = this.offlineMessage;
|
|
callback(error);
|
|
}
|
|
return ret;
|
|
}
|
|
};
|
|
|
|
});
|