diff options
Diffstat (limited to 'js/dav/lib/calendars.js')
-rw-r--r-- | js/dav/lib/calendars.js | 274 |
1 files changed, 274 insertions, 0 deletions
diff --git a/js/dav/lib/calendars.js b/js/dav/lib/calendars.js new file mode 100644 index 00000000..89a44fe1 --- /dev/null +++ b/js/dav/lib/calendars.js @@ -0,0 +1,274 @@ +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; +}); |