summaryrefslogtreecommitdiffstats
path: root/js/dav/lib/calendars.js
blob: 89a44fe184bc91922b21998cabb0792ce1f7109c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
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