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.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
712
gaia/apps/calendar/js/backend/provider/caldav.js
Executable file
712
gaia/apps/calendar/js/backend/provider/caldav.js
Executable file
|
|
@ -0,0 +1,712 @@
|
|||
define(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;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
Loading…
Reference in a new issue