VulpesOS/assets/webapps/calendar.gaiamobile.org/js/backend/provider/caldav.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

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;
}
};
});