contacts/js/dav/lib/calendars.js

275 lines
7.9 KiB
JavaScript

import co from 'co';
import url from 'url';
import fuzzyUrlEquals from './fuzzy_url_equals';
import { Calendar, CalendarObject } from './model';
import * as ns from './namespace';
import * as request from './request';
import * as webdav from './webdav';
let debug = require('./debug')('dav:calendars');
const ICAL_OBJS = new Set([
'VEVENT',
'VTODO',
'VJOURNAL',
'VFREEBUSY',
'VTIMEZONE',
'VALARM'
]);
/**
* @param {dav.Account} account to fetch calendars for.
*/
export let listCalendars = co.wrap(function *(account, options) {
debug(`Fetch calendars from home url ${account.homeUrl}`);
var req = request.propfind({
props: [
{ name: 'calendar-description', namespace: ns.CALDAV },
{ name: 'calendar-timezone', namespace: ns.CALDAV },
{ name: 'displayname', namespace: ns.DAV },
{ name: 'getctag', namespace: ns.CALENDAR_SERVER },
{ name: 'resourcetype', namespace: ns.DAV },
{ name: 'supported-calendar-component-set', namespace: ns.CALDAV },
{ name: 'sync-token', namespace: ns.DAV }
],
depth: 1
});
let responses = yield options.xhr.send(req, account.homeUrl, {
sandbox: options.sandbox
});
debug(`Found ${responses.length} calendars.`);
let cals = responses
.filter(res => {
// We only want the calendar if it contains iCalendar objects.
let components = res.props.supportedCalendarComponentSet || [];
return components.reduce((hasObjs, component) => {
return hasObjs || ICAL_OBJS.has(component)
}, false)
})
.map(res => {
debug(`Found calendar ${res.props.displayname},
props: ${JSON.stringify(res.props)}`);
return new Calendar({
data: res,
account: account,
description: res.props.calendarDescription,
timezone: res.props.calendarTimezone,
url: url.resolve(account.rootUrl, res.href),
ctag: res.props.getctag,
displayName: res.props.displayname,
components: res.props.supportedCalendarComponentSet,
resourcetype: res.props.resourcetype,
syncToken: res.props.syncToken
});
});
yield cals.map(co.wrap(function *(cal) {
cal.reports = yield webdav.supportedReportSet(cal, options);
}));
return cals;
});
/**
* @param {dav.Calendar} calendar the calendar to put the object on.
* @return {Promise} promise will resolve when the calendar has been created.
*
* Options:
*
* (String) data - rfc 5545 VCALENDAR object.
* (String) filename - name for the calendar ics file.
* (dav.Sandbox) sandbox - optional request sandbox.
* (dav.Transport) xhr - request sender.
*/
export function createCalendarObject(calendar, options) {
var objectUrl = url.resolve(calendar.url, options.filename);
return webdav.createObject(objectUrl, options.data, options);
};
/**
* @param {dav.CalendarObject} calendarObject updated calendar object.
* @return {Promise} promise will resolve when the calendar has been updated.
*
* Options:
*
* (dav.Sandbox) sandbox - optional request sandbox.
* (dav.Transport) xhr - request sender.
*/
export function updateCalendarObject(calendarObject, options) {
return webdav.updateObject(
calendarObject.url,
calendarObject.calendarData,
calendarObject.etag,
options
);
}
/**
* @param {dav.CalendarObject} calendarObject target calendar object.
* @return {Promise} promise will resolve when the calendar has been deleted.
*
* Options:
*
* (dav.Sandbox) sandbox - optional request sandbox.
* (dav.Transport) xhr - request sender.
*/
export function deleteCalendarObject(calendarObject, options) {
return webdav.deleteObject(
calendarObject.url,
calendarObject.etag,
options
);
}
/**
* @param {dav.Calendar} calendar the calendar to fetch objects for.
*
* Options:
*
* (Array.<Object>) filters - optional caldav filters.
* (dav.Sandbox) sandbox - optional request sandbox.
* (dav.Transport) xhr - request sender.
*/
export let listCalendarObjects = co.wrap(function *(calendar, options) {
debug(`Doing REPORT on calendar ${calendar.url} which belongs to
${calendar.account.credentials.username}`);
let filters = options.filters || [{
type: 'comp-filter',
attrs: { name: 'VCALENDAR' },
children: [{
type: 'comp-filter',
attrs: { name: 'VEVENT' }
}]
}];
let req = request.calendarQuery({
depth: 1,
props: [
{ name: 'getetag', namespace: ns.DAV },
{ name: 'calendar-data', namespace: ns.CALDAV }
],
filters: filters
});
let responses = yield options.xhr.send(req, calendar.url, {
sandbox: options.sandbox
});
return responses.map(res => {
debug(`Found calendar object with url ${res.href}`);
return new CalendarObject({
data: res,
calendar: calendar,
url: url.resolve(calendar.account.rootUrl, res.href),
etag: res.props.getetag,
calendarData: res.props.calendarData
});
});
});
/**
* @param {dav.Calendar} calendar the calendar to fetch updates to.
* @return {Promise} promise will resolve with updated calendar object.
*
* Options:
*
* (Array.<Object>) filters - list of caldav filters to send with request.
* (dav.Sandbox) sandbox - optional request sandbox.
* (String) syncMethod - either 'basic' or 'webdav'. If unspecified, will
* try to do webdav sync and failover to basic sync if rfc 6578 is not
* supported by the server.
* (String) timezone - VTIMEZONE calendar object.
* (dav.Transport) xhr - request sender.
*/
export function syncCalendar(calendar, options) {
options.basicSync = basicSync;
options.webdavSync = webdavSync;
return webdav.syncCollection(calendar, options);
}
/**
* @param {dav.Account} account the account to fetch updates for.
* @return {Promise} promise will resolve with updated account.
*
* Options:
*
* (dav.Sandbox) sandbox - optional request sandbox.
* (dav.Transport) xhr - request sender.
*/
export let syncCaldavAccount = co.wrap(function *(account, options={}) {
options.loadObjects = false;
if (!account.calendars) account.calendars = [];
let cals = yield listCalendars(account, options);
cals
.filter(cal => {
// Filter the calendars not previously seen.
return account.calendars.every(prev => !fuzzyUrlEquals(prev.url, cal.url));
})
.forEach(cal => {
// Add them to the account's calendar list.
account.calendars.push(cal);
});
options.loadObjects = true;
yield account.calendars.map(co.wrap(function *(cal, index) {
try {
yield syncCalendar(cal, options);
} catch (error) {
debug(`Sync calendar ${cal.displayName} failed with ${error}`);
account.calendars.splice(index, 1);
}
}));
return account;
});
let basicSync = co.wrap(function *(calendar, options) {
let sync = yield webdav.isCollectionDirty(calendar, options);
if (!sync) {
debug('Local ctag matched remote! No need to sync :).');
return calendar;
}
debug('ctag changed so we need to fetch stuffs.');
calendar.objects = yield listCalendarObjects(calendar, options);
return calendar;
});
let webdavSync = co.wrap(function *(calendar, options) {
var req = request.syncCollection({
props: [
{ name: 'getetag', namespace: ns.DAV },
{ name: 'calendar-data', namespace: ns.CALDAV }
],
syncLevel: 1,
syncToken: calendar.syncToken
});
let result = yield options.xhr.send(req, calendar.url, {
sandbox: options.sandbox
});
// TODO(gareth): Handle creations and deletions.
result.responses.forEach(function(response) {
// Find the calendar object that this response corresponds with.
var calendarObject = calendar.objects.filter(function(object) {
return fuzzyUrlEquals(object.url, response.href);
})[0];
if (!calendarObject) {
return;
}
calendarObject.etag = response.props.getetag;
calendarObject.calendarData = response.props.calendarData;
});
calendar.syncToken = result.syncToken;
return calendar;
});