1 /* http_caldav.c -- Routines for handling CalDAV collections in httpd
2 *
3 * Copyright (c) 1994-2011 Carnegie Mellon University. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * 3. The name "Carnegie Mellon University" must not be used to
18 * endorse or promote products derived from this software without
19 * prior written permission. For permission or any legal
20 * details, please contact
21 * Carnegie Mellon University
22 * Center for Technology Transfer and Enterprise Creation
23 * 4615 Forbes Avenue
24 * Suite 302
25 * Pittsburgh, PA 15213
26 * (412) 268-7393, fax: (412) 268-7395
27 * innovation@andrew.cmu.edu
28 *
29 * 4. Redistributions of any form whatsoever must retain the following
30 * acknowledgment:
31 * "This product includes software developed by Computing Services
32 * at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33 *
34 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41 *
42 */
43 /*
44 * TODO:
45 *
46 * - Make proxying more robust. Currently depends on calendar collections
47 * residing on same server as user's INBOX. Doesn't handle global/shared
48 * calendars.
49 * - Support COPY/MOVE on collections
50 * - Add more required properties?
51 * - free-busy-query REPORT (check ACL and transp on all calendars)
52 * - sync-collection REPORT - need to handle Depth infinity?
53 */
54
55 #include <config.h>
56
57 #include <sysexits.h>
58 #include <syslog.h>
59
60 #include <libical/ical.h>
61 #include <libxml/tree.h>
62 #include <libxml/uri.h>
63 #include <sys/types.h>
64
65 #include "acl.h"
66 #include "append.h"
67 #include "caldav_db.h"
68 #include "charset.h"
69 #include "css3_color.h"
70 #include "global.h"
71 #include "hash.h"
72 #include "httpd.h"
73 #include "http_caldav.h"
74 #include "http_caldav_sched.h"
75 #include "http_dav.h"
76 #include "http_dav_sharing.h"
77 #include "http_proxy.h"
78 #include "index.h"
79 #include "ical_support.h"
80 #include "jmap_ical.h"
81 #include "jcal.h"
82 #include "xcal.h"
83 #include "map.h"
84 #include "mailbox.h"
85 #include "mboxlist.h"
86 #include "message.h"
87 #include "message_guid.h"
88 #include "proxy.h"
89 #include "times.h"
90 #include "spool.h"
91 #include "strhash.h"
92 #include "tok.h"
93 #include "user.h"
94 #include "util.h"
95 #include "version.h"
96 #include "webdav_db.h"
97 #include "xmalloc.h"
98 #include "xml_support.h"
99 #include "xstrlcat.h"
100 #include "xstrlcpy.h"
101 #include "xstrnchr.h"
102 #include "zoneinfo_db.h"
103
104 /* generated headers are not necessarily in current directory */
105 #include "imap/http_err.h"
106 #include "imap/imap_err.h"
107
108 #define TZ_STRIP (1<<9)
109
110 #define SHARED_MODSEQ \
111 DAV_ANNOT_NS "<" XML_NS_CYRUS ">shared-modseq"
112
113
114 #ifdef HAVE_RSCALE
115 #include <unicode/uversion.h>
116
rscale_cmp(const void * a,const void * b)117 static int rscale_cmp(const void *a, const void *b)
118 {
119 /* Convert to uppercase since that's what we prefer to output */
120 return strcmp(ucase(*((char **) a)), ucase(*((char **) b)));
121 }
122 #endif /* HAVE_RSCALE */
123
124
125 static struct caldav_db *auth_caldavdb = NULL;
126 static time_t compile_time;
127 static struct buf ical_prodid_buf = BUF_INITIALIZER;
128
129 unsigned config_allowsched = IMAP_ENUM_CALDAV_ALLOWSCHEDULING_OFF;
130 const char *ical_prodid = NULL;
131 icaltimezone *utc_zone = NULL;
132 struct strlist *cua_domains = NULL;
133 icalarray *rscale_calendars = NULL;
134
135 struct partial_comp_t {
136 icalcomponent_kind kind;
137 arrayu64_t props;
138 struct partial_comp_t *sibling;
139 struct partial_comp_t *child;
140 };
141
142 struct partial_caldata_t {
143 unsigned expand : 1;
144 struct icalperiodtype range;
145 struct partial_comp_t *comp;
146 };
147
148 static int meth_options_cal(struct transaction_t *txn, void *params);
149 static int meth_get_head_cal(struct transaction_t *txn, void *params);
150 static int meth_get_head_fb(struct transaction_t *txn, void *params);
151
152 static void my_caldav_init(struct buf *serverinfo);
153 static int my_caldav_auth(const char *userid);
154 static void my_caldav_reset(void);
155 static void my_caldav_shutdown(void);
156
157 static unsigned long caldav_allow_cb(struct request_target_t *tgt);
158 static int caldav_parse_path(const char *path, struct request_target_t *tgt,
159 const char **resultstr);
160
161 static int caldav_get_validators(struct mailbox *mailbox, void *data,
162 const char *userid, struct index_record *record,
163 const char **etag, time_t *lastmod);
164
165 static modseq_t caldav_get_modseq(struct mailbox *mailbox,
166 void *data, const char *userid);
167
168 static int caldav_check_precond(struct transaction_t *txn,
169 struct meth_params *params,
170 struct mailbox *mailbox, const void *data,
171 const char *etag, time_t lastmod);
172
173 static int caldav_acl(struct transaction_t *txn, xmlNodePtr priv, int *rights);
174 static int caldav_copy(struct transaction_t *txn, void *obj,
175 struct mailbox *dest_mbox, const char *dest_rsrc,
176 void *destdb, unsigned flags);
177 static int caldav_delete_cal(struct transaction_t *txn,
178 struct mailbox *mailbox,
179 struct index_record *record, void *data);
180 static int caldav_get(struct transaction_t *txn, struct mailbox *mailbox,
181 struct index_record *record, void *data, void **obj);
182 static int caldav_mkcol(struct mailbox *mailbox);
183 static int caldav_post(struct transaction_t *txn);
184 static int caldav_patch(struct transaction_t *txn, void *obj);
185 static int caldav_put(struct transaction_t *txn, void *obj,
186 struct mailbox *mailbox, const char *resource,
187 void *destdb, unsigned flags);
188 static int caldav_import(struct transaction_t *txn, void *obj,
189 struct mailbox *mailbox, void *destdb,
190 xmlNodePtr root, xmlNsPtr *ns, unsigned flags);
191
192 static int propfind_getcontenttype(const xmlChar *name, xmlNsPtr ns,
193 struct propfind_ctx *fctx,
194 xmlNodePtr prop, xmlNodePtr resp,
195 struct propstat propstat[], void *rock);
196 static int propfind_restype(const xmlChar *name, xmlNsPtr ns,
197 struct propfind_ctx *fctx,
198 xmlNodePtr prop, xmlNodePtr resp,
199 struct propstat propstat[], void *rock);
200 static int propfind_scheduser(const xmlChar *name, xmlNsPtr ns,
201 struct propfind_ctx *fctx,
202 xmlNodePtr prop, xmlNodePtr resp,
203 struct propstat propstat[], void *rock);
204 static int propfind_caldata(const xmlChar *name, xmlNsPtr ns,
205 struct propfind_ctx *fctx,
206 xmlNodePtr prop, xmlNodePtr resp,
207 struct propstat propstat[], void *rock);
208 static int propfind_calcompset(const xmlChar *name, xmlNsPtr ns,
209 struct propfind_ctx *fctx,
210 xmlNodePtr prop, xmlNodePtr resp,
211 struct propstat propstat[], void *rock);
212 static int proppatch_calcompset(xmlNodePtr prop, unsigned set,
213 struct proppatch_ctx *pctx,
214 struct propstat propstat[], void *rock);
215 static int propfind_suppcaldata(const xmlChar *name, xmlNsPtr ns,
216 struct propfind_ctx *fctx,
217 xmlNodePtr prop, xmlNodePtr resp,
218 struct propstat propstat[], void *rock);
219 static int propfind_maxsize(const xmlChar *name, xmlNsPtr ns,
220 struct propfind_ctx *fctx,
221 xmlNodePtr prop, xmlNodePtr resp,
222 struct propstat propstat[], void *rock);
223 static int propfind_minmaxdate(const xmlChar *name, xmlNsPtr ns,
224 struct propfind_ctx *fctx,
225 xmlNodePtr prop, xmlNodePtr resp,
226 struct propstat propstat[], void *rock);
227 static int propfind_scheddefault(const xmlChar *name, xmlNsPtr ns,
228 struct propfind_ctx *fctx,
229 xmlNodePtr prop, xmlNodePtr resp,
230 struct propstat propstat[], void *rock);
231 static int proppatch_scheddefault(xmlNodePtr prop, unsigned set,
232 struct proppatch_ctx *pctx,
233 struct propstat propstat[],
234 void *rock __attribute__((unused)));
235 static int propfind_schedtag(const xmlChar *name, xmlNsPtr ns,
236 struct propfind_ctx *fctx,
237 xmlNodePtr prop, xmlNodePtr resp,
238 struct propstat propstat[], void *rock);
239 static int proppatch_caltransp(xmlNodePtr prop, unsigned set,
240 struct proppatch_ctx *pctx,
241 struct propstat propstat[], void *rock);
242 static int propfind_timezone(const xmlChar *name, xmlNsPtr ns,
243 struct propfind_ctx *fctx,
244 xmlNodePtr prop, xmlNodePtr resp,
245 struct propstat propstat[], void *rock);
246 static int proppatch_timezone(xmlNodePtr prop, unsigned set,
247 struct proppatch_ctx *pctx,
248 struct propstat propstat[], void *rock);
249 static int propfind_availability(const xmlChar *name, xmlNsPtr ns,
250 struct propfind_ctx *fctx,
251 xmlNodePtr prop, xmlNodePtr resp,
252 struct propstat propstat[], void *rock);
253 static int proppatch_availability(xmlNodePtr prop, unsigned set,
254 struct proppatch_ctx *pctx,
255 struct propstat propstat[], void *rock);
256 static int propfind_tzservset(const xmlChar *name, xmlNsPtr ns,
257 struct propfind_ctx *fctx,
258 xmlNodePtr prop, xmlNodePtr resp,
259 struct propstat propstat[], void *rock);
260 static int propfind_tzid(const xmlChar *name, xmlNsPtr ns,
261 struct propfind_ctx *fctx,
262 xmlNodePtr prop, xmlNodePtr resp,
263 struct propstat propstat[], void *rock);
264 static int proppatch_tzid(xmlNodePtr prop, unsigned set,
265 struct proppatch_ctx *pctx,
266 struct propstat propstat[], void *rock);
267 static int propfind_rscaleset(const xmlChar *name, xmlNsPtr ns,
268 struct propfind_ctx *fctx,
269 xmlNodePtr prop, xmlNodePtr resp,
270 struct propstat propstat[], void *rock);
271 static int propfind_sharingmodes(const xmlChar *name, xmlNsPtr ns,
272 struct propfind_ctx *fctx,
273 xmlNodePtr prop, xmlNodePtr resp,
274 struct propstat propstat[], void *rock);
275
276 static void strip_vtimezones(icalcomponent *ical);
277
278 static int report_cal_query(struct transaction_t *txn,
279 struct meth_params *rparams,
280 xmlNodePtr inroot, struct propfind_ctx *fctx);
281 static int report_fb_query(struct transaction_t *txn,
282 struct meth_params *rparams,
283 xmlNodePtr inroot, struct propfind_ctx *fctx);
284
285 static const char *begin_icalendar(struct buf *buf, struct mailbox *mailbox,
286 const char *prodid, const char *name,
287 const char *desc, const char *color);
288 static void end_icalendar(struct buf *buf);
289
290 #define ICALENDAR_CONTENT_TYPE "text/calendar; charset=utf-8"
291
292 static struct mime_type_t caldav_mime_types[] = {
293 /* First item MUST be the default type and storage format */
294 { ICALENDAR_CONTENT_TYPE, "2.0", "ics",
295 (struct buf* (*)(void *)) &my_icalcomponent_as_ical_string,
296 (void * (*)(const struct buf*)) &ical_string_as_icalcomponent,
297 (void (*)(void *)) &icalcomponent_free, &begin_icalendar, &end_icalendar
298 },
299 { "application/calendar+xml; charset=utf-8", NULL, "xcs",
300 (struct buf* (*)(void *)) &icalcomponent_as_xcal_string,
301 (void * (*)(const struct buf*)) &xcal_string_as_icalcomponent,
302 NULL, &begin_xcal, &end_xcal
303 },
304 { "application/calendar+json; charset=utf-8", NULL, "jcs",
305 (struct buf* (*)(void *)) &icalcomponent_as_jcal_string,
306 (void * (*)(const struct buf*)) &jcal_string_as_icalcomponent,
307 NULL, &begin_jcal, &end_jcal
308 },
309 #ifdef WITH_JMAP
310 { "application/event+json; charset=utf-8", NULL, "jevent",
311 (struct buf* (*)(void *)) &icalcomponent_as_jevent_string,
312 (void * (*)(const struct buf*)) &jevent_string_as_icalcomponent,
313 NULL, NULL, NULL
314 },
315 #endif
316 { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
317 };
318
319 static struct patch_doc_t caldav_patch_docs[] = {
320 { ICALENDAR_CONTENT_TYPE "; component=VPATCH; optinfo=\"PATCH-VERSION:1\"",
321 &caldav_patch },
322 { NULL, &caldav_patch }
323 };
324
325 /* Array of supported REPORTs */
326 static const struct report_type_t caldav_reports[] = {
327
328 /* WebDAV Versioning (RFC 3253) REPORTs */
329 { "expand-property", NS_DAV, "multistatus", &report_expand_prop,
330 DACL_READ, 0 },
331
332 /* WebDAV ACL (RFC 3744) REPORTs */
333 { "acl-principal-prop-set", NS_DAV, "multistatus", &report_acl_prin_prop,
334 DACL_ADMIN, REPORT_NEED_MBOX | REPORT_NEED_PROPS | REPORT_DEPTH_ZERO },
335
336 /* WebDAV Sync (RFC 6578) REPORTs */
337 { "sync-collection", NS_DAV, "multistatus", &report_sync_col,
338 DACL_READ, REPORT_NEED_MBOX | REPORT_NEED_PROPS },
339
340 /* CalDAV (RFC 4791) REPORTs */
341 { "calendar-query", NS_CALDAV, "multistatus", &report_cal_query,
342 DACL_READ, REPORT_NEED_MBOX | REPORT_ALLOW_PROPS },
343 { "calendar-multiget", NS_CALDAV, "multistatus", &report_multiget,
344 DACL_READ, REPORT_NEED_MBOX | REPORT_ALLOW_PROPS },
345 { "free-busy-query", NS_CALDAV, NULL, &report_fb_query,
346 DACL_READFB, REPORT_NEED_MBOX },
347
348 { NULL, 0, NULL, NULL, 0, 0 }
349 };
350
351 /* Array of known "live" properties */
352 static const struct prop_entry caldav_props[] = {
353
354 /* WebDAV (RFC 4918) properties */
355 { "creationdate", NS_DAV,
356 PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
357 propfind_creationdate, NULL, NULL },
358 { "displayname", NS_DAV,
359 PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE | PROP_PERUSER,
360 propfind_collectionname, proppatch_todb, NULL },
361 { "getcontentlanguage", NS_DAV,
362 PROP_ALLPROP | PROP_RESOURCE,
363 propfind_fromhdr, NULL, "Content-Language" },
364 { "getcontentlength", NS_DAV,
365 PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
366 propfind_getlength, NULL, NULL },
367 { "getcontenttype", NS_DAV,
368 PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
369 propfind_getcontenttype, NULL, "Content-Type" },
370 { "getetag", NS_DAV,
371 PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
372 propfind_getetag, NULL, NULL },
373 { "getlastmodified", NS_DAV,
374 PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
375 propfind_getlastmod, NULL, NULL },
376 { "lockdiscovery", NS_DAV,
377 PROP_ALLPROP | PROP_RESOURCE,
378 propfind_lockdisc, NULL, NULL },
379 { "resourcetype", NS_DAV,
380 PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE | PROP_PRESCREEN,
381 propfind_restype, proppatch_restype, "calendar" },
382 { "supportedlock", NS_DAV,
383 PROP_ALLPROP | PROP_RESOURCE,
384 propfind_suplock, NULL, NULL },
385
386 /* WebDAV Versioning (RFC 3253) properties */
387 { "supported-report-set", NS_DAV,
388 PROP_COLLECTION | PROP_PRESCREEN,
389 propfind_reportset, NULL, (void *) caldav_reports },
390 { "supported-method-set", NS_DAV,
391 PROP_COLLECTION | PROP_RESOURCE,
392 propfind_methodset, NULL, (void *) &caldav_allow_cb },
393
394 /* WebDAV ACL (RFC 3744) properties */
395 { "owner", NS_DAV,
396 PROP_COLLECTION | PROP_RESOURCE,
397 propfind_owner, NULL, NULL },
398 { "group", NS_DAV,
399 PROP_COLLECTION | PROP_RESOURCE,
400 NULL, NULL, NULL },
401 { "supported-privilege-set", NS_DAV,
402 PROP_COLLECTION | PROP_RESOURCE | PROP_PRESCREEN,
403 propfind_supprivset, NULL, NULL },
404 { "current-user-privilege-set", NS_DAV,
405 PROP_COLLECTION | PROP_RESOURCE | PROP_PRESCREEN,
406 propfind_curprivset, NULL, NULL },
407 { "acl", NS_DAV, PROP_COLLECTION | PROP_RESOURCE | PROP_PRESCREEN,
408 propfind_acl, NULL, NULL },
409 { "acl-restrictions", NS_DAV,
410 PROP_COLLECTION | PROP_RESOURCE,
411 propfind_aclrestrict, NULL, NULL },
412 { "inherited-acl-set", NS_DAV,
413 PROP_COLLECTION | PROP_RESOURCE,
414 NULL, NULL, NULL },
415 { "principal-collection-set", NS_DAV,
416 PROP_COLLECTION | PROP_RESOURCE,
417 propfind_princolset, NULL, NULL },
418
419 /* WebDAV Quota (RFC 4331) properties */
420 { "quota-available-bytes", NS_DAV,
421 PROP_COLLECTION,
422 propfind_quota, NULL, NULL },
423 { "quota-used-bytes", NS_DAV,
424 PROP_COLLECTION,
425 propfind_quota, NULL, NULL },
426
427 /* WebDAV Current Principal (RFC 5397) properties */
428 { "current-user-principal", NS_DAV,
429 PROP_COLLECTION | PROP_RESOURCE,
430 propfind_curprin, NULL, NULL },
431
432 /* WebDAV POST (RFC 5995) properties */
433 { "add-member", NS_DAV,
434 PROP_COLLECTION,
435 propfind_addmember, NULL, NULL },
436
437 /* WebDAV Sync (RFC 6578) properties */
438 { "sync-token", NS_DAV,
439 PROP_COLLECTION,
440 propfind_sync_token, NULL, SYNC_TOKEN_URL_SCHEME },
441
442 /* WebDAV Sharing (draft-pot-webdav-resource-sharing) properties */
443 { "share-access", NS_DAV,
444 PROP_COLLECTION,
445 propfind_shareaccess, NULL, NULL },
446 { "invite", NS_DAV,
447 PROP_COLLECTION,
448 propfind_invite, NULL, NULL },
449 { "sharer-resource-uri", NS_DAV,
450 PROP_COLLECTION,
451 propfind_sharedurl, NULL, NULL },
452
453 /* Backwards compatibility with Apple calendar sharing clients */
454 { "invite", NS_CS,
455 PROP_COLLECTION,
456 propfind_invite, NULL, "calendarserver-sharing" },
457 { "allowed-sharing-modes", NS_CS,
458 PROP_COLLECTION,
459 propfind_sharingmodes, NULL, NULL },
460 { "shared-url", NS_CS,
461 PROP_COLLECTION,
462 propfind_sharedurl, NULL, "calendarserver-sharing" },
463
464 /* CalDAV (RFC 4791) properties */
465 { "calendar-data", NS_CALDAV,
466 PROP_RESOURCE | PROP_PRESCREEN | PROP_CLEANUP,
467 propfind_caldata, NULL, (void *) CALDAV_SUPP_DATA },
468 { "schedule-user-address", NS_CYRUS,
469 PROP_RESOURCE,
470 propfind_scheduser, NULL, NULL },
471 { "calendar-description", NS_CALDAV,
472 PROP_COLLECTION | PROP_PERUSER,
473 propfind_fromdb, proppatch_todb, NULL },
474 { "calendar-timezone", NS_CALDAV,
475 PROP_COLLECTION | PROP_PERUSER | PROP_PRESCREEN,
476 propfind_timezone, proppatch_timezone, (void *) CALDAV_SUPP_DATA },
477 { "supported-calendar-component-set", NS_CALDAV,
478 PROP_COLLECTION,
479 propfind_calcompset, proppatch_calcompset, NULL },
480 { "supported-calendar-data", NS_CALDAV,
481 PROP_COLLECTION,
482 propfind_suppcaldata, NULL, NULL },
483 { "max-resource-size", NS_CALDAV,
484 PROP_COLLECTION,
485 propfind_maxsize, NULL, NULL },
486 { "min-date-time", NS_CALDAV,
487 PROP_COLLECTION,
488 propfind_minmaxdate, NULL, &caldav_epoch },
489 { "max-date-time", NS_CALDAV,
490 PROP_COLLECTION,
491 propfind_minmaxdate, NULL, &caldav_eternity },
492 { "max-instances", NS_CALDAV,
493 PROP_COLLECTION,
494 NULL, NULL, NULL },
495 { "max-attendees-per-instance", NS_CALDAV,
496 PROP_COLLECTION,
497 NULL, NULL, NULL },
498
499 /* CalDAV Scheduling (RFC 6638) properties */
500 { "schedule-tag", NS_CALDAV,
501 PROP_RESOURCE,
502 propfind_schedtag, NULL, NULL },
503 { "schedule-default-calendar-URL", NS_CALDAV,
504 PROP_COLLECTION,
505 propfind_scheddefault, proppatch_scheddefault, NULL },
506 { "schedule-calendar-transp", NS_CALDAV,
507 PROP_COLLECTION | PROP_PERUSER,
508 propfind_fromdb, proppatch_caltransp, NULL },
509
510 /* CalDAV Sharing (draft-pot-caldav-sharing) properties */
511 { "calendar-user-address-set", NS_CALDAV,
512 PROP_COLLECTION | PROP_PERUSER,
513 propfind_caluseraddr, proppatch_caluseraddr, NULL },
514
515 /* Calendar Availability (RFC 7953) properties */
516 { "calendar-availability", NS_CALDAV,
517 PROP_COLLECTION | PROP_PERUSER | PROP_PRESCREEN,
518 propfind_availability, proppatch_availability, (void *) CALDAV_SUPP_DATA },
519
520 /* Backwards compatibility with Apple VAVAILABILITY clients */
521 { "calendar-availability", NS_CS,
522 PROP_COLLECTION | PROP_PERUSER | PROP_PRESCREEN,
523 propfind_availability, proppatch_availability, (void *) CALDAV_SUPP_DATA },
524
525 /* Time Zones by Reference (RFC 7809) properties */
526 { "timezone-service-set", NS_CALDAV,
527 PROP_COLLECTION,
528 propfind_tzservset, NULL, NULL },
529 { "calendar-timezone-id", NS_CALDAV,
530 PROP_COLLECTION | PROP_PERUSER,
531 propfind_tzid, proppatch_tzid, NULL },
532
533 /* RSCALE (RFC 7529) properties */
534 { "supported-rscale-set", NS_CALDAV,
535 PROP_COLLECTION,
536 propfind_rscaleset, NULL, NULL },
537
538 /* CalDAV Extensions (draft-daboo-caldav-extensions) properties */
539 { "supported-calendar-component-sets", NS_CALDAV,
540 PROP_COLLECTION,
541 propfind_calcompset, NULL, NULL },
542
543 /* Apple Calendar Server properties */
544 { "getctag", NS_CS,
545 PROP_ALLPROP | PROP_COLLECTION,
546 propfind_sync_token, NULL, "" },
547
548 /* Apple Mobile Me properties */
549 { "bulk-requests", NS_MECOM,
550 PROP_COLLECTION,
551 propfind_bulkrequests, NULL, NULL },
552
553 /* Apple Push Notifications Service properties */
554 { "push-transports", NS_CS,
555 PROP_COLLECTION | PROP_PRESCREEN,
556 propfind_push_transports, NULL, (void *) MBTYPE_CALENDAR },
557 { "pushkey", NS_CS,
558 PROP_COLLECTION,
559 propfind_pushkey, NULL, NULL },
560
561 { NULL, 0, 0, NULL, NULL, NULL }
562 };
563
564
565 static struct meth_params caldav_params = {
566 caldav_mime_types,
567 &caldav_parse_path,
568 &caldav_get_validators,
569 &caldav_get_modseq,
570 &caldav_check_precond,
571 { (db_open_proc_t) &caldav_open_mailbox,
572 (db_close_proc_t) &caldav_close,
573 (db_proc_t) &caldav_begin,
574 (db_proc_t) &caldav_commit,
575 (db_proc_t) &caldav_abort,
576 (db_lookup_proc_t) &caldav_lookup_resource,
577 (db_imapuid_proc_t) &caldav_lookup_imapuid,
578 (db_foreach_proc_t) &caldav_foreach,
579 (db_updates_proc_t) &caldav_get_updates,
580 (db_write_proc_t) &caldav_write,
581 (db_delete_proc_t) &caldav_delete },
582 &caldav_acl,
583 { CALDAV_UID_CONFLICT, &caldav_copy },
584 &caldav_delete_cal,
585 &caldav_get,
586 { CALDAV_LOCATION_OK, MBTYPE_CALENDAR, &caldav_mkcol },
587 caldav_patch_docs,
588 { POST_ADDMEMBER | POST_SHARE, &caldav_post,
589 { NS_CALDAV, "calendar-data", &caldav_import } },
590 { CALDAV_SUPP_DATA, &caldav_put },
591 { 0, caldav_props }, /* Allow infinite depth */
592 caldav_reports
593 };
594
595
596 /* Namespace for CalDAV collections */
597 struct namespace_t namespace_calendar = {
598 URL_NS_CALENDAR, 0, "calendar", "/dav/calendars", "/.well-known/caldav",
599 http_allow_noauth_get, /*authschemes*/0,
600 MBTYPE_CALENDAR,
601 (ALLOW_READ | ALLOW_POST | ALLOW_WRITE | ALLOW_DELETE |
602 ALLOW_PATCH | ALLOW_USERDATA |
603 ALLOW_CAL_AVAIL |
604 ALLOW_DAV | ALLOW_PROPPATCH | ALLOW_MKCOL | ALLOW_ACL | ALLOW_CAL ),
605 &my_caldav_init, &my_caldav_auth, my_caldav_reset, &my_caldav_shutdown,
606 &dav_premethod, /*bearer*/NULL,
607 {
608 { &meth_acl, &caldav_params }, /* ACL */
609 { NULL, NULL }, /* BIND */
610 { NULL, NULL }, /* CONNECT */
611 { &meth_copy_move, &caldav_params }, /* COPY */
612 { &meth_delete, &caldav_params }, /* DELETE */
613 { &meth_get_head_cal, &caldav_params }, /* GET */
614 { &meth_get_head_cal, &caldav_params }, /* HEAD */
615 { &meth_lock, &caldav_params }, /* LOCK */
616 { &meth_mkcol, &caldav_params }, /* MKCALENDAR */
617 { &meth_mkcol, &caldav_params }, /* MKCOL */
618 { &meth_copy_move, &caldav_params }, /* MOVE */
619 { &meth_options_cal, &caldav_params }, /* OPTIONS */
620 { &meth_patch, &caldav_params }, /* PATCH */
621 { &meth_post, &caldav_params }, /* POST */
622 { &meth_propfind, &caldav_params }, /* PROPFIND */
623 { &meth_proppatch, &caldav_params }, /* PROPPATCH */
624 { &meth_put, &caldav_params }, /* PUT */
625 { &meth_report, &caldav_params }, /* REPORT */
626 { &meth_trace, &caldav_parse_path }, /* TRACE */
627 { NULL, NULL }, /* UNBIND */
628 { &meth_unlock, &caldav_params } /* UNLOCK */
629 }
630 };
631
632
633 /* Namespace for Freebusy Read URL */
634 struct namespace_t namespace_freebusy = {
635 URL_NS_FREEBUSY, 0, "freebusy", "/freebusy", NULL,
636 http_allow_noauth_get, /*authschemes*/0,
637 MBTYPE_CALENDAR,
638 ALLOW_READ,
639 NULL, NULL, NULL, NULL, NULL, NULL,
640 {
641 { NULL, NULL }, /* ACL */
642 { NULL, NULL }, /* BIND */
643 { NULL, NULL }, /* CONNECT */
644 { NULL, NULL }, /* COPY */
645 { NULL, NULL }, /* DELETE */
646 { &meth_get_head_fb, &caldav_params }, /* GET */
647 { &meth_get_head_fb, &caldav_params }, /* HEAD */
648 { NULL, NULL }, /* LOCK */
649 { NULL, NULL }, /* MKCALENDAR */
650 { NULL, NULL }, /* MKCOL */
651 { NULL, NULL }, /* MOVE */
652 { &meth_options, &caldav_parse_path }, /* OPTIONS */
653 { NULL, NULL }, /* PATCH */
654 { NULL, NULL }, /* POST */
655 { NULL, NULL }, /* PROPFIND */
656 { NULL, NULL }, /* PROPPATCH */
657 { NULL, NULL }, /* PUT */
658 { NULL, NULL }, /* REPORT */
659 { &meth_trace, &caldav_parse_path }, /* TRACE */
660 { NULL, NULL }, /* UNBIND */
661 { NULL, NULL } /* UNLOCK */
662 }
663 };
664
665
666 static const struct cal_comp_t {
667 const char *name;
668 unsigned long type;
669 } cal_comps[] = {
670 { "VEVENT", CAL_COMP_VEVENT },
671 { "VTODO", CAL_COMP_VTODO },
672 { "VJOURNAL", CAL_COMP_VJOURNAL },
673 { "VFREEBUSY", CAL_COMP_VFREEBUSY },
674 { "VAVAILABILITY", CAL_COMP_VAVAILABILITY },
675 { "VPOLL", CAL_COMP_VPOLL },
676 // { "VTIMEZONE", CAL_COMP_VTIMEZONE },
677 // { "VALARM", CAL_COMP_VALARM },
678 { NULL, 0 }
679 };
680
681
my_caldav_init(struct buf * serverinfo)682 static void my_caldav_init(struct buf *serverinfo)
683 {
684 const char *domains;
685 char *domain;
686 tok_t tok;
687
688 buf_printf(serverinfo, " LibiCal/%s", ICAL_VERSION);
689 #ifdef HAVE_RSCALE
690 if ((rscale_calendars = icalrecurrencetype_rscale_supported_calendars())) {
691 icalarray_sort(rscale_calendars, &rscale_cmp);
692
693 buf_printf(serverinfo, " ICU4C/%s", U_ICU_VERSION);
694 }
695 #endif
696
697 namespace_calendar.enabled =
698 config_httpmodules & IMAP_ENUM_HTTPMODULES_CALDAV;
699
700 if (!namespace_calendar.enabled) return;
701
702 if (!config_getstring(IMAPOPT_CALENDARPREFIX)) {
703 fatal("Required 'calendarprefix' option is not set", EX_CONFIG);
704 }
705
706 config_allowsched = config_getenum(IMAPOPT_CALDAV_ALLOWSCHEDULING);
707 if (config_allowsched) {
708 namespace_calendar.allow |= ALLOW_CAL_SCHED;
709
710 /* Always treat unknown parameters as IANA */
711 ical_set_unknown_token_handling_setting(ICAL_ASSUME_IANA_TOKEN);
712 }
713
714 if (config_getswitch(IMAPOPT_CALDAV_ALLOWATTACH))
715 namespace_calendar.allow |= ALLOW_CAL_ATTACH;
716
717 if (namespace_tzdist.enabled) {
718 namespace_calendar.allow |= ALLOW_CAL_NOTZ;
719 }
720
721 caldav_init();
722 webdav_init();
723
724 namespace_principal.enabled = 1;
725 /* Apple clients check principal resources for these DAV tokens */
726 namespace_principal.allow |= namespace_calendar.allow &
727 (ALLOW_CAL | ALLOW_CAL_AVAIL | ALLOW_CAL_SCHED | ALLOW_CAL_ATTACH);
728
729 namespace_freebusy.enabled =
730 config_httpmodules & IMAP_ENUM_HTTPMODULES_FREEBUSY;
731
732 compile_time = calc_compile_time(__TIME__, __DATE__);
733
734 buf_printf(&ical_prodid_buf,
735 "-//CyrusIMAP.org/Cyrus %s//EN", CYRUS_VERSION);
736 ical_prodid = buf_cstring(&ical_prodid_buf);
737
738 /* Create an array of calendar-user-address-set domains */
739 domains = config_getstring(IMAPOPT_CALENDAR_USER_ADDRESS_SET);
740 if (!domains) domains = config_defdomain;
741 if (!domains) domains = config_servername;
742
743 tok_init(&tok, domains, " \t", TOK_TRIMLEFT|TOK_TRIMRIGHT);
744 while ((domain = tok_next(&tok))) appendstrlist(&cua_domains, domain);
745 tok_fini(&tok);
746
747 utc_zone = icaltimezone_get_utc_timezone();
748 }
749
_create_mailbox(const char * userid,const char * mailboxname,int type,unsigned long comp_types,int useracl,int anyoneacl,const char * displayname,struct mboxlock ** namespacelockp)750 static int _create_mailbox(const char *userid, const char *mailboxname,
751 int type, unsigned long comp_types,
752 int useracl, int anyoneacl, const char *displayname,
753 struct mboxlock **namespacelockp)
754 {
755 int r = 0;
756 char rights[100];
757 struct mailbox *mailbox = NULL;
758
759 r = mboxlist_lookup(mailboxname, NULL, NULL);
760 if (!r) return 0;
761 if (r != IMAP_MAILBOX_NONEXISTENT) return r;
762
763 if (!*namespacelockp) {
764 *namespacelockp = mboxname_usernamespacelock(mailboxname);
765 // maybe we lost the race on this one
766 r = mboxlist_lookup(mailboxname, NULL, NULL);
767 if (!r) return 0;
768 if (r != IMAP_MAILBOX_NONEXISTENT) return r;
769 }
770
771 /* Create locally */
772 r = mboxlist_createmailbox(mailboxname, type,
773 NULL, 0,
774 userid, httpd_authstate,
775 0, 0, 0, 0, displayname ? &mailbox : NULL);
776 if (!r && displayname) {
777 annotate_state_t *astate = NULL;
778
779 r = mailbox_get_annotate_state(mailbox, 0, &astate);
780 if (!r) {
781 const char *disp_annot = DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
782 const char *comp_annot =
783 DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
784 struct buf value = BUF_INITIALIZER;
785
786 buf_init_ro_cstr(&value, displayname);
787 r = annotate_state_writemask(astate, disp_annot, userid, &value);
788 if (!r && comp_types) {
789 buf_reset(&value);
790 buf_printf(&value, "%lu", comp_types);
791 r = annotate_state_writemask(astate, comp_annot, userid, &value);
792 }
793 buf_free(&value);
794 }
795
796 mailbox_close(&mailbox);
797 }
798 if (!r && useracl) {
799 cyrus_acl_masktostr(useracl, rights);
800 r = mboxlist_setacl(&httpd_namespace, mailboxname, userid, rights,
801 1, httpd_userid, httpd_authstate);
802 }
803 if (!r && anyoneacl) {
804 cyrus_acl_masktostr(anyoneacl, rights);
805 r = mboxlist_setacl(&httpd_namespace, mailboxname, "anyone", rights,
806 1, httpd_userid, httpd_authstate);
807 }
808
809 if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
810 mailboxname, error_message(r));
811 return r;
812 }
813
config_types_to_caldav_types(void)814 unsigned long config_types_to_caldav_types(void)
815 {
816 unsigned long config_types =
817 config_getbitfield(IMAPOPT_CALENDAR_COMPONENT_SET);
818 unsigned long types = 0;
819
820 if (config_types & IMAP_ENUM_CALENDAR_COMPONENT_SET_VEVENT)
821 types |= CAL_COMP_VEVENT;
822 if (config_types & IMAP_ENUM_CALENDAR_COMPONENT_SET_VTODO)
823 types |= CAL_COMP_VTODO;
824 if (config_types & IMAP_ENUM_CALENDAR_COMPONENT_SET_VJOURNAL)
825 types |= CAL_COMP_VJOURNAL;
826 if (config_types & IMAP_ENUM_CALENDAR_COMPONENT_SET_VFREEBUSY)
827 types |= CAL_COMP_VFREEBUSY;
828 if (config_types & IMAP_ENUM_CALENDAR_COMPONENT_SET_VAVAILABILITY)
829 types |= CAL_COMP_VAVAILABILITY;
830 #ifdef VPOLL
831 if (config_types & IMAP_ENUM_CALENDAR_COMPONENT_SET_VPOLL)
832 types |= CAL_COMP_VPOLL;
833 #endif
834
835 return types;
836 }
837
caldav_create_defaultcalendars(const char * userid)838 int caldav_create_defaultcalendars(const char *userid)
839 {
840 int r;
841 char *mailboxname;
842 struct mboxlock *namespacelock = NULL;
843
844 /* calendar-home-set */
845 mailboxname = caldav_mboxname(userid, NULL);
846 r = mboxlist_lookup(mailboxname, NULL, NULL);
847 if (r == IMAP_MAILBOX_NONEXISTENT) {
848 /* Find location of INBOX */
849 char *inboxname = mboxname_user_mbox(userid, NULL);
850 mbentry_t *mbentry = NULL;
851
852 r = proxy_mlookup(inboxname, &mbentry, NULL, NULL);
853 free(inboxname);
854 if (r == IMAP_MAILBOX_NONEXISTENT) r = IMAP_INVALID_USER;
855 if (!r && mbentry->server) {
856 proxy_findserver(mbentry->server, &http_protocol, httpd_userid,
857 &backend_cached, NULL, NULL, httpd_in);
858 mboxlist_entry_free(&mbentry);
859 free(mailboxname);
860 return r;
861 }
862 mboxlist_entry_free(&mbentry);
863
864 if (!r) r = _create_mailbox(userid, mailboxname, MBTYPE_CALENDAR, 0,
865 ACL_ALL | DACL_READFB, DACL_READFB, NULL,
866 &namespacelock);
867 }
868
869 free(mailboxname);
870 if (r) goto done;
871
872 if (config_getswitch(IMAPOPT_CALDAV_CREATE_DEFAULT)) {
873 /* Default calendar */
874 unsigned long comp_types = config_types_to_caldav_types();
875
876 mailboxname = caldav_mboxname(userid, SCHED_DEFAULT);
877 r = _create_mailbox(userid, mailboxname, MBTYPE_CALENDAR, comp_types,
878 ACL_ALL | DACL_READFB, DACL_READFB,
879 config_getstring(IMAPOPT_CALENDAR_DEFAULT_DISPLAYNAME),
880 &namespacelock);
881 free(mailboxname);
882 if (r) goto done;
883 }
884
885 if (config_getswitch(IMAPOPT_CALDAV_CREATE_SCHED) &&
886 namespace_calendar.allow & ALLOW_CAL_SCHED) {
887 /* Scheduling Inbox */
888 mailboxname = caldav_mboxname(userid, SCHED_INBOX);
889 r = _create_mailbox(userid, mailboxname, MBTYPE_CALENDAR, 0,
890 ACL_ALL | DACL_SCHED, DACL_SCHED, NULL,
891 &namespacelock);
892 free(mailboxname);
893 if (r) goto done;
894
895 /* Scheduling Outbox */
896 mailboxname = caldav_mboxname(userid, SCHED_OUTBOX);
897 r = _create_mailbox(userid, mailboxname, MBTYPE_CALENDAR, 0,
898 ACL_ALL | DACL_SCHED, 0, NULL,
899 &namespacelock);
900 free(mailboxname);
901 if (r) goto done;
902 }
903
904 if (config_getswitch(IMAPOPT_CALDAV_CREATE_ATTACH) &&
905 namespace_calendar.allow & ALLOW_CAL_ATTACH) {
906 /* Managed Attachment Collection */
907 mailboxname = caldav_mboxname(userid, MANAGED_ATTACH);
908 r = _create_mailbox(userid, mailboxname, MBTYPE_COLLECTION, 0,
909 ACL_ALL, ACL_READ, NULL,
910 &namespacelock);
911 free(mailboxname);
912 if (r) goto done;
913 }
914
915 done:
916 if (namespacelock) mboxname_release(&namespacelock);
917 return r;
918 }
919
my_caldav_auth(const char * userid)920 static int my_caldav_auth(const char *userid)
921 {
922 if (httpd_userisadmin ||
923 global_authisa(httpd_authstate, IMAPOPT_PROXYSERVERS)) {
924 /* admin or proxy from frontend - won't have DAV database */
925 return 0;
926 }
927 if (config_mupdate_server && !config_getstring(IMAPOPT_PROXYSERVERS)) {
928 /* proxy-only server - won't have DAV database */
929 return 0;
930 }
931 else {
932 /* Open CalDAV DB for 'userid' */
933 my_caldav_reset();
934 auth_caldavdb = caldav_open_userid(userid);
935 if (!auth_caldavdb) {
936 syslog(LOG_ERR, "Unable to open CalDAV DB for userid %s", userid);
937 return HTTP_UNAVAILABLE;
938 }
939 }
940
941 /* Auto-provision calendars for 'userid' */
942 int r = caldav_create_defaultcalendars(userid);
943 if (r) {
944 syslog(LOG_ERR, "could not autoprovision calendars for userid %s: %s",
945 userid, error_message(r));
946 return HTTP_SERVER_ERROR;
947 }
948
949 return 0;
950 }
951
my_caldav_reset(void)952 static void my_caldav_reset(void)
953 {
954 if (auth_caldavdb) caldav_close(auth_caldavdb);
955 auth_caldavdb = NULL;
956 }
957
my_caldav_shutdown(void)958 static void my_caldav_shutdown(void)
959 {
960 if (rscale_calendars) icalarray_free(rscale_calendars);
961 rscale_calendars = NULL;
962
963 buf_free(&ical_prodid_buf);
964
965 freestrlist(cua_domains);
966 cua_domains = NULL;
967
968 my_caldav_reset();
969 webdav_done();
970 caldav_done();
971 }
972
973
974 /* Determine allowed methods in CalDAV namespace */
caldav_allow_cb(struct request_target_t * tgt)975 static unsigned long caldav_allow_cb(struct request_target_t *tgt)
976 {
977 unsigned long allow = calcarddav_allow_cb(tgt);
978
979 if (!tgt->collection) {
980 if (tgt->userid) {
981 /* Allow POST to cal-home-set (share reply) */
982 allow |= ALLOW_POST;
983 }
984 }
985 else if (!strncmp(tgt->collection, MANAGED_ATTACH, strlen(MANAGED_ATTACH))) {
986 /* Read-only non-calendar collection */
987 allow = ALLOW_READ;
988 }
989 else if (!strncmp(tgt->collection, SCHED_INBOX, strlen(SCHED_INBOX))) {
990 /* Can only read and DELETE resources from this collection */
991 allow &= ALLOW_READ_MASK;
992
993 if (tgt->resource) allow |= ALLOW_DELETE;
994 }
995 else if (!strncmp(tgt->collection, SCHED_OUTBOX, strlen(SCHED_OUTBOX))){
996 /* Can only POST to this collection (free/busy request) */
997 allow &= ALLOW_READ_MASK;
998
999 if (!tgt->resource) allow |= ALLOW_POST;
1000 }
1001 else if (tgt->resource) {
1002 /* Resource in regular calendar collection (POST for managed attach) */
1003 allow |= ALLOW_POST;
1004 }
1005
1006 return allow;
1007 }
1008
1009
1010 /* Parse request-target path in CalDAV namespace */
caldav_parse_path(const char * path,struct request_target_t * tgt,const char ** resultstr)1011 static int caldav_parse_path(const char *path, struct request_target_t *tgt,
1012 const char **resultstr)
1013 {
1014 int r;
1015
1016 r = calcarddav_parse_path(path, tgt,
1017 config_getstring(IMAPOPT_CALENDARPREFIX),
1018 resultstr);
1019 if (r) return r;
1020
1021 /* Set proper Allow bits based on collection */
1022 if (tgt->namespace && tgt->namespace->id == URL_NS_FREEBUSY) {
1023 /* Read-only collections */
1024 tgt->allow = ALLOW_READ;
1025 }
1026 else if (!tgt->collection) {
1027 if (tgt->userid) {
1028 /* Allow POST to cal-home-set (share reply) */
1029 tgt->allow |= ALLOW_POST;
1030 }
1031 }
1032 else if (!strncmp(tgt->collection, MANAGED_ATTACH, strlen(MANAGED_ATTACH))) {
1033 /* Read-only non-calendar collection */
1034 tgt->allow = ALLOW_READ;
1035
1036 tgt->flags = TGT_MANAGED_ATTACH;
1037 }
1038 else if (!strncmp(tgt->collection, SCHED_INBOX, strlen(SCHED_INBOX))) {
1039 /* Can only read and DELETE resources from this collection */
1040 tgt->allow &= ALLOW_READ_MASK;
1041
1042 if (tgt->resource) tgt->allow |= ALLOW_DELETE;
1043
1044 tgt->flags = TGT_SCHED_INBOX;
1045 }
1046 else if (!strncmp(tgt->collection, SCHED_OUTBOX, strlen(SCHED_OUTBOX))){
1047 /* Can only POST to this collection (free/busy request) */
1048 tgt->allow &= ALLOW_READ_MASK;
1049
1050 if (!tgt->resource) tgt->allow |= ALLOW_POST;
1051
1052 tgt->flags = TGT_SCHED_OUTBOX;
1053 }
1054 else if (tgt->resource) {
1055 /* Resource in regular calendar collection (POST for managed attach) */
1056 tgt->allow |= (namespace_calendar.allow & ALLOW_PATCH) | ALLOW_POST;
1057 }
1058
1059 return 0;
1060 }
1061
1062
1063 #define STRIP_OWNER_CAL_DATA \
1064 "CALDATA %(VPATCH {248+}\r\n" \
1065 "BEGIN:VPATCH\r\n" \
1066 "VERSION:1\r\n" \
1067 "DTSTAMP:19760401T005545Z\r\n" \
1068 "UID:strip-owner-cal-data\r\n" \
1069 "BEGIN:PATCH\r\n" \
1070 "PATCH-TARGET:/VCALENDAR/ANY\r\n" \
1071 "PATCH-DELETE:/VALARM\r\n" \
1072 "PATCH-DELETE:#TRANSP\r\n" \
1073 "PATCH-DELETE:#X-MOZ-LASTACK\r\n" \
1074 "PATCH-DELETE:#X-MOZ-SNOOZE-TIME\r\n" \
1075 "END:PATCH\r\n" \
1076 "END:VPATCH\r\n)"
1077
1078
caldav_is_personalized(struct mailbox * mailbox,const struct caldav_data * cdata,const char * userid,struct buf * userdata)1079 HIDDEN int caldav_is_personalized(struct mailbox *mailbox,
1080 const struct caldav_data *cdata,
1081 const char *userid,
1082 struct buf *userdata)
1083 {
1084 if (cdata->comp_flags.shared) {
1085 /* Lookup per-user calendar data */
1086 int r = mailbox_get_annotate_state(mailbox, cdata->dav.imap_uid, NULL);
1087
1088 if (!r) {
1089 mbname_t *mbname = NULL;
1090
1091 if (mailbox->i.options & OPT_IMAP_SHAREDSEEN) {
1092 /* No longer using per-user-data - use owner data */
1093 mbname = mbname_from_intname(mailbox->name);
1094 userid = mbname_userid(mbname);
1095 }
1096
1097 r = mailbox_annotation_lookup(mailbox, cdata->dav.imap_uid,
1098 PER_USER_CAL_DATA, userid, userdata);
1099 mbname_free(&mbname);
1100 }
1101
1102 if (!r && buf_len(userdata)) return 1;
1103 buf_free(userdata);
1104 }
1105 else if (!(mailbox->i.options & OPT_IMAP_SHAREDSEEN) &&
1106 !mboxname_userownsmailbox(userid, mailbox->name)) {
1107 buf_init_ro_cstr(userdata, STRIP_OWNER_CAL_DATA);
1108 return 1;
1109 }
1110
1111 return 0;
1112 }
1113
1114
caldav_get_validators(struct mailbox * mailbox,void * data,const char * userid,struct index_record * record,const char ** etag,time_t * lastmod)1115 static int caldav_get_validators(struct mailbox *mailbox, void *data,
1116 const char *userid, struct index_record *record,
1117 const char **etag, time_t *lastmod)
1118 {
1119
1120 const struct caldav_data *cdata = (const struct caldav_data *) data;
1121 struct buf userdata = BUF_INITIALIZER;
1122
1123 int r = dav_get_validators(mailbox, data, userid, record, etag, lastmod);
1124 if (r) return r;
1125
1126 if ((namespace_calendar.allow & ALLOW_USERDATA) &&
1127 cdata->dav.imap_uid && cdata->comp_flags.shared &&
1128 caldav_is_personalized(mailbox, cdata, userid, &userdata)) {
1129 struct dlist *dl;
1130
1131 /* Parse the userdata and fetch the validators */
1132 dlist_parsemap(&dl, 1, 0, buf_base(&userdata), buf_len(&userdata));
1133
1134 if (etag) {
1135 char buf[2*MESSAGE_GUID_SIZE];
1136 struct message_guid *user_guid;
1137
1138 dlist_getguid(dl, "GUID", &user_guid);
1139
1140 /* Per-user ETag is GUID of concatenated GUIDs */
1141 message_guid_export(&record->guid, buf);
1142 message_guid_export(user_guid, buf+MESSAGE_GUID_SIZE);
1143 message_guid_generate(user_guid, buf, sizeof(buf));
1144 *etag = message_guid_encode(user_guid);
1145 }
1146 if (lastmod) {
1147 time_t user_lastmod;
1148
1149 dlist_getdate(dl, "LASTMOD", &user_lastmod);
1150
1151 /* Per-user Last-Modified is latest mod time */
1152 *lastmod = MAX(record->internaldate, user_lastmod);
1153 }
1154
1155 dlist_free(&dl);
1156 buf_free(&userdata);
1157 }
1158
1159 return 0;
1160 }
1161
1162
caldav_get_modseq(struct mailbox * mailbox,void * data,const char * userid)1163 static modseq_t caldav_get_modseq(struct mailbox *mailbox,
1164 void *data, const char *userid)
1165 {
1166 struct caldav_data *cdata = (struct caldav_data *) data;
1167 struct buf userdata = BUF_INITIALIZER;
1168 modseq_t modseq = cdata->dav.modseq;
1169
1170 if ((namespace_calendar.allow & ALLOW_USERDATA) &&
1171 cdata->comp_flags.shared &&
1172 caldav_is_personalized(mailbox, cdata, userid, &userdata)) {
1173 modseq_t shared_modseq = cdata->dav.modseq;
1174 struct dlist *dl;
1175 int r;
1176
1177 /* Parse the userdata and fetch the modseq */
1178 dlist_parsemap(&dl, 1, 0, buf_base(&userdata), buf_len(&userdata));
1179 dlist_getnum64(dl, "MODSEQ", &modseq);
1180 dlist_free(&dl);
1181 buf_free(&userdata);
1182
1183 /* Lookup shared modseq */
1184 r = mailbox_annotation_lookup(mailbox, cdata->dav.imap_uid,
1185 SHARED_MODSEQ, "", &userdata);
1186 if (!r && buf_len(&userdata)) {
1187 sscanf(buf_cstring(&userdata), MODSEQ_FMT, &shared_modseq);
1188 }
1189 buf_free(&userdata);
1190
1191 modseq = MAX(modseq, shared_modseq);
1192 }
1193
1194 return modseq;
1195 }
1196
1197 /* Returns the calendar collection name to use as scheduling default.
1198 * This is just the *last* part of the complete path without trailing
1199 * path separator, e.g. 'Default' */
caldav_scheddefault(const char * userid)1200 HIDDEN char *caldav_scheddefault(const char *userid)
1201 {
1202 const char *annotname =
1203 DAV_ANNOT_NS "<" XML_NS_CALDAV ">schedule-default-calendar";
1204
1205 char *calhomename = caldav_mboxname(userid, NULL);
1206 char *defaultname = NULL;
1207 struct buf attrib = BUF_INITIALIZER;
1208
1209 int r = annotatemore_lookupmask(calhomename, annotname, userid, &attrib);
1210 if (!r && attrib.len) {
1211 defaultname = buf_release(&attrib);
1212 }
1213 else defaultname = xstrdup(SCHED_DEFAULT);
1214
1215 size_t len = strlen(defaultname);
1216 if (defaultname[len-1] == '/') defaultname[len-1] = '\0';
1217
1218 buf_free(&attrib);
1219 free(calhomename);
1220 return defaultname;
1221 }
1222
proppatch_scheddefault(xmlNodePtr prop,unsigned set,struct proppatch_ctx * pctx,struct propstat propstat[],void * rock)1223 static int proppatch_scheddefault(xmlNodePtr prop, unsigned set,
1224 struct proppatch_ctx *pctx,
1225 struct propstat propstat[],
1226 void *rock __attribute__((unused)))
1227 {
1228 /* Only allow PROPPATCH on CALDAV:schedule-inbox-URL */
1229 if ((pctx->txn->req_tgt.flags != TGT_SCHED_INBOX) || !set) {
1230 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID],
1231 prop->name, prop->ns, NULL, DAV_PROT_PROP);
1232 *pctx->ret = HTTP_FORBIDDEN;
1233 return HTTP_FORBIDDEN;
1234 }
1235
1236 /* Validate property value */
1237 int precond = CALDAV_VALID_DEFAULT;
1238 char *href = NULL;
1239 mbname_t *mbname = NULL;
1240
1241 if (prop->children && !prop->children->next) {
1242 xmlNodePtr cur = prop->children;
1243 if (cur->type == XML_ELEMENT_NODE) {
1244 if (!xmlStrcmp(cur->name, BAD_CAST "href")) {
1245 href = (char*) xmlNodeGetContent(cur);
1246 if (href && *href) {
1247 /* Strip trailing '/' character */
1248 size_t len = strlen(href);
1249 if (len > 1 && href[len-1] == '/') {
1250 href[len-1] = '\0';
1251 }
1252 }
1253 }
1254 }
1255 }
1256
1257 if (href) {
1258 buf_reset(&pctx->buf);
1259 if (strchr(httpd_userid, '@') || !httpd_extradomain) {
1260 buf_printf(&pctx->buf, "%s/%s/%s/", namespace_calendar.prefix,
1261 USER_COLLECTION_PREFIX, httpd_userid);
1262 }
1263 else {
1264 buf_printf(&pctx->buf, "%s/%s/%s@%s/", namespace_calendar.prefix,
1265 USER_COLLECTION_PREFIX, httpd_userid, httpd_extradomain);
1266 }
1267 if (!strncmp(href, buf_cstring(&pctx->buf), buf_len(&pctx->buf))) {
1268 const char *cal = href + buf_len(&pctx->buf);
1269 if (cal) {
1270 char *mboxname = caldav_mboxname(httpd_userid, cal);
1271 if (mboxname_iscalendarmailbox(mboxname, 0) &&
1272 mboxname_policycheck(mboxname) == 0) {
1273 mbname = mbname_from_intname(mboxname);
1274 }
1275 free(mboxname);
1276 }
1277 }
1278 }
1279
1280 if (mbname) {
1281 char *calhomename = caldav_mboxname(httpd_userid, NULL);
1282 struct mailbox *calhome = NULL;
1283 struct mailbox *mailbox = NULL;
1284 int r = mailbox_open_iwl(calhomename, &calhome);
1285 if (!r) r = mailbox_open_iwl(mbname_intname(mbname), &mailbox);
1286 if (!r) {
1287 annotate_state_t *astate = NULL;
1288 r = mailbox_get_annotate_state(calhome, 0, &astate);
1289 if (!r) {
1290 const char *annotname =
1291 DAV_ANNOT_NS "<" XML_NS_CALDAV ">schedule-default-calendar";
1292
1293 const strarray_t *boxes = mbname_boxes(mbname);
1294 buf_setcstr(&pctx->buf, strarray_nth(boxes, strarray_size(boxes)-1));
1295 r = annotate_state_writemask(astate, annotname, httpd_userid, &pctx->buf);
1296 }
1297 }
1298 mailbox_close(&mailbox);
1299 mailbox_close(&calhome);
1300 free(calhomename);
1301 if (!r) precond = 0;
1302 }
1303
1304 if (precond) {
1305 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID],
1306 prop->name, prop->ns, NULL, precond);
1307 }
1308 else {
1309 xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
1310 prop->name, prop->ns, NULL, 0);
1311 }
1312
1313 mbname_free(&mbname);
1314 free(href);
1315 return precond ? HTTP_FORBIDDEN : 0;
1316 }
1317
1318 /* Check headers for any preconditions */
caldav_check_precond(struct transaction_t * txn,struct meth_params * params,struct mailbox * mailbox,const void * data,const char * etag,time_t lastmod)1319 static int caldav_check_precond(struct transaction_t *txn,
1320 struct meth_params *params,
1321 struct mailbox *mailbox, const void *data,
1322 const char *etag, time_t lastmod)
1323 {
1324 const struct caldav_data *cdata = (const struct caldav_data *) data;
1325 const char *stag = cdata && cdata->organizer ? cdata->sched_tag : NULL;
1326 const char **hdr;
1327 int precond = 0;
1328
1329 if (txn->meth == METH_DELETE && !cdata) {
1330 /* Must not delete default scheduling calendar */
1331 char *defaultname = caldav_scheddefault(httpd_userid);
1332 char *defaultmboxname = caldav_mboxname(httpd_userid, defaultname);
1333 if (!strcmp(mailbox->name, defaultmboxname)) {
1334 precond = HTTP_FORBIDDEN;
1335 txn->error.precond = CALDAV_DEFAULT_NEEDED;
1336 }
1337 free(defaultmboxname);
1338 free(defaultname);
1339 if (precond) return precond;
1340 }
1341
1342 /* Do normal WebDAV/HTTP checks (primarily for lock-token via If header) */
1343 precond = dav_check_precond(txn, params, mailbox, data, etag, lastmod);
1344 if (precond == HTTP_PRECOND_FAILED &&
1345 cdata->comp_flags.tzbyref && !cdata->organizer && cdata->sched_tag) {
1346 /* Resource has just had VTIMEZONEs stripped -
1347 check if conditional matches previous ETag */
1348
1349 precond = check_precond(txn, cdata->sched_tag, lastmod);
1350 }
1351 if (!(precond == HTTP_OK || precond == HTTP_PARTIAL)) return precond;
1352
1353 /* Per RFC 6638, check Schedule-Tag */
1354 if ((hdr = spool_getheader(txn->req_hdrs, "If-Schedule-Tag-Match"))) {
1355 /* Special case for Apple 'If-Schedule-Tag-Match:' with no value
1356 * and also no schedule tag on the record - let that match */
1357 if (cdata && !stag && !hdr[0][0]) return precond;
1358 if (etagcmp(hdr[0], stag)) return HTTP_PRECOND_FAILED;
1359 }
1360
1361 if (txn->meth == METH_GET || txn->meth == METH_HEAD) {
1362 /* Fill in Schedule-Tag for successful GET/HEAD */
1363 txn->resp_body.stag = stag;
1364 }
1365
1366 return precond;
1367 }
1368
1369
caldav_acl(struct transaction_t * txn,xmlNodePtr priv,int * rights)1370 static int caldav_acl(struct transaction_t *txn, xmlNodePtr priv, int *rights)
1371 {
1372 if (!xmlStrcmp(priv->ns->href, BAD_CAST XML_NS_CALDAV)) {
1373 /* CalDAV privileges */
1374 switch (txn->req_tgt.flags) {
1375 case TGT_SCHED_INBOX:
1376 if (!xmlStrcmp(priv->name, BAD_CAST "schedule-deliver"))
1377 *rights |= DACL_SCHED;
1378 else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-deliver-invite"))
1379 *rights |= DACL_INVITE;
1380 else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-deliver-reply"))
1381 *rights |= DACL_REPLY;
1382 else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-query-freebusy"))
1383 *rights |= DACL_SCHEDFB;
1384 else {
1385 /* DAV:not-supported-privilege */
1386 txn->error.precond = DAV_SUPP_PRIV;
1387 }
1388 break;
1389 case TGT_SCHED_OUTBOX:
1390 if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send"))
1391 *rights |= DACL_SCHED;
1392 else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send-invite"))
1393 *rights |= DACL_INVITE;
1394 else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send-reply"))
1395 *rights |= DACL_REPLY;
1396 else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send-freebusy"))
1397 *rights |= DACL_SCHEDFB;
1398 else {
1399 /* DAV:not-supported-privilege */
1400 txn->error.precond = DAV_SUPP_PRIV;
1401 }
1402 break;
1403 default:
1404 if (!xmlStrcmp(priv->name, BAD_CAST "read-free-busy"))
1405 *rights |= DACL_READFB;
1406 else {
1407 /* DAV:not-supported-privilege */
1408 txn->error.precond = DAV_SUPP_PRIV;
1409 }
1410 break;
1411 }
1412
1413 /* Done processing this priv */
1414 return 1;
1415 }
1416 else if (!xmlStrcmp(priv->ns->href, BAD_CAST XML_NS_DAV)) {
1417 /* WebDAV privileges */
1418 if (!xmlStrcmp(priv->name, BAD_CAST "all")) {
1419 switch (txn->req_tgt.flags) {
1420 case TGT_SCHED_INBOX:
1421 /* DAV:all aggregates CALDAV:schedule-deliver */
1422 *rights |= DACL_SCHED;
1423 break;
1424 case TGT_SCHED_OUTBOX:
1425 /* DAV:all aggregates CALDAV:schedule-send */
1426 *rights |= DACL_SCHED;
1427 break;
1428 default:
1429 /* DAV:all aggregates CALDAV:read-free-busy */
1430 *rights |= DACL_READFB;
1431 break;
1432 }
1433 }
1434 else if (!xmlStrcmp(priv->name, BAD_CAST "read")) {
1435 switch (txn->req_tgt.flags) {
1436 case TGT_SCHED_INBOX:
1437 case TGT_SCHED_OUTBOX:
1438 break;
1439 default:
1440 /* DAV:read aggregates CALDAV:read-free-busy */
1441 *rights |= DACL_READFB;
1442 break;
1443 }
1444 }
1445 }
1446
1447 /* Process this priv in meth_acl() */
1448 return 0;
1449 }
1450
_scheduling_enabled(struct transaction_t * txn,const struct mailbox * mailbox)1451 static int _scheduling_enabled(struct transaction_t *txn,
1452 const struct mailbox *mailbox)
1453 {
1454 if (!(namespace_calendar.allow & ALLOW_CAL_SCHED)) return 0;
1455
1456 const char *entry = DAV_ANNOT_NS "<" XML_NS_CYRUS ">scheduling-enabled";
1457 struct buf buf = BUF_INITIALIZER;
1458 int is_enabled = 1;
1459
1460 annotatemore_lookup(mailbox->name, entry, "", &buf);
1461 /* legacy */
1462 if (!strcasecmp(buf_cstring(&buf), "no"))
1463 is_enabled = 0;
1464 if (!strcasecmp(buf_cstring(&buf), "F"))
1465 is_enabled = 0;
1466
1467 const char **hdr = spool_getheader(txn->req_hdrs, "Scheduling-Enabled");
1468 if (hdr && !strcasecmp(hdr[0], "F"))
1469 is_enabled = 0;
1470
1471 buf_free(&buf);
1472 return is_enabled;
1473 }
1474
1475 /* Perform a COPY/MOVE request
1476 *
1477 * preconditions:
1478 * CALDAV:supported-calendar-data
1479 * CALDAV:valid-calendar-data
1480 * CALDAV:valid-calendar-object-resource
1481 * CALDAV:supported-calendar-component
1482 * CALDAV:no-uid-conflict (DAV:href)
1483 * CALDAV:calendar-collection-location-ok
1484 * CALDAV:max-resource-size
1485 * CALDAV:min-date-time
1486 * CALDAV:max-date-time
1487 * CALDAV:max-instances
1488 * CALDAV:max-attendees-per-instance
1489 */
caldav_copy(struct transaction_t * txn,void * obj,struct mailbox * dest_mbox,const char * dest_rsrc,void * destdb,unsigned flags)1490 static int caldav_copy(struct transaction_t *txn, void *obj,
1491 struct mailbox *dest_mbox, const char *dest_rsrc,
1492 void *destdb, unsigned flags)
1493 {
1494 int r;
1495 struct caldav_db *db = (struct caldav_db *)destdb;
1496
1497 icalcomponent *comp, *ical = (icalcomponent *) obj;
1498 const char *organizer = NULL;
1499 icalproperty *prop;
1500
1501 if (!ical) {
1502 txn->error.precond = CALDAV_VALID_DATA;
1503 return HTTP_FORBIDDEN;
1504 }
1505
1506 if (_scheduling_enabled(txn, dest_mbox)) {
1507 comp = icalcomponent_get_first_real_component(ical);
1508 prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
1509 if (prop) organizer = icalproperty_get_organizer(prop);
1510 if (organizer) flags |= NEW_STAG;
1511 }
1512
1513 /* Store source resource at destination */
1514 /* XXX - set calendar-user-address based on original message? */
1515 /* XXX - get createdmodseq from source */
1516 r = caldav_store_resource(txn, ical, dest_mbox, dest_rsrc, 0,
1517 db, flags, httpd_userid, NULL);
1518
1519 return r;
1520 }
1521
1522
1523 static void decrement_refcount(const char *managed_id,
1524 struct mailbox *attachments,
1525 struct webdav_db *webdavdb);
1526 static struct webdav_data *increment_refcount(const char *managed_id,
1527 struct webdav_db *webdavdb);
1528
1529 enum {
1530 REFCNT_DEC = -1,
1531 REFCNT_HOLD = 0,
1532 REFCNT_INC = 1
1533 };
1534
1535 struct update_rock {
1536 struct mailbox *attachments;
1537 struct webdav_db *webdavdb;
1538 };
1539
update_refcount(const char * mid,short * op,struct update_rock * urock)1540 static void update_refcount(const char *mid, short *op,
1541 struct update_rock *urock)
1542 {
1543 switch (*op) {
1544 case REFCNT_DEC:
1545 decrement_refcount(mid, urock->attachments, urock->webdavdb);
1546 break;
1547
1548 case REFCNT_INC:
1549 increment_refcount(mid, urock->webdavdb);
1550 break;
1551 }
1552 }
1553
open_attachments(const char * userid,struct mailbox ** attachments,struct webdav_db ** webdavdb)1554 static int open_attachments(const char *userid, struct mailbox **attachments,
1555 struct webdav_db **webdavdb)
1556 {
1557 char *mailboxname = mailboxname = caldav_mboxname(userid, MANAGED_ATTACH);
1558 int r, ret = 0;
1559
1560 /* Open attachments collection for writing */
1561 r = mailbox_open_iwl(mailboxname, attachments);
1562 if (r) {
1563 syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
1564 mailboxname, error_message(r));
1565 ret = HTTP_SERVER_ERROR;
1566 }
1567 else {
1568 /* Open the WebDAV DB corresponding to the attachments collection */
1569 *webdavdb = webdav_open_mailbox(*attachments);
1570 if (!*webdavdb) {
1571 syslog(LOG_ERR,
1572 "webdav_open_mailbox(%s) failed", (*attachments)->name);
1573 ret = HTTP_SERVER_ERROR;
1574 }
1575 }
1576
1577 free(mailboxname);
1578
1579 return ret;
1580 }
1581
1582 /* Check an iCal object to see if managed attachments are being manipulated */
manage_attachments(struct transaction_t * txn,struct mailbox * mailbox,icalcomponent * ical,struct caldav_data * cdata,icalcomponent ** oldical,strarray_t * schedule_addresses)1583 static int manage_attachments(struct transaction_t *txn,
1584 struct mailbox *mailbox,
1585 icalcomponent *ical, struct caldav_data *cdata,
1586 icalcomponent **oldical, strarray_t *schedule_addresses)
1587 {
1588 /* Compare any managed attachments in new and existing resources */
1589 struct mailbox *attachments = NULL;
1590 struct webdav_db *webdavdb = NULL;
1591 struct hash_table mattach_table = HASH_TABLE_INITIALIZER;
1592 icalcomponent *comp = NULL;
1593 icalcomponent_kind kind;
1594 icalproperty *prop;
1595 icalparameter *param;
1596 const char *mid;
1597 short *op;
1598 int ret = 0;
1599
1600 /* Create hash table of managed attachments in new resource */
1601 construct_hash_table(&mattach_table, 10, 1);
1602
1603 if (ical) {
1604 comp = icalcomponent_get_first_real_component(ical);
1605 kind = icalcomponent_isa(comp);
1606 }
1607
1608 for (; comp;
1609 comp = icalcomponent_get_next_component(ical, kind)) {
1610
1611 for (prop = icalcomponent_get_first_property(comp,
1612 ICAL_ATTACH_PROPERTY);
1613 prop;
1614 prop = icalcomponent_get_next_property(comp,
1615 ICAL_ATTACH_PROPERTY)) {
1616
1617 icalattach *attach = icalproperty_get_attach(prop);
1618
1619 if (icalattach_get_is_url(attach)) {
1620 struct webdav_data *wdata;
1621
1622 param = icalproperty_get_managedid_parameter(prop);
1623 if (!param) continue;
1624
1625 if (!attachments) {
1626 /* Open attachments collection and its DAV DB for writing */
1627 ret = open_attachments(httpd_userid,
1628 &attachments, &webdavdb);
1629 if (ret) goto done;
1630 }
1631
1632 /* Find DAV record for the attachment with this managed-id */
1633 mid = icalparameter_get_managedid(param);
1634 webdav_lookup_uid(webdavdb, mid, &wdata);
1635
1636 if (!wdata->dav.rowid) {
1637 txn->error.precond = CALDAV_VALID_MANAGEDID;
1638 ret = HTTP_FORBIDDEN;
1639 goto done;
1640 }
1641
1642 if (!hash_lookup(mid, &mattach_table)) {
1643 /* Assume attachment is being added to ical */
1644 op = xmalloc(sizeof(short));
1645 *op = REFCNT_INC;
1646 hash_insert(mid, op, &mattach_table);
1647 }
1648 }
1649 else {
1650 /* XXX Do we want to strip and manage inline attachments? */
1651 }
1652 }
1653 }
1654
1655 /* Compare existing managed attachments to those in new resource */
1656 if (cdata->comp_flags.mattach) {
1657 if (!*oldical) {
1658 syslog(LOG_NOTICE, "LOADING ICAL %u", cdata->dav.imap_uid);
1659
1660 /* Load message containing the resource and parse iCal data */
1661 *oldical = caldav_record_to_ical(mailbox, cdata,
1662 NULL, schedule_addresses);
1663 if (!*oldical) {
1664 txn->error.desc = "Failed to read record";
1665 ret = HTTP_SERVER_ERROR;
1666 goto done;
1667 }
1668 }
1669
1670 comp = icalcomponent_get_first_real_component(*oldical);
1671 kind = icalcomponent_isa(comp);
1672
1673 for (; comp;
1674 comp = icalcomponent_get_next_component(*oldical, kind)) {
1675 for (prop = icalcomponent_get_first_property(comp,
1676 ICAL_ATTACH_PROPERTY);
1677 prop;
1678 prop = icalcomponent_get_next_property(comp,
1679 ICAL_ATTACH_PROPERTY)) {
1680
1681 param = icalproperty_get_managedid_parameter(prop);
1682 if (!param) continue;
1683
1684 mid = icalparameter_get_managedid(param);
1685 op = hash_lookup(mid, &mattach_table);
1686 if (!op) {
1687 /* Attachment removed from ical */
1688 op = xmalloc(sizeof(short));
1689 *op = REFCNT_DEC;
1690 hash_insert(mid, op, &mattach_table);
1691 }
1692 else if (*op != REFCNT_DEC) {
1693 /* Attachment still in ical */
1694 *op = REFCNT_HOLD;
1695 }
1696 }
1697 }
1698 }
1699
1700 if (hash_numrecords(&mattach_table)) {
1701 if (!attachments) {
1702 /* Open attachments collection and its DAV DB for writing */
1703 ret = open_attachments(httpd_userid, &attachments, &webdavdb);
1704 if (ret) goto done;
1705 }
1706
1707 /* Update reference counts of attachments in hash table */
1708 struct update_rock urock = { attachments, webdavdb };
1709 hash_enumerate(&mattach_table,
1710 (void(*)(const char*,void*,void*)) &update_refcount,
1711 &urock);
1712 }
1713
1714 done:
1715 free_hash_table(&mattach_table, free);
1716 if (webdavdb) webdav_close(webdavdb);
1717 mailbox_close(&attachments);
1718
1719 return ret;
1720 }
1721
1722
1723 /* Perform scheduling actions for a DELETE request */
caldav_delete_cal(struct transaction_t * txn,struct mailbox * mailbox,struct index_record * record,void * data)1724 static int caldav_delete_cal(struct transaction_t *txn,
1725 struct mailbox *mailbox,
1726 struct index_record *record, void *data)
1727 {
1728 struct caldav_data *cdata = (struct caldav_data *) data;
1729 icalcomponent *ical = NULL;
1730 struct buf buf = BUF_INITIALIZER;
1731 strarray_t schedule_addresses = STRARRAY_INITIALIZER;
1732 int r = 0;
1733
1734 /* Only process deletes on regular calendar collections */
1735 if (txn->req_tgt.flags) return 0;
1736
1737 if ((namespace_calendar.allow & ALLOW_CAL_ATTACH) &&
1738 cdata->comp_flags.mattach) {
1739 r = manage_attachments(txn, mailbox, NULL,
1740 cdata, &ical, &schedule_addresses);
1741 if (r) goto done;
1742 }
1743
1744 if (cdata->organizer) {
1745 /* Scheduling object resource */
1746 const char **hdr;
1747
1748 /* XXX - check date range? - don't send in the past */
1749
1750 /* Load message containing the resource and parse iCal data */
1751 if (!ical) ical = record_to_ical(mailbox, record, &schedule_addresses);
1752
1753 if (!ical) {
1754 syslog(LOG_ERR,
1755 "meth_delete: failed to parse iCalendar object %s:%u",
1756 txn->req_tgt.mbentry->name, record->uid);
1757 return HTTP_SERVER_ERROR;
1758 }
1759
1760 get_schedule_addresses(txn->req_hdrs, txn->req_tgt.mbentry->name,
1761 txn->req_tgt.userid, &schedule_addresses);
1762
1763 /* XXX - after legacy records are gone, we can strip this and just not send a
1764 * cancellation if deleting a record which was never replied to... */
1765
1766 char *userid = mboxname_to_userid(txn->req_tgt.mbentry->name);
1767 if (strarray_find_case(&schedule_addresses, cdata->organizer, 0) >= 0) {
1768 /* Organizer scheduling object resource */
1769 if (_scheduling_enabled(txn, mailbox))
1770 sched_request(userid, &schedule_addresses, cdata->organizer, ical, NULL);
1771 }
1772 else if (!(hdr = spool_getheader(txn->req_hdrs, "Schedule-Reply")) ||
1773 strcasecmp(hdr[0], "F")) {
1774 /* Attendee scheduling object resource */
1775 if (_scheduling_enabled(txn, mailbox) && strarray_size(&schedule_addresses))
1776 sched_reply(userid, &schedule_addresses, ical, NULL);
1777 }
1778
1779 free(userid);
1780 }
1781
1782 done:
1783 if (ical) icalcomponent_free(ical);
1784 strarray_fini(&schedule_addresses);
1785 buf_free(&buf);
1786
1787 return r;
1788 }
1789
begin_icalendar(struct buf * buf,struct mailbox * mailbox,const char * prodid,const char * name,const char * desc,const char * color)1790 static const char *begin_icalendar(struct buf *buf, struct mailbox *mailbox,
1791 const char *prodid, const char *name,
1792 const char *desc, const char *color)
1793 {
1794 icalcomponent *ical;
1795 icalproperty *prop;
1796
1797 /* Begin iCalendar stream */
1798 buf_setcstr(buf, "BEGIN:VCALENDAR\r\n");
1799
1800 /* Add toplevel properties */
1801 ical = icalcomponent_new_stream(mailbox, prodid, name, desc, color);
1802
1803 for (prop = icalcomponent_get_first_property(ical, ICAL_ANY_PROPERTY);
1804 prop;
1805 prop = icalcomponent_get_next_property(ical, ICAL_ANY_PROPERTY)) {
1806
1807 buf_appendcstr(buf, icalproperty_as_ical_string(prop));
1808 }
1809 icalcomponent_free(ical);
1810
1811 return "";
1812 }
1813
end_icalendar(struct buf * buf)1814 static void end_icalendar(struct buf *buf)
1815 {
1816 /* End iCalendar stream */
1817 buf_setcstr(buf, "END:VCALENDAR\r\n");
1818 }
1819
1820
1821 struct timezone_rock {
1822 icalcomponent *old;
1823 icalcomponent *new;
1824 };
1825
add_timezone(icalparameter * param,void * data)1826 static void add_timezone(icalparameter *param, void *data)
1827 {
1828 struct timezone_rock *tzrock = (struct timezone_rock *) data;
1829 const char *tzid = icalparameter_get_tzid(param);
1830
1831 /* Check if this tz is in our new object */
1832 if (!icalcomponent_get_timezone(tzrock->new, tzid)) {
1833 icalcomponent *vtz = NULL;
1834
1835 if (tzrock->old) {
1836 /* Fetch tz from old object and add to new */
1837 icaltimezone *tz = icalcomponent_get_timezone(tzrock->old, tzid);
1838 if (tz) vtz = icalcomponent_clone(icaltimezone_get_component(tz));
1839 }
1840 else {
1841 /* Fetch tz from builtin repository */
1842 icaltimezone *tz = icaltimezone_get_builtin_timezone(tzid);
1843
1844 if (tz) vtz = icalcomponent_clone(icaltimezone_get_component(tz));
1845 }
1846
1847 if (vtz) icalcomponent_add_component(tzrock->new, vtz);
1848 }
1849 }
1850
1851
export_calendar(struct transaction_t * txn)1852 static int export_calendar(struct transaction_t *txn)
1853 {
1854 int ret = 0, r, n, precond, need_tz = 1;
1855 struct resp_body_t *resp_body = &txn->resp_body;
1856 struct buf *buf = &resp_body->payload;
1857 struct mailbox *mailbox = NULL;
1858 static char etag[33];
1859 struct hash_table tzid_table;
1860 static const char *displayname_annot =
1861 DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
1862 static const char *description_annot =
1863 DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-description";
1864 static const char *color_annot =
1865 DAV_ANNOT_NS "<" XML_NS_APPLE ">calendar-color";
1866 struct buf name = BUF_INITIALIZER, link = BUF_INITIALIZER;
1867 struct buf desc = BUF_INITIALIZER, color = BUF_INITIALIZER;
1868 const char **hdr, *sep;
1869 struct mime_type_t *mime = NULL;
1870 modseq_t syncmodseq = 0;
1871 int unbind_flag = -1, unchanged_flag = -1;
1872 struct caldav_db *caldavdb = NULL;
1873
1874 /* Check requested MIME type:
1875 1st entry in caldav_mime_types array MUST be default MIME type */
1876 if ((hdr = spool_getheader(txn->req_hdrs, "Accept")))
1877 mime = get_accept_type(hdr, caldav_mime_types);
1878 else mime = caldav_mime_types;
1879 if (!mime) return HTTP_NOT_ACCEPTABLE;
1880
1881 /* Open mailbox for reading */
1882 r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox);
1883 if (r) {
1884 syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
1885 txn->req_tgt.mbentry->name, error_message(r));
1886 txn->error.desc = error_message(r);
1887 ret = HTTP_SERVER_ERROR;
1888 goto done;
1889 }
1890
1891 /* Check any preconditions */
1892 assert(!buf_len(&txn->buf));
1893 dav_get_synctoken(mailbox, &txn->buf, "");
1894 strlcpy(etag, buf_cstring(&txn->buf), sizeof(etag));
1895 precond = check_precond(txn, etag, mailbox->index_mtime);
1896
1897 switch (precond) {
1898 case HTTP_OK:
1899 case HTTP_NOT_MODIFIED:
1900 /* Fill in ETag, Last-Modified, Expires, and Cache-Control */
1901 txn->resp_body.etag = etag;
1902 txn->resp_body.lastmod = mailbox->index_mtime;
1903 txn->resp_body.maxage = 3600; /* 1 hr */
1904 txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE; /* don't use stale data */
1905 if (httpd_userid) txn->flags.cc |= CC_PRIVATE;
1906
1907 if (precond != HTTP_NOT_MODIFIED) break;
1908
1909 GCC_FALLTHROUGH
1910
1911 default:
1912 /* We failed a precondition - don't perform the request */
1913 ret = precond;
1914 goto done;
1915 }
1916
1917 /* Setup for chunked response */
1918 txn->flags.te |= TE_CHUNKED;
1919 txn->flags.vary |= VARY_ACCEPT | VARY_PREFER | VARY_IFNONE | VARY_CALTZ;
1920 txn->resp_body.type = mime->content_type;
1921
1922 /* Set filename of resource */
1923 r = annotatemore_lookupmask(mailbox->name, displayname_annot,
1924 httpd_userid, &name);
1925 /* fall back to last part of mailbox name */
1926 if (r || !name.len) buf_setcstr(&name, strrchr(mailbox->name, '.') + 1);
1927
1928 buf_reset(&txn->buf);
1929 buf_printf(&txn->buf, "%s.%s", buf_cstring(&name), mime->file_ext);
1930 txn->resp_body.dispo.fname = buf_cstring(&txn->buf);
1931
1932 /* Add subscription upgrade links */
1933 buf_printf(&link, "<%s>; rel=\"subscribe-caldav_auth\"", txn->req_tgt.path);
1934 strarray_appendm(&txn->resp_body.links, buf_release(&link));
1935 buf_printf(&link, "<%s>; rel=\"subscribe-webdav_sync\"", txn->req_tgt.path);
1936 strarray_appendm(&txn->resp_body.links, buf_release(&link));
1937
1938 if (get_preferences(txn) & PREFER_MIN) {
1939 /* Enhanced GET request */
1940 need_tz = 0;
1941
1942 if ((hdr = spool_getheader(txn->req_hdrs, "If-None-Match"))) {
1943 /* Report only changed resources since ETag (sync-token) */
1944 uint32_t uidvalidity;
1945 char dquote[2];
1946
1947 /* Parse sync-token */
1948 n = sscanf((char *) hdr[0], "\"%u-" MODSEQ_FMT "%1s",
1949 &uidvalidity, &syncmodseq, dquote /* trailing DQUOTE */);
1950
1951 syslog(LOG_DEBUG, "scanned token %s to %d %u %llu",
1952 hdr[0], n, uidvalidity, syncmodseq);
1953
1954 /* Sanity check the token components */
1955 if (n != 3 || dquote[0] != '"' ||
1956 uidvalidity != mailbox->i.uidvalidity ||
1957 syncmodseq > mailbox->i.highestmodseq ||
1958 syncmodseq < mailbox->i.deletedmodseq) {
1959 syncmodseq = 0;
1960 txn->resp_body.prefs &= ~PREFER_MIN;
1961 }
1962 else {
1963 mailbox_user_flag(mailbox, DFLAG_UNBIND, &unbind_flag, 1);
1964 mailbox_user_flag(mailbox, DFLAG_UNCHANGED, &unchanged_flag, 1);
1965
1966 if (namespace_calendar.allow & ALLOW_CAL_NOTZ) {
1967 /* Add link to tzdist */
1968 buf_printf(&link, "<%s>; rel=\"timezone-service\"",
1969 namespace_tzdist.prefix);
1970 strarray_appendm(&txn->resp_body.links, buf_release(&link));
1971 }
1972 }
1973
1974 /* Check for optional CalDAV-Timezones header */
1975 hdr = spool_getheader(txn->req_hdrs, "CalDAV-Timezones");
1976 if (hdr && !strcmp(hdr[0], "T")) need_tz = 1;
1977 }
1978 }
1979 else {
1980 buf_printf(&link,
1981 "<%s>; rel=\"subscribe-enhanced-get\"", txn->req_tgt.path);
1982 strarray_appendm(&txn->resp_body.links, buf_release(&link));
1983 }
1984
1985 /* Short-circuit for HEAD request */
1986 if (txn->meth == METH_HEAD) {
1987 ret = HTTP_OK;
1988 goto done;
1989 }
1990
1991 /* iCalendar data in response should not be transformed */
1992 txn->flags.cc |= CC_NOTRANSFORM;
1993
1994 /* Create hash table for TZIDs */
1995 construct_hash_table(&tzid_table, 10, 1);
1996
1997 /* Get description and color of calendar */
1998 r = annotatemore_lookupmask(mailbox->name, description_annot,
1999 httpd_userid, &desc);
2000 r = annotatemore_lookupmask(mailbox->name, color_annot,
2001 httpd_userid, &color);
2002
2003 /* Begin (converted) iCalendar stream */
2004 sep = mime->begin_stream(buf, mailbox, ical_prodid, buf_cstring(&name),
2005 buf_cstringnull(&desc),
2006 css3_color_hex_to_name(buf_cstringnull(&color)));
2007 write_body(HTTP_OK, txn, buf_cstring(buf), buf_len(buf));
2008
2009 struct mailbox_iter *iter =
2010 mailbox_iter_init(mailbox, syncmodseq,
2011 syncmodseq ? 0 : ITER_SKIP_EXPUNGED|ITER_SKIP_DELETED);
2012
2013 if (!syncmodseq) caldavdb = caldav_open_mailbox(mailbox);
2014
2015 n = 0;
2016 const message_t *msg;
2017 while ((msg = mailbox_iter_step(iter))) {
2018 const struct index_record *record = msg_record(msg);
2019 struct caldav_data *cdata;
2020 icalcomponent *ical = NULL;
2021
2022 r = caldav_lookup_imapuid(caldavdb, mailbox->name,
2023 record->uid, &cdata, 0);
2024
2025 if (syncmodseq) {
2026 if ((unbind_flag >= 0) &&
2027 record->user_flags[unbind_flag / 32] & (1 << (unbind_flag & 31))) {
2028 /* Resource replaced by a PUT, COPY, or MOVE - ignore it */
2029 continue;
2030 }
2031
2032 if ((record->modseq - syncmodseq == 1) &&
2033 (unchanged_flag >= 0) &&
2034 (record->user_flags[unchanged_flag / 32] &
2035 (1 << (unchanged_flag & 31)))) {
2036 /* Resource has just had VTIMEZONEs stripped - ignore it */
2037 continue;
2038 }
2039 }
2040
2041 /* Map and parse existing iCalendar resource */
2042 if (!r) ical = caldav_record_to_ical(mailbox, cdata, httpd_userid, NULL);
2043
2044 if (ical) {
2045 icalcomponent *comp;
2046
2047 if (!syncmodseq) {
2048 struct caldav_data *cdata;
2049
2050 /* Fetch the CalDAV db record */
2051 r = caldav_lookup_imapuid(caldavdb, mailbox->name,
2052 record->uid, &cdata, 0);
2053
2054 if (!r && need_tz && cdata->comp_flags.tzbyref) {
2055 /* Add VTIMEZONE components for known TZIDs */
2056 struct timezone_rock tzrock = { NULL, ical };
2057 icalcomponent *next;
2058 icalcomponent_kind kind;
2059
2060 comp = icalcomponent_get_first_real_component(ical);
2061 kind = icalcomponent_isa(comp);
2062 for (; comp; comp = next) {
2063 next = icalcomponent_get_next_component(ical, kind);
2064 icalcomponent_foreach_tzid(comp, &add_timezone, &tzrock);
2065 }
2066 }
2067 }
2068
2069 for (comp = icalcomponent_get_first_component(ical,
2070 ICAL_ANY_COMPONENT);
2071 comp;
2072 comp = icalcomponent_get_next_component(ical,
2073 ICAL_ANY_COMPONENT)) {
2074 icalcomponent_kind kind = icalcomponent_isa(comp);
2075
2076 /* Don't duplicate any VTIMEZONEs in our iCalendar */
2077 if (kind == ICAL_VTIMEZONE_COMPONENT) {
2078 if (syncmodseq) continue;
2079 if (record->internal_flags & FLAG_INTERNAL_EXPUNGED) continue;
2080
2081 icalproperty *prop =
2082 icalcomponent_get_first_property(comp,
2083 ICAL_TZID_PROPERTY);
2084 const char *tzid = icalproperty_get_tzid(prop);
2085
2086 if (!tzid) continue;
2087
2088 if (hash_lookup(tzid, &tzid_table)) continue;
2089 else hash_insert(tzid, (void *)0xDEADBEEF, &tzid_table);
2090 }
2091 else if (record->internal_flags & FLAG_INTERNAL_EXPUNGED) {
2092 /* Resource was deleted - remove non-mandatory properties */
2093 icalproperty *prop, *next;
2094
2095 for (prop =
2096 icalcomponent_get_first_property(comp,
2097 ICAL_ANY_PROPERTY);
2098 prop; prop = next) {
2099 next =
2100 icalcomponent_get_next_property(comp,
2101 ICAL_ANY_PROPERTY);
2102
2103 switch (icalproperty_isa(prop)) {
2104 case ICAL_UID_PROPERTY:
2105 case ICAL_DTSTAMP_PROPERTY:
2106 case ICAL_DTSTART_PROPERTY:
2107 /* Mandatory - keep */
2108 break;
2109
2110 default:
2111 /* Optional - strip */
2112 icalcomponent_remove_property(comp, prop);
2113 icalproperty_free(prop);
2114 break;
2115 }
2116 }
2117
2118 /* Set STATUS:DELETED */
2119 icalcomponent_set_status(comp, ICAL_STATUS_DELETED);
2120 }
2121
2122 /* Include this component in our iCalendar */
2123 if (n++ && *sep) {
2124 /* Add separator, if necessary */
2125 buf_reset(buf);
2126 buf_printf_markup(buf, 0, "%s", sep);
2127 write_body(0, txn, buf_cstring(buf), buf_len(buf));
2128 }
2129 struct buf *cal_str = mime->from_object(comp);
2130 write_body(0, txn, buf_base(cal_str), buf_len(cal_str));
2131 buf_destroy(cal_str);
2132 }
2133
2134 icalcomponent_free(ical);
2135 }
2136 }
2137
2138 mailbox_iter_done(&iter);
2139
2140 caldav_close(caldavdb);
2141
2142 free_hash_table(&tzid_table, NULL);
2143
2144 /* End (converted) iCalendar stream */
2145 mime->end_stream(buf);
2146 write_body(0, txn, buf_cstring(buf), buf_len(buf));
2147
2148 /* End of output */
2149 write_body(0, txn, NULL, 0);
2150
2151 done:
2152 buf_free(&name);
2153 buf_free(&desc);
2154 buf_free(&color);
2155 mailbox_close(&mailbox);
2156
2157 return ret;
2158 }
2159
2160
2161 /*
2162 * mboxlist_findall() callback function to list calendars
2163 */
2164
2165 struct cal_info {
2166 char shortname[MAX_MAILBOX_NAME];
2167 char displayname[MAX_MAILBOX_NAME];
2168 unsigned flags;
2169 unsigned long types;
2170 };
2171
2172 enum {
2173 CAL_IS_DEFAULT = (1<<0),
2174 CAL_CAN_DELETE = (1<<1),
2175 CAL_CAN_ADMIN = (1<<2),
2176 CAL_IS_PUBLIC = (1<<3),
2177 CAL_IS_TRANSP = (1<<4)
2178 };
2179
2180 struct list_cal_rock {
2181 struct cal_info *cal;
2182 unsigned len;
2183 unsigned alloc;
2184 char *scheddefault;
2185 size_t defaultlen;
2186 };
2187
list_cal_cb(const mbentry_t * mbentry,void * rock)2188 static int list_cal_cb(const mbentry_t *mbentry, void *rock)
2189 {
2190 struct list_cal_rock *lrock = (struct list_cal_rock *) rock;
2191 struct cal_info *cal;
2192 static size_t inboxlen = 0;
2193 static size_t outboxlen = 0;
2194 char *shortname;
2195 size_t len;
2196 int r, rights, any_rights = 0;
2197 static const char *displayname_annot =
2198 DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
2199 static const char *schedtransp_annot =
2200 DAV_ANNOT_NS "<" XML_NS_CALDAV ">schedule-calendar-transp";
2201 static const char *calcompset_annot =
2202 DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
2203 struct buf displayname = BUF_INITIALIZER, schedtransp = BUF_INITIALIZER;
2204 struct buf calcompset = BUF_INITIALIZER;
2205
2206 if (!inboxlen) inboxlen = strlen(SCHED_INBOX) - 1;
2207 if (!outboxlen) outboxlen = strlen(SCHED_OUTBOX) - 1;
2208
2209 /* Make sure its a calendar */
2210 if (mbentry->mbtype != MBTYPE_CALENDAR) goto done;
2211
2212 /* Make sure its readable */
2213 rights = httpd_myrights(httpd_authstate, mbentry);
2214 if ((rights & DACL_READ) != DACL_READ) goto done;
2215
2216 /* Don't list scheduling Inbox/Outbox */
2217 shortname = strrchr(mbentry->name, '.') + 1;
2218 len = strlen(shortname);
2219
2220 if ((len == inboxlen && !strncmp(shortname, SCHED_INBOX, inboxlen)) ||
2221 (len == outboxlen && !strncmp(shortname, SCHED_OUTBOX, outboxlen)))
2222 goto done;
2223
2224 /* Lookup DAV:displayname */
2225 r = annotatemore_lookupmask(mbentry->name, displayname_annot,
2226 httpd_userid, &displayname);
2227 /* fall back to the last part of the mailbox name */
2228 if (r || !displayname.len) buf_setcstr(&displayname, shortname);
2229
2230 /* Make sure we have room in our array */
2231 if (lrock->len == lrock->alloc) {
2232 lrock->alloc += 100;
2233 lrock->cal = xrealloc(lrock->cal,
2234 lrock->alloc * sizeof(struct cal_info));
2235 }
2236
2237 /* Add our calendar to the array */
2238 cal = &lrock->cal[lrock->len];
2239 strlcpy(cal->shortname, shortname, MAX_MAILBOX_NAME);
2240 strlcpy(cal->displayname, buf_cstring(&displayname), MAX_MAILBOX_NAME);
2241 cal->flags = 0;
2242
2243 /* Is this the default calendar? */
2244 if (len == lrock->defaultlen &&
2245 !strncmp(shortname, lrock->scheddefault, lrock->defaultlen)) {
2246 cal->flags |= CAL_IS_DEFAULT;
2247 }
2248
2249 /* Can we delete this calendar? */
2250 else if (rights & DACL_RMCOL) {
2251 cal->flags |= CAL_CAN_DELETE;
2252 }
2253
2254 /* Can we admin this calendar? */
2255 if (rights & DACL_ADMIN) {
2256 cal->flags |= CAL_CAN_ADMIN;
2257 }
2258
2259 /* Is this calendar public? */
2260 if (mbentry->acl) {
2261 struct auth_state *auth_anyone = auth_newstate("anyone");
2262
2263 any_rights = cyrus_acl_myrights(auth_anyone, mbentry->acl);
2264 auth_freestate(auth_anyone);
2265 }
2266 if ((any_rights & DACL_READ) == DACL_READ) {
2267 cal->flags |= CAL_IS_PUBLIC;
2268 }
2269
2270 /* Is this calendar transparent? */
2271 r = annotatemore_lookupmask(mbentry->name, schedtransp_annot,
2272 httpd_userid, &schedtransp);
2273 if (!r && !strcmp(buf_cstring(&schedtransp), "transparent")) {
2274 cal->flags |= CAL_IS_TRANSP;
2275 }
2276 buf_free(&schedtransp);
2277
2278 /* Which component types are supported? */
2279 r = annotatemore_lookupmask(mbentry->name, calcompset_annot,
2280 httpd_userid, &calcompset);
2281 if (!r && buf_len(&calcompset)) {
2282 cal->types = strtoul(buf_cstring(&calcompset), NULL, 10);
2283 }
2284 else {
2285 /* ALL component types */
2286 cal->types = -1;
2287 }
2288 buf_free(&calcompset);
2289
2290 lrock->len++;
2291
2292 done:
2293 buf_free(&displayname);
2294
2295 return 0;
2296 }
2297
cal_compare(const void * a,const void * b)2298 static int cal_compare(const void *a, const void *b)
2299 {
2300 struct cal_info *c1 = (struct cal_info *) a;
2301 struct cal_info *c2 = (struct cal_info *) b;
2302
2303 return strcmp(c1->displayname, c2->displayname);
2304 }
2305
2306
2307 struct list_tzid_rock {
2308 struct buf *body;
2309 unsigned *level;
2310 };
2311
list_tzid_cb(const char * tzid,int tzidlen,struct zoneinfo * zi,void * rock)2312 int list_tzid_cb(const char *tzid,
2313 int tzidlen __attribute__((unused)),
2314 struct zoneinfo *zi __attribute__((unused)),
2315 void *rock)
2316 {
2317 struct list_tzid_rock *tzrock = (struct list_tzid_rock *) rock;
2318
2319 /* Skip Etc and other non-standard zones */
2320 if (strnchr(tzid, '/', tzidlen) && strncmp(tzid, "Etc/", 4)) {
2321 buf_printf_markup(tzrock->body, *tzrock->level,
2322 "<option>%.*s</option>", tzidlen, tzid);
2323 }
2324
2325 return 0;
2326 }
2327
2328
2329 /* Create a HTML document listing all calendars available to the user */
list_calendars(struct transaction_t * txn)2330 static int list_calendars(struct transaction_t *txn)
2331 {
2332 int ret = 0, precond, rights;
2333 char mboxlist[MAX_MAILBOX_PATH+1];
2334 struct stat sbuf;
2335 time_t lastmod;
2336 const char *etag, *base_path = txn->req_tgt.path;
2337 unsigned level = 0, i;
2338 struct buf *body = &txn->resp_body.payload;
2339 struct list_cal_rock lrock;
2340 const char *proto = NULL;
2341 const char *host = NULL;
2342 const struct cal_comp_t *comp;
2343 #include "imap/http_caldav_js.h"
2344
2345 /* stat() mailboxes.db for Last-Modified and ETag */
2346 snprintf(mboxlist, MAX_MAILBOX_PATH, "%s%s", config_dir, FNAME_MBOXLIST);
2347 stat(mboxlist, &sbuf);
2348 lastmod = MAX(compile_time, sbuf.st_mtime);
2349 assert(!buf_len(&txn->buf));
2350 buf_printf(&txn->buf, TIME_T_FMT "-" TIME_T_FMT "-" OFF_T_FMT,
2351 compile_time, sbuf.st_mtime, sbuf.st_size);
2352
2353 /* stat() config file for Last-Modified and ETag */
2354 stat(config_filename, &sbuf);
2355 lastmod = MAX(lastmod, sbuf.st_mtime);
2356 buf_printf(&txn->buf, "-" TIME_T_FMT "-" OFF_T_FMT, sbuf.st_mtime, sbuf.st_size);
2357 etag = buf_cstring(&txn->buf);
2358
2359 /* Check any preconditions */
2360 precond = check_precond(txn, etag, lastmod);
2361
2362 switch (precond) {
2363 case HTTP_OK:
2364 case HTTP_NOT_MODIFIED:
2365 /* Fill in ETag, Last-Modified, and Expires */
2366 txn->resp_body.etag = etag;
2367 txn->resp_body.lastmod = lastmod;
2368 txn->flags.cc |= CC_REVALIDATE;
2369
2370 if (precond != HTTP_NOT_MODIFIED) break;
2371
2372 GCC_FALLTHROUGH
2373
2374 default:
2375 /* We failed a precondition - don't perform the request */
2376 ret = precond;
2377 goto done;
2378 }
2379
2380 /* Setup for chunked response */
2381 txn->flags.te |= TE_CHUNKED;
2382 txn->resp_body.type = "text/html; charset=utf-8";
2383
2384 /* Short-circuit for HEAD request */
2385 if (txn->meth == METH_HEAD) {
2386 response_header(HTTP_OK, txn);
2387 goto done;
2388 }
2389
2390 /* Send HTML header */
2391 buf_reset(body);
2392 buf_printf_markup(body, level, HTML_DOCTYPE);
2393 buf_printf_markup(body, level++, "<html>");
2394 buf_printf_markup(body, level++, "<head>");
2395 buf_printf_markup(body, level, "<title>%s</title>", "Available Calendars");
2396 buf_printf_markup(body, level++, "<script type=\"text/javascript\">");
2397 buf_appendcstr(body, "//<![CDATA[\n");
2398 buf_printf(body, (const char *) http_caldav_js,
2399 CYRUS_VERSION, http_caldav_js_len);
2400 buf_appendcstr(body, "//]]>\n");
2401 buf_printf_markup(body, --level, "</script>");
2402 buf_printf_markup(body, level++, "<noscript>");
2403 buf_printf_markup(body, level, "<i>*** %s ***</i>",
2404 "JavaScript required to create/modify/delete calendars");
2405 buf_printf_markup(body, --level, "</noscript>");
2406 buf_printf_markup(body, --level, "</head>");
2407 buf_printf_markup(body, level++, "<body>");
2408
2409 write_body(HTTP_OK, txn, buf_cstring(body), buf_len(body));
2410 buf_reset(body);
2411
2412 /* Check ACL for current user */
2413 rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
2414
2415 if (rights & DACL_MKCOL) {
2416 /* Add "create" form */
2417 struct list_tzid_rock tzrock = { body, &level };
2418
2419 buf_printf_markup(body, level, "<h2>%s</h2>", "Create New Calendar");
2420 buf_printf_markup(body, level++, "<form name='create'>");
2421 buf_printf_markup(body, level++, "<table cellpadding=5>");
2422 buf_printf_markup(body, level++, "<tr>");
2423 buf_printf_markup(body, level, "<td align=right>Name:</td>");
2424 buf_printf_markup(body, level,
2425 "<td><input name=name size=30 maxlength=40></td>");
2426 buf_printf_markup(body, --level, "</tr>");
2427
2428 buf_printf_markup(body, level++, "<tr>");
2429 buf_printf_markup(body, level, "<td align=right>Description:</td>");
2430 buf_printf_markup(body, level,
2431 "<td><input name=desc size=75 maxlength=120></td>");
2432 buf_printf_markup(body, --level, "</tr>");
2433
2434 buf_printf_markup(body, level++, "<tr>");
2435 buf_printf_markup(body, level, "<td align=right>Components:</td>");
2436 buf_printf_markup(body, level++, "<td>");
2437 for (comp = cal_comps; comp->name; comp++) {
2438 buf_printf_markup(body, level,
2439 "<input type=checkbox%s name=comp value=%s>%s",
2440 !strcmp(comp->name, "VEVENT") ? " checked" : "",
2441 comp->name, comp->name);
2442 }
2443 buf_printf_markup(body, --level, "</td>");
2444 buf_printf_markup(body, --level, "</tr>");
2445
2446 if (namespace_calendar.allow & ALLOW_CAL_NOTZ) {
2447 buf_printf_markup(body, level++, "<tr>");
2448 buf_printf_markup(body, level, "<td align=right>Time Zone:</td>");
2449 buf_printf_markup(body, level++, "<td>");
2450 buf_printf_markup(body, level++, "<select name=tzid>");
2451 buf_printf_markup(body, level, "<option></option>");
2452 zoneinfo_find(NULL, 1, 0, &list_tzid_cb, &tzrock);
2453 buf_printf_markup(body, --level, "</select>");
2454 buf_printf_markup(body, --level, "</td>");
2455 buf_printf_markup(body, --level, "</tr>");
2456 }
2457
2458 buf_printf_markup(body, level++, "<tr>");
2459 buf_printf_markup(body, level, "<td></td>");
2460 buf_printf_markup(body, level,
2461 "<td><br><input type=button value='Create'"
2462 " onclick=\"createCalendar('%s')\">"
2463 " <input type=reset></td>",
2464 base_path);
2465 buf_printf_markup(body, --level, "</tr>");
2466
2467 buf_printf_markup(body, --level, "</table>");
2468 buf_printf_markup(body, --level, "</form>");
2469
2470 buf_printf_markup(body, level, "<br><hr><br>");
2471
2472 write_body(0, txn, buf_cstring(body), buf_len(body));
2473 buf_reset(body);
2474 }
2475
2476 buf_printf_markup(body, level, "<h2>%s</h2>", "Available Calendars");
2477 buf_printf_markup(body, level++, "<table border cellpadding=5>");
2478
2479 /* Create base URL for calendars */
2480 http_proto_host(txn->req_hdrs, &proto, &host);
2481 buf_reset(&txn->buf);
2482 buf_printf(&txn->buf, "%s://%s%s", proto, host, txn->req_tgt.path);
2483
2484 memset(&lrock, 0, sizeof(struct list_cal_rock));
2485 lrock.scheddefault = caldav_scheddefault(httpd_userid);
2486 lrock.defaultlen = strlen(lrock.scheddefault);
2487 mboxlist_mboxtree(txn->req_tgt.mbentry->name,
2488 list_cal_cb, &lrock, MBOXTREE_SKIP_ROOT);
2489 free(lrock.scheddefault);
2490 lrock.scheddefault = NULL;
2491
2492 /* Sort calendars by displayname */
2493 qsort(lrock.cal, lrock.len, sizeof(struct cal_info), &cal_compare);
2494
2495 /* Add available calendars with action items */
2496 for (i = 0; i < lrock.len; i++) {
2497 struct cal_info *cal = &lrock.cal[i];
2498
2499 /* Send a body chunk once in a while */
2500 if (buf_len(body) > PROT_BUFSIZE) {
2501 write_body(0, txn, buf_cstring(body), buf_len(body));
2502 buf_reset(body);
2503 }
2504
2505 /* Calendar name */
2506 buf_printf_markup(body, level++, "<tr>");
2507 buf_printf_markup(body, level, "<td>%s%s%s",
2508 (cal->flags & CAL_IS_DEFAULT) ? "<b>" : "",
2509 cal->displayname,
2510 (cal->flags & CAL_IS_DEFAULT) ? "</b>" : "");
2511
2512 /* Supported components list */
2513 buf_printf_markup(body, level++, "<td>");
2514 buf_printf_markup(body, level++,
2515 "<select multiple name=comp size=3"
2516 " onChange=\"compsetCalendar('%s%s', '%s', this.options)\">",
2517 base_path, cal->shortname, cal->displayname);
2518 for (comp = cal_comps; comp->name; comp++) {
2519 buf_printf_markup(body, level, "<option%s>%s</option>",
2520 (cal->types & comp->type) ? " selected" : "",
2521 comp->name);
2522 }
2523 buf_printf_markup(body, --level, "</select>");
2524 buf_printf_markup(body, --level, "</td>");
2525
2526 /* Subscribe link */
2527 buf_printf_markup(body, level,
2528 "<td><a href=\"webcal://%s%s%s\">Subscribe</a></td>",
2529 host, base_path, cal->shortname);
2530
2531 /* Download link */
2532 buf_printf_markup(body, level, "<td><a href=\"%s%s\">Download</a></td>",
2533 base_path, cal->shortname);
2534
2535 /* Delete button */
2536 buf_printf_markup(body, level,
2537 "<td><input type=button%s value='Delete'"
2538 " onclick=\"deleteCalendar('%s%s', '%s')\"></td>",
2539 !(cal->flags & CAL_CAN_DELETE) ? " disabled" : "",
2540 base_path, cal->shortname, cal->displayname);
2541
2542 /* Public (shared) checkbox */
2543 buf_printf_markup(body, level,
2544 "<td><input type=checkbox%s%s name=share"
2545 " onclick=\"shareCalendar('%s%s', this.checked)\">"
2546 "Public</td>",
2547 !(cal->flags & CAL_CAN_ADMIN) ? " disabled" : "",
2548 (cal->flags & CAL_IS_PUBLIC) ? " checked" : "",
2549 base_path, cal->shortname);
2550
2551 /* Transparent checkbox */
2552 buf_printf_markup(body, level,
2553 "<td><input type=checkbox%s%s name=transp"
2554 " onclick=\"transpCalendar('%s%s', this.checked)\">"
2555 "Transparent</td>",
2556 !(cal->flags & CAL_CAN_ADMIN) ? " disabled" : "",
2557 (cal->flags & CAL_IS_TRANSP) ? " checked" : "",
2558 base_path, cal->shortname);
2559
2560 buf_printf_markup(body, --level, "</tr>");
2561 }
2562
2563 free(lrock.cal);
2564
2565 /* Finish list */
2566 buf_printf_markup(body, --level, "</table>");
2567
2568 /* Finish HTML */
2569 buf_printf_markup(body, --level, "</body>");
2570 buf_printf_markup(body, --level, "</html>");
2571 write_body(0, txn, buf_cstring(body), buf_len(body));
2572
2573 /* End of output */
2574 write_body(0, txn, NULL, 0);
2575
2576 done:
2577 return ret;
2578 }
2579
2580
2581 /* Parse an RFC 3339 date/time per
2582 http://www.calconnect.org/pubdocs/CD0903%20Freebusy%20Read%20URL.pdf */
icaltime_from_rfc3339_string(const char * str)2583 static struct icaltimetype icaltime_from_rfc3339_string(const char *str)
2584 {
2585 struct icaltimetype tt = icaltime_null_time();
2586 size_t size;
2587
2588 size = strlen(str);
2589
2590 if (size == 20) {
2591 /* UTC */
2592 if (sscanf(str, "%4u-%02u-%02uT%02u:%02u:%02uZ",
2593 &tt.year, &tt.month, &tt.day,
2594 &tt.hour, &tt.minute, &tt.second) < 6) {
2595 goto fail;
2596 }
2597
2598 tt = icaltime_normalize(tt);
2599 }
2600 else if (size == 25) {
2601 /* TZ offset */
2602 int offset_hour, offset_minute;
2603 char offset_sign;
2604
2605 if (sscanf(str, "%4u-%02u-%02uT%02u:%02u:%02u%c%02u:%02u",
2606 &tt.year, &tt.month, &tt.day,
2607 &tt.hour, &tt.minute, &tt.second,
2608 &offset_sign, &offset_hour, &offset_minute) < 9) {
2609 goto fail;
2610 }
2611
2612 if (offset_sign == '-') {
2613 /* negative offset */
2614 offset_hour *= -1;
2615 offset_minute *= -1;
2616 }
2617 else if (offset_sign != '+') {
2618 goto fail;
2619 }
2620
2621 icaltime_adjust(&tt, 0, -offset_hour, -offset_minute, 0);
2622 }
2623 else {
2624 goto fail;
2625 }
2626
2627 icaltime_set_utc(&tt, 1);
2628 return tt;
2629
2630 fail:
2631 return icaltime_null_time();
2632 }
2633
2634
add_personal_data(icalcomponent * ical,struct buf * userdata)2635 static void add_personal_data(icalcomponent *ical, struct buf *userdata)
2636 {
2637 struct dlist *dl;
2638 const char *icalstr;
2639 icalcomponent *vpatch;
2640
2641 /* Parse the value and fetch the patch */
2642 dlist_parsemap(&dl, 1, 0, buf_base(userdata), buf_len(userdata));
2643 dlist_getatom(dl, "VPATCH", &icalstr);
2644 vpatch = icalparser_parse_string(icalstr);
2645 dlist_free(&dl);
2646
2647 /* Apply the patch to the "base" resource */
2648 icalcomponent_apply_vpatch(ical, vpatch, NULL, NULL);
2649
2650 icalcomponent_free(vpatch);
2651 }
2652
2653
caldav_record_to_ical(struct mailbox * mailbox,const struct caldav_data * cdata,const char * userid,strarray_t * schedule_addresses)2654 EXPORTED icalcomponent *caldav_record_to_ical(struct mailbox *mailbox,
2655 const struct caldav_data *cdata,
2656 const char *userid,
2657 strarray_t *schedule_addresses)
2658 {
2659 icalcomponent *ical = NULL;
2660 struct index_record record;
2661 int r;
2662
2663 /* Fetch index record for the cal resource */
2664 r = mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &record);
2665 if (r) return NULL;
2666
2667 ical = record_to_ical(mailbox, &record, schedule_addresses);
2668
2669 if (userid && (namespace_calendar.allow & ALLOW_USERDATA)) {
2670 struct buf userdata = BUF_INITIALIZER;
2671
2672 if (caldav_is_personalized(mailbox, cdata, httpd_userid, &userdata)) {
2673 add_personal_data(ical, &userdata);
2674 }
2675
2676 buf_free(&userdata);
2677 }
2678
2679 return ical;
2680 }
2681
2682
2683 /* Perform a GET/HEAD request on a CalDAV resource */
caldav_get(struct transaction_t * txn,struct mailbox * mailbox,struct index_record * record,void * data,void ** obj)2684 static int caldav_get(struct transaction_t *txn, struct mailbox *mailbox,
2685 struct index_record *record, void *data, void **obj)
2686 {
2687 int r, rights;
2688
2689 if (!(txn->req_tgt.collection || txn->req_tgt.userid))
2690 return HTTP_NO_CONTENT;
2691
2692 /* Check ACL for current user */
2693 rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
2694 if ((rights & DACL_READ) != DACL_READ) {
2695 /* DAV:need-privileges */
2696 txn->error.precond = DAV_NEED_PRIVS;
2697 txn->error.resource = txn->req_tgt.path;
2698 txn->error.rights = DACL_READ;
2699 return HTTP_NO_PRIVS;
2700 }
2701
2702 if (record && record->uid) {
2703 /* GET on a resource */
2704 struct caldav_data *cdata = (struct caldav_data *) data;
2705 unsigned need_tz = 0;
2706 const char **hdr;
2707 icalcomponent *ical = NULL;
2708 int ret = HTTP_CONTINUE;
2709
2710 /* Check for optional CalDAV-Timezones header */
2711 hdr = spool_getheader(txn->req_hdrs, "CalDAV-Timezones");
2712 if (hdr && !strcmp(hdr[0], "T")) need_tz = 1;
2713
2714 if (cdata->comp_flags.tzbyref) {
2715 if (!cdata->organizer && cdata->sched_tag) {
2716 /* Resource has just had VTIMEZONEs stripped -
2717 check if conditional matches previous ETag */
2718
2719 if (check_precond(txn, cdata->sched_tag,
2720 record->internaldate) == HTTP_NOT_MODIFIED) {
2721 /* Fill in previous ETag and don't return Last-Modified */
2722 txn->resp_body.etag = cdata->sched_tag;
2723 txn->resp_body.lastmod = 0;
2724 ret = HTTP_NOT_MODIFIED;
2725 }
2726 }
2727 if (need_tz) {
2728 /* Add VTIMEZONE components for known TZIDs */
2729 *obj = ical = record_to_ical(mailbox, record, NULL);
2730
2731 icalcomponent_add_required_timezones(ical);
2732 }
2733 }
2734 else if (!need_tz && (namespace_calendar.allow & ALLOW_CAL_NOTZ)) {
2735 /* Strip known VTIMEZONEs */
2736 struct caldav_db *caldavdb = caldav_open_mailbox(mailbox);
2737
2738 mailbox_unlock_index(mailbox, NULL);
2739 r = mailbox_lock_index(mailbox, LOCK_EXCLUSIVE);
2740 if (r) {
2741 syslog(LOG_ERR, "relock index(%s) failed: %s",
2742 mailbox->name, error_message(r));
2743 goto done;
2744 }
2745
2746 strarray_t schedule_addresses = STRARRAY_INITIALIZER;
2747
2748 *obj = ical = record_to_ical(mailbox, record, &schedule_addresses);
2749
2750 caldav_store_resource(txn, ical, mailbox,
2751 cdata->dav.resource, cdata->dav.createdmodseq, caldavdb,
2752 TZ_STRIP | (!cdata->sched_tag ? NEW_STAG : 0),
2753 NULL, &schedule_addresses);
2754
2755 strarray_fini(&schedule_addresses);
2756
2757 /* Fetch the new DAV and index records */
2758 /* NOTE: previous contents of cdata was freed by store_resource */
2759 caldav_lookup_resource(caldavdb, mailbox->name,
2760 txn->req_tgt.resource, &cdata, /*tombstones*/0);
2761
2762 mailbox_find_index_record(mailbox, cdata->dav.imap_uid, record);
2763
2764 /* Fill in new ETag and Last-Modified */
2765 txn->resp_body.etag = message_guid_encode(&record->guid);
2766 txn->resp_body.lastmod = record->internaldate;
2767
2768 caldav_close(caldavdb);
2769 }
2770
2771 /* Personalize resource, if necessary */
2772 if (namespace_calendar.allow & ALLOW_USERDATA) {
2773 struct buf userdata = BUF_INITIALIZER;
2774
2775 if (caldav_is_personalized(mailbox, cdata, httpd_userid, &userdata)) {
2776 if (!ical) *obj = ical = record_to_ical(mailbox, record, NULL);
2777
2778 add_personal_data(ical, &userdata);
2779 buf_free(&userdata);
2780 }
2781 }
2782
2783 /* iCalendar data in response should not be transformed */
2784 txn->flags.cc |= CC_NOTRANSFORM;
2785 txn->flags.vary |= VARY_CALTZ;
2786
2787 done:
2788 return ret;
2789 }
2790
2791 if (txn->req_tgt.mbentry->server) {
2792 /* Remote mailbox */
2793 struct backend *be;
2794
2795 be = proxy_findserver(txn->req_tgt.mbentry->server,
2796 &http_protocol, httpd_userid,
2797 &backend_cached, NULL, NULL, httpd_in);
2798 if (!be) return HTTP_UNAVAILABLE;
2799
2800 return http_pipe_req_resp(be, txn);
2801 }
2802
2803 /* Local Mailbox */
2804
2805 if (txn->req_tgt.collection) {
2806 /* Download an entire calendar collection */
2807 return export_calendar(txn);
2808 }
2809 else if (txn->req_tgt.userid &&
2810 config_getswitch(IMAPOPT_CALDAV_ALLOWCALENDARADMIN)) {
2811 /* GET a list of calendars under calendar-home-set */
2812 return list_calendars(txn);
2813 }
2814
2815 /* Unknown action */
2816 return HTTP_NO_CONTENT;
2817 }
2818
2819
2820 /* Perform post-create MKCOL/MKCALENDAR processing */
caldav_mkcol(struct mailbox * mailbox)2821 static int caldav_mkcol(struct mailbox *mailbox)
2822 {
2823 const char *comp_annot =
2824 DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
2825 struct buf attrib = BUF_INITIALIZER;
2826 unsigned long types = 0;
2827 int r;
2828
2829 /* Check if client specified CALDAV:supported-calendar-component-set */
2830 r = annotatemore_lookupmask(mailbox->name, comp_annot,
2831 httpd_userid, &attrib);
2832 if (r) return HTTP_SERVER_ERROR;
2833
2834 if (attrib.len) {
2835 types = strtoul(buf_cstring(&attrib), NULL, 10);
2836 }
2837
2838 if (!types) {
2839 /* Client didn't specify, so use imap.conf option */
2840 annotate_state_t *astate = NULL;
2841
2842 r = mailbox_get_annotate_state(mailbox, 0, &astate);
2843 if (!r) {
2844 types = config_types_to_caldav_types();
2845 buf_reset(&attrib);
2846 buf_printf(&attrib, "%lu", types);
2847
2848 r = annotate_state_writemask(astate, comp_annot,
2849 httpd_userid, &attrib);
2850 }
2851 }
2852
2853 buf_free(&attrib);
2854
2855 return r;
2856 }
2857
2858 /* Perform a GET/HEAD request on a CalDAV/M-Attach resource */
meth_get_head_cal(struct transaction_t * txn,void * params)2859 static int meth_get_head_cal(struct transaction_t *txn, void *params)
2860 {
2861 struct meth_params *gparams = (struct meth_params *) params;
2862 int r;
2863
2864 /* Parse the path */
2865 r = dav_parse_req_target(txn, gparams);
2866 if (r) return r;
2867
2868 if (txn->req_tgt.flags == TGT_MANAGED_ATTACH) gparams = &webdav_params;
2869 return meth_get_head(txn, gparams);
2870 }
2871
2872 /* Decrement reference count on a managed attachment resource */
decrement_refcount(const char * managed_id,struct mailbox * attachments,struct webdav_db * webdavdb)2873 static void decrement_refcount(const char *managed_id,
2874 struct mailbox *attachments,
2875 struct webdav_db *webdavdb)
2876 {
2877 int r;
2878 struct webdav_data *wdata;
2879
2880 /* Find DAV record for the attachment with this managed-id */
2881 webdav_lookup_uid(webdavdb, managed_id, &wdata);
2882
2883 if (!wdata->dav.rowid) return;
2884
2885 if (!--wdata->ref_count) {
2886 /* Delete attachment resource */
2887 struct index_record record;
2888
2889 mailbox_find_index_record(attachments, wdata->dav.imap_uid, &record);
2890 record.internal_flags |= FLAG_INTERNAL_EXPUNGED;
2891
2892 r = mailbox_rewrite_index_record(attachments, &record);
2893
2894 if (r) {
2895 syslog(LOG_ERR, "expunging record (%s) failed: %s",
2896 attachments->name, error_message(r));
2897 }
2898 }
2899 else {
2900 /* Update reference count on WebDAV record */
2901 r = webdav_write(webdavdb, wdata);
2902
2903 if (r) {
2904 syslog(LOG_ERR, "updating ref count (%s) failed: %s",
2905 wdata->dav.resource, error_message(r));
2906 }
2907 }
2908 }
2909
2910 /* Increment reference count on a managed attachment resource */
increment_refcount(const char * managed_id,struct webdav_db * webdavdb)2911 static struct webdav_data *increment_refcount(const char *managed_id,
2912 struct webdav_db *webdavdb)
2913 {
2914 int r;
2915 struct webdav_data *wdata;
2916
2917 /* Find DAV record for the attachment with this managed-id */
2918 webdav_lookup_uid(webdavdb, managed_id, &wdata);
2919
2920 if (wdata->dav.rowid) {
2921 /* Update reference count on WebDAV record */
2922 wdata->ref_count++;
2923 r = webdav_write(webdavdb, wdata);
2924
2925 if (r) {
2926 syslog(LOG_ERR, "updating ref count (%s) failed: %s",
2927 wdata->dav.resource, error_message(r));
2928 }
2929 }
2930
2931 return wdata;
2932 }
2933
2934 /* Manage attachment */
caldav_post_attach(struct transaction_t * txn,int rights)2935 static int caldav_post_attach(struct transaction_t *txn, int rights)
2936 {
2937 int ret = 0, r, precond;
2938 struct resp_body_t *resp_body = &txn->resp_body;
2939 struct strlist *action, *mid, *rid;
2940 struct mime_type_t *mime = NULL;
2941 struct mailbox *calendar = NULL, *attachments = NULL;
2942 struct caldav_db *caldavdb = NULL;
2943 struct caldav_data *cdata;
2944 struct webdav_db *webdavdb = NULL;
2945 struct webdav_data *wdata;
2946 struct index_record record;
2947 strarray_t schedule_addresses = STRARRAY_INITIALIZER;
2948 const char *etag = NULL, **hdr;
2949 time_t lastmod = 0;
2950 icalcomponent *ical = NULL, *comp, *nextc, *master = NULL;
2951 icalcomponent_kind kind;
2952 icalproperty *aprop = NULL, *prop;
2953 icalparameter *param;
2954 unsigned op, return_rep;
2955 strarray_t *rids = NULL;
2956 enum {
2957 ATTACH_ADD,
2958 ATTACH_UPDATE,
2959 ATTACH_REMOVE
2960 };
2961
2962 if (!(namespace_calendar.allow & ALLOW_CAL_ATTACH)) return HTTP_NOT_ALLOWED;
2963
2964 /* Check ACL for current user */
2965 if (!(rights & DACL_WRITECONT)) {
2966 /* DAV:need-privileges */
2967 txn->error.precond = DAV_NEED_PRIVS;
2968 txn->error.resource = txn->req_tgt.path;
2969 txn->error.rights = DACL_WRITECONT;
2970 return HTTP_NO_PRIVS;
2971 }
2972
2973 if ((return_rep = (get_preferences(txn) & PREFER_REP))) {
2974 /* Check requested MIME type:
2975 1st entry in gparams->mime_types array MUST be default MIME type */
2976 if ((hdr = spool_getheader(txn->req_hdrs, "Accept")))
2977 mime = get_accept_type(hdr, caldav_mime_types);
2978 else mime = caldav_mime_types;
2979 if (!mime) return HTTP_NOT_ACCEPTABLE;
2980 }
2981
2982 /* Fetch and sanity check parameters */
2983 action = hash_lookup("action", &txn->req_qparams);
2984 mid = hash_lookup("managed-id", &txn->req_qparams);
2985 rid = hash_lookup("rid", &txn->req_qparams);
2986
2987 if (!action || action->next) return HTTP_BAD_REQUEST;
2988 else if (!strcmp(action->s, "attachment-add")) {
2989 op = ATTACH_ADD;
2990 if (mid) return HTTP_BAD_REQUEST;
2991 }
2992 else if (!strcmp(action->s, "attachment-update")) {
2993 op = ATTACH_UPDATE;
2994 if (rid || !mid || mid->next) return HTTP_BAD_REQUEST;
2995 }
2996 else if (!strcmp(action->s, "attachment-remove")) {
2997 op = ATTACH_REMOVE;
2998 if (!mid || mid->next) return HTTP_BAD_REQUEST;
2999 }
3000 else return HTTP_BAD_REQUEST;
3001
3002 /* Open calendar for writing */
3003 r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &calendar);
3004 if (r) {
3005 syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
3006 txn->req_tgt.mbentry->name, error_message(r));
3007 txn->error.desc = error_message(r);
3008 ret = HTTP_SERVER_ERROR;
3009 goto done;
3010 }
3011
3012 /* Open the CalDAV DB corresponding to the calendar */
3013 caldavdb = caldav_open_mailbox(calendar);
3014
3015 /* Find message UID for the cal resource */
3016 caldav_lookup_resource(caldavdb, txn->req_tgt.mbentry->name,
3017 txn->req_tgt.resource, &cdata, 0);
3018 if (!cdata->dav.rowid) ret = HTTP_NOT_FOUND;
3019 else if (!cdata->dav.imap_uid) ret = HTTP_CONFLICT;
3020 if (ret) goto done;
3021
3022 /* Fetch index record for the cal resource */
3023 memset(&record, 0, sizeof(struct index_record));
3024 r = mailbox_find_index_record(calendar, cdata->dav.imap_uid, &record);
3025 if (r) {
3026 txn->error.desc = error_message(r);
3027 ret = HTTP_SERVER_ERROR;
3028 goto done;
3029 }
3030
3031 etag = message_guid_encode(&record.guid);
3032 lastmod = record.internaldate;
3033
3034 /* Load and parse message containing the resource */
3035 ical = record_to_ical(calendar, &record, &schedule_addresses);
3036 comp = icalcomponent_get_first_real_component(ical);
3037 kind = icalcomponent_isa(comp);
3038
3039 /* Check any preconditions */
3040 precond = caldav_check_precond(txn, &caldav_params,
3041 calendar, cdata, etag, lastmod);
3042
3043 switch (precond) {
3044 case HTTP_OK:
3045 break;
3046
3047 case HTTP_LOCKED:
3048 txn->error.precond = DAV_NEED_LOCK_TOKEN;
3049 txn->error.resource = txn->req_tgt.path;
3050
3051 GCC_FALLTHROUGH
3052
3053 default:
3054 /* We failed a precondition - don't perform the request */
3055 ret = precond;
3056
3057 if ((precond == HTTP_PRECOND_FAILED) && return_rep) goto return_rep;
3058 else goto done;
3059 }
3060
3061 /* Open attachments collection and its DAV DB for writing */
3062 ret = open_attachments(httpd_userid, &attachments, &webdavdb);
3063 if (ret) goto done;
3064
3065 if (mid) {
3066 /* Locate first ATTACH property with this MANAGED-ID */
3067 do {
3068 for (aprop = icalcomponent_get_first_property(comp,
3069 ICAL_ATTACH_PROPERTY);
3070 aprop;
3071 aprop = icalcomponent_get_next_property(comp,
3072 ICAL_ATTACH_PROPERTY)) {
3073 param = icalproperty_get_managedid_parameter(aprop);
3074 if (param &&
3075 !strcmp(mid->s, icalparameter_get_managedid(param))) break;
3076 }
3077
3078 /* Check if this is master component */
3079 if (rid && !master &&
3080 !icalcomponent_get_first_property(comp,
3081 ICAL_RECURRENCEID_PROPERTY)) {
3082 master = comp;
3083 }
3084
3085 } while (!aprop &&
3086 (comp = icalcomponent_get_next_component(ical, kind)));
3087
3088 if (!aprop) {
3089 txn->error.precond = CALDAV_VALID_MANAGEDID;
3090 ret = HTTP_BAD_REQUEST;
3091 goto done;
3092 }
3093
3094 /* Update reference count */
3095 decrement_refcount(mid->s, attachments, webdavdb);
3096 }
3097
3098 if (op == ATTACH_REMOVE) aprop = NULL;
3099 else {
3100 /* SHA1 of content used as resource UID, resource name, & managed-id */
3101 static char uid[2*MESSAGE_GUID_SIZE+1];
3102 struct message_guid guid;
3103
3104 /* Read body */
3105 txn->req_body.flags |= BODY_DECODE;
3106 r = http_read_req_body(txn);
3107 if (r) {
3108 txn->flags.conn = CONN_CLOSE;
3109 return r;
3110 }
3111
3112 /* Make sure we have a body */
3113 if (!buf_len(&txn->req_body.payload)) {
3114 txn->error.desc = "Missing request body";
3115 ret = HTTP_BAD_REQUEST;
3116 goto done;
3117 }
3118
3119 /* Generate UID of body content */
3120 message_guid_generate(&guid, buf_base(&txn->req_body.payload),
3121 buf_len(&txn->req_body.payload));
3122 strcpy(uid, message_guid_encode(&guid));
3123
3124 /* Store the new/updated attachment using WebDAV callback */
3125 ret = webdav_params.put.proc(txn, &txn->req_body.payload,
3126 attachments, uid, webdavdb, 0);
3127
3128 switch (ret) {
3129 case HTTP_CREATED:
3130 case HTTP_NO_CONTENT:
3131 resp_body->cmid = uid;
3132 resp_body->etag = NULL;
3133 break;
3134
3135 default:
3136 goto done;
3137 break;
3138 }
3139
3140 /* Update reference count */
3141 wdata = increment_refcount(uid, webdavdb);
3142
3143 /* Create new ATTACH property */
3144 const char *proto = NULL, *host = NULL;
3145 icalattach *attach;
3146
3147 assert(!buf_len(&txn->buf));
3148 http_proto_host(txn->req_hdrs, &proto, &host);
3149 buf_printf(&txn->buf, "%s://%s%s/%s/%s/%s%s",
3150 proto, host, namespace_calendar.prefix,
3151 USER_COLLECTION_PREFIX,
3152 txn->req_tgt.userid, MANAGED_ATTACH, uid);
3153 attach = icalattach_new_from_url(buf_cstring(&txn->buf));
3154 buf_reset(&txn->buf);
3155
3156 aprop = icalproperty_new_attach(attach);
3157 icalattach_unref(attach);
3158
3159 /* Update ATTACH parameters - MANAGED-ID, FILENAME, SIZE, FMTTYPE */
3160 param = icalproperty_get_managedid_parameter(aprop);
3161 if (param) icalparameter_set_managedid(param, resp_body->cmid);
3162 else {
3163 param = icalparameter_new_managedid(resp_body->cmid);
3164 icalproperty_add_parameter(aprop, param);
3165 }
3166
3167 if (wdata->filename) {
3168 param = icalproperty_get_filename_parameter(aprop);
3169 if (param) icalparameter_set_filename(param, wdata->filename);
3170 else {
3171 param = icalparameter_new_filename(wdata->filename);
3172 icalproperty_add_parameter(aprop, param);
3173 }
3174 }
3175
3176 assert(!buf_len(&txn->buf));
3177 buf_printf(&txn->buf, "%tu", buf_len(&txn->req_body.payload));
3178 param = icalproperty_get_size_parameter(aprop);
3179 if (param) icalparameter_set_size(param, buf_cstring(&txn->buf));
3180 else {
3181 param = icalparameter_new_size(buf_cstring(&txn->buf));
3182 icalproperty_add_parameter(aprop, param);
3183 }
3184 buf_reset(&txn->buf);
3185
3186 if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) {
3187 param = icalproperty_get_first_parameter(aprop,
3188 ICAL_FMTTYPE_PARAMETER);
3189 if (param) icalparameter_set_fmttype(param, *hdr);
3190 else {
3191 param = icalparameter_new_fmttype(*hdr);
3192 icalproperty_add_parameter(aprop, param);
3193 }
3194 }
3195 }
3196
3197 if (rid) {
3198 /* Split list of RECURRENCE-IDs */
3199 rids = strarray_split(rid->s, ",", STRARRAY_TRIM);
3200 }
3201
3202 /* Process each component */
3203 for (; comp; comp = nextc) {
3204 int idx;
3205
3206 nextc = icalcomponent_get_next_component(ical, kind);
3207
3208 if (rid) {
3209 /* Check if RECURRENCE-ID is in our list */
3210 const char *recurid;
3211
3212 prop = icalcomponent_get_first_property(comp,
3213 ICAL_RECURRENCEID_PROPERTY);
3214 if (prop) recurid = icalproperty_get_value_as_string(prop);
3215 else {
3216 master = comp;
3217 recurid = "M";
3218 }
3219
3220 idx = strarray_find_case(rids, recurid, 0);
3221 if (idx >= 0) {
3222 /* Remove found recurid from list -
3223 we will create new overrides for unfound recurids */
3224 free(strarray_remove(rids, idx));
3225 }
3226 else if (!nextc && strarray_size(rids)) {
3227 /* Create new overrides */
3228 struct icaldatetimeperiodtype dtp;
3229 icalproperty *nextp;
3230
3231 master = icalcomponent_clone(master);
3232
3233 /* Get DTSTART and Remove unwanted recurrence properties */
3234 for (prop = icalcomponent_get_first_property(master,
3235 ICAL_ANY_PROPERTY);
3236 prop; prop = nextp) {
3237 nextp = icalcomponent_get_next_property(master,
3238 ICAL_ANY_PROPERTY);
3239 switch (icalproperty_isa(prop)) {
3240 case ICAL_RRULE_PROPERTY:
3241 case ICAL_RDATE_PROPERTY:
3242 case ICAL_EXDATE_PROPERTY:
3243 case ICAL_EXRULE_PROPERTY:
3244 icalcomponent_remove_property(master, prop);
3245 icalproperty_free(prop);
3246 break;
3247
3248 case ICAL_DTSTART_PROPERTY:
3249 dtp = icalproperty_get_datetimeperiod(prop);
3250 break;
3251
3252 default:
3253 break;
3254 }
3255 }
3256
3257 /* Get TZID of DTSTART */
3258 struct icaltimetype dtstart = dtp.time;
3259 const icaltimezone *tz = icaltime_get_timezone(dtstart);
3260 const char *tzid = icaltimezone_get_tzid((icaltimezone *) tz);
3261
3262 for (idx = 0; idx < strarray_size(rids); idx++) {
3263 /* Create new component and set DTSTART and RECURRENCE-ID */
3264 dtstart = icaltime_from_string(strarray_nth(rids, idx));
3265 if (icaltime_is_null_time(dtstart)) continue;
3266
3267 icaltime_set_timezone(&dtstart, tz);
3268
3269 comp = icalcomponent_clone(master);
3270 icalcomponent_add_component(ical, comp);
3271 icalcomponent_set_dtstart(comp, dtstart);
3272
3273 prop = icalproperty_new_recurrenceid(dtstart);
3274 icalcomponent_add_property(comp, prop);
3275 if (tzid) {
3276 icalproperty_add_parameter(prop,
3277 icalparameter_new_tzid(tzid));
3278 }
3279 }
3280
3281 icalcomponent_free(master);
3282 nextc = icalcomponent_get_next_component(ical, kind);
3283 rid = NULL;
3284 }
3285 else {
3286 /* No matching RECURRENCE-ID - Skip this component */
3287 continue;
3288 }
3289 }
3290
3291 if (mid) {
3292 /* Remove matching ATTACH property */
3293 for (prop = icalcomponent_get_first_property(comp,
3294 ICAL_ATTACH_PROPERTY);
3295 prop;
3296 prop = icalcomponent_get_next_property(comp,
3297 ICAL_ATTACH_PROPERTY)) {
3298 param = icalproperty_get_managedid_parameter(prop);
3299 if (param &&
3300 !strcmp(mid->s, icalparameter_get_managedid(param))) {
3301 icalcomponent_remove_property(comp, prop);
3302 icalproperty_free(prop);
3303 break;
3304 }
3305 }
3306
3307 if (!prop) {
3308 /* No matching ATTACH - Skip this component */
3309 continue;
3310 }
3311 }
3312
3313 if (aprop) {
3314 /* Add new/updated ATTACH property */
3315 icalcomponent_add_property(comp, icalproperty_clone(aprop));
3316 }
3317 }
3318
3319 /* Finished with attachment collection */
3320 mailbox_unlock_index(attachments, NULL);
3321
3322 /* Store updated calendar resource */
3323 ret = caldav_store_resource(txn, ical, calendar, txn->req_tgt.resource,
3324 record.createdmodseq,
3325 caldavdb, return_rep, NULL, &schedule_addresses);
3326
3327 if (ret == HTTP_NO_CONTENT) {
3328 if (aprop) {
3329 buf_setcstr(&txn->buf, icalproperty_get_value_as_string(aprop));
3330 txn->location = buf_cstring(&txn->buf);
3331 }
3332
3333 if (return_rep) {
3334 struct buf *data;
3335
3336 ret = (op == ATTACH_ADD) ? HTTP_CREATED : HTTP_OK;
3337
3338 return_rep:
3339 /* Convert into requested MIME type */
3340 data = mime->from_object(ical);
3341
3342 /* Fill in Content-Type, Content-Length */
3343 resp_body->type = mime->content_type;
3344 resp_body->len = buf_len(data);
3345
3346 /* Fill in Content-Location */
3347 resp_body->loc = txn->req_tgt.path;
3348
3349 /* Fill in Expires and Cache-Control */
3350 resp_body->maxage = 3600; /* 1 hr */
3351 txn->flags.cc = CC_MAXAGE
3352 | CC_REVALIDATE /* don't use stale data */
3353 | CC_NOTRANSFORM; /* don't alter iCal data */
3354
3355 /* Output current representation */
3356 write_body(ret, txn, buf_base(data), buf_len(data));
3357
3358 buf_destroy(data);
3359 ret = 0;
3360 }
3361 }
3362
3363 done:
3364 strarray_free(rids);
3365 strarray_fini(&schedule_addresses);
3366 if (aprop) icalproperty_free(aprop);
3367 if (ical) icalcomponent_free(ical);
3368 if (caldavdb) caldav_close(caldavdb);
3369 if (webdavdb) webdav_close(webdavdb);
3370 mailbox_close(&attachments);
3371 mailbox_close(&calendar);
3372
3373 return ret;
3374 }
3375
3376
3377 /* Perform a busy time request */
caldav_post_outbox(struct transaction_t * txn,int rights)3378 static int caldav_post_outbox(struct transaction_t *txn, int rights)
3379 {
3380 int ret = 0, r;
3381 const char **hdr;
3382 struct mime_type_t *mime = NULL;
3383 icalcomponent *ical = NULL, *comp;
3384 icalcomponent_kind kind = 0;
3385 icalproperty_method meth = 0;
3386 icalproperty *prop = NULL;
3387 const char *uid = NULL, *organizer = NULL;
3388 struct caldav_sched_param sparam;
3389
3390 /* Check Content-Type */
3391 if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) {
3392 for (mime = caldav_mime_types; mime->content_type; mime++) {
3393 if (is_mediatype(mime->content_type, hdr[0])) break;
3394 }
3395 }
3396 if (!mime || !mime->content_type) {
3397 txn->error.precond = CALDAV_SUPP_DATA;
3398 return HTTP_BAD_REQUEST;
3399 }
3400
3401 /* Read body */
3402 txn->req_body.flags |= BODY_DECODE;
3403 r = http_read_req_body(txn);
3404 if (r) {
3405 txn->flags.conn = CONN_CLOSE;
3406 return r;
3407 }
3408
3409 /* Make sure we have a body */
3410 if (!buf_len(&txn->req_body.payload)) {
3411 txn->error.desc = "Missing request body\r\n";
3412 return HTTP_BAD_REQUEST;
3413 }
3414
3415 /* Parse the iCal data for important properties */
3416 ical = mime->to_object(&txn->req_body.payload);
3417 if (!ical || !icalrestriction_check(ical)) {
3418 txn->error.precond = CALDAV_VALID_DATA;
3419 ret = HTTP_BAD_REQUEST;
3420 goto done;
3421 }
3422
3423 meth = icalcomponent_get_method(ical);
3424 comp = icalcomponent_get_first_real_component(ical);
3425 if (comp) {
3426 uid = icalcomponent_get_uid(comp);
3427 kind = icalcomponent_isa(comp);
3428 prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
3429 }
3430
3431 /* Check method preconditions */
3432 if (!meth || !uid || !prop) {
3433 txn->error.precond = CALDAV_VALID_SCHED;
3434 ret = HTTP_BAD_REQUEST;
3435 goto done;
3436 }
3437
3438 /* Organizer MUST be local to use CalDAV Scheduling */
3439 organizer = icalproperty_get_organizer(prop);
3440 if (!organizer) {
3441 txn->error.precond = CALDAV_VALID_ORGANIZER;
3442 ret = HTTP_FORBIDDEN;
3443 goto done;
3444 }
3445 r = caladdress_lookup(organizer, &sparam, NULL); // XXX: this needs to lookup the user's principal
3446 if (r) {
3447 txn->error.precond = CALDAV_VALID_ORGANIZER;
3448 ret = HTTP_FORBIDDEN;
3449 goto done;
3450 }
3451 if (!sparam.isyou) {
3452 sched_param_fini(&sparam);
3453 txn->error.precond = CALDAV_VALID_ORGANIZER;
3454 ret = HTTP_FORBIDDEN;
3455 goto done;
3456 }
3457 sched_param_fini(&sparam);
3458
3459 switch (kind) {
3460 case ICAL_VFREEBUSY_COMPONENT:
3461 if (meth == ICAL_METHOD_REQUEST)
3462 if (!(rights & DACL_SCHEDFB)) {
3463 /* DAV:need-privileges */
3464 txn->error.precond = DAV_NEED_PRIVS;
3465 txn->error.resource = txn->req_tgt.path;
3466 txn->error.rights = DACL_SCHEDFB;
3467 ret = HTTP_NO_PRIVS;
3468 }
3469 else ret = sched_busytime_query(txn, mime, ical);
3470 else {
3471 txn->error.precond = CALDAV_VALID_SCHED;
3472 ret = HTTP_BAD_REQUEST;
3473 }
3474 break;
3475
3476 default:
3477 txn->error.precond = CALDAV_VALID_SCHED;
3478 ret = HTTP_BAD_REQUEST;
3479 }
3480
3481 done:
3482 if (ical) icalcomponent_free(ical);
3483
3484 return ret;
3485 }
3486
3487
3488 struct import_rock {
3489 struct transaction_t *txn;
3490 icalcomponent *ical;
3491 struct mailbox *mailbox;
3492 struct caldav_db *caldavdb;
3493 xmlNodePtr root;
3494 xmlNsPtr *ns;
3495 unsigned flags;
3496
3497 ptrarray_t *props;
3498 size_t baselen;
3499 xmlBufferPtr xmlbuf;
3500 };
3501
3502
import_resource(const char * uid,void * data,void * rock)3503 static void import_resource(const char *uid, void *data, void *rock)
3504 {
3505 ptrarray_t *comps = (ptrarray_t *) data;
3506 struct import_rock *irock = (struct import_rock *) rock;
3507 struct transaction_t *txn = irock->txn;
3508 xmlNodePtr root = irock->root, resp, node;
3509 xmlNsPtr *ns = irock->ns;
3510 xmlBufferPtr *xmlbuf = &irock->xmlbuf;
3511 icalcomponent *newical;
3512 int i, r;
3513
3514 /* Create DAV:response element */
3515 resp = xmlNewChild(root, ns[NS_DAV], BAD_CAST "response", NULL);
3516 if (!resp) {
3517 syslog(LOG_ERR,
3518 "import_resource()): Unable to add response XML element");
3519 fatal("import_resource()): Unable to add response XML element",
3520 EX_SOFTWARE);
3521 }
3522
3523 /* Create new object, making copies of PRODID, VERSION, CALSCALE */
3524 newical = icalcomponent_new(ICAL_VCALENDAR_COMPONENT);
3525
3526 for (i = 0; i < ptrarray_size(irock->props); i++) {
3527 icalproperty *newprop =
3528 icalproperty_clone(ptrarray_nth(irock->props, i));
3529
3530 icalcomponent_add_property(newical, newprop);
3531 }
3532
3533 /* Add component in recurrence set */
3534 for (i = 0; i < ptrarray_size(comps); i++) {
3535 struct timezone_rock tzrock = { irock->ical, newical };
3536 icalcomponent *comp = ptrarray_nth(comps, i);
3537
3538 icalcomponent_add_component(newical, comp);
3539
3540 /* Add required timezone components */
3541 icalcomponent_foreach_tzid(comp, &add_timezone, &tzrock);
3542 }
3543
3544 /* Append a unique resource name to URL and perform a PUT */
3545 txn->req_tgt.reslen =
3546 snprintf(txn->req_tgt.resource, MAX_MAILBOX_PATH - irock->baselen,
3547 "%s.ics", makeuuid());
3548
3549 r = caldav_put(txn, newical, irock->mailbox,
3550 txn->req_tgt.resource, irock->caldavdb, irock->flags);
3551
3552 switch (r) {
3553 case HTTP_OK:
3554 case HTTP_CREATED:
3555 case HTTP_NO_CONTENT:
3556 /* Success: Add DAV:href and DAV:propstat elements */
3557 xml_add_href(resp, NULL, txn->req_tgt.path);
3558
3559 node = xmlNewChild(resp, ns[NS_DAV], BAD_CAST "propstat", NULL);
3560 xmlNewChild(node, ns[NS_DAV], BAD_CAST "status",
3561 BAD_CAST http_statusline(VER_1_1, HTTP_OK));
3562
3563 node = xmlNewChild(node, ns[NS_DAV], BAD_CAST "prop", NULL);
3564
3565 if (txn->resp_body.etag) {
3566 /* Add DAV:getetag property */
3567 xmlNewTextChild(node, ns[NS_DAV], BAD_CAST "getetag",
3568 BAD_CAST txn->resp_body.etag);
3569 }
3570 if ((irock->flags & PREFER_REP) &&
3571 icalcomponent_get_first_property(ptrarray_nth(comps, 0),
3572 ICAL_ORGANIZER_PROPERTY)) {
3573 /* Add CALDAV:calendar-data property */
3574 const char *icalstr = icalcomponent_as_ical_string(newical);
3575 xmlNodePtr cdata = xmlNewChild(node, ns[NS_CALDAV],
3576 BAD_CAST "calendar-data", NULL);
3577
3578 xmlAddChild(cdata, xmlNewCDataBlock(root->doc, BAD_CAST icalstr,
3579 strlen(icalstr)));
3580 }
3581 break;
3582
3583 default:
3584 /* Failure: Add DAV:href, DAV:status, and DAV:error elements */
3585 xml_add_href(resp, NULL, NULL);
3586
3587 xmlNewChild(resp, ns[NS_DAV], BAD_CAST "status",
3588 BAD_CAST http_statusline(VER_1_1, r));
3589
3590 node = xml_add_error(resp, &txn->error, ns);
3591 break;
3592 }
3593
3594 /* Add CS:uid property */
3595 xmlNewTextChild(node, ns[NS_CS], BAD_CAST "uid", BAD_CAST uid);
3596
3597 /* Add DAV:response element for this resource to output buffer.
3598 Only output the xmlBuffer every PROT_BUFSIZE bytes */
3599 xml_partial_response((xmlBufferLength(*xmlbuf) > PROT_BUFSIZE) ? txn : NULL,
3600 root->doc, resp, 1, xmlbuf);
3601
3602 /* Remove DAV:response element from root (no need to keep in memory) */
3603 xmlReplaceNode(resp, NULL);
3604 xmlFreeNode(resp);
3605
3606 icalcomponent_free(newical);
3607 ptrarray_free(comps);
3608 }
3609
3610
3611 /* Perform a bulk import */
caldav_import(struct transaction_t * txn,void * obj,struct mailbox * mailbox,void * destdb,xmlNodePtr root,xmlNsPtr * ns,unsigned flags)3612 static int caldav_import(struct transaction_t *txn, void *obj,
3613 struct mailbox *mailbox, void *destdb,
3614 xmlNodePtr root, xmlNsPtr *ns, unsigned flags)
3615 {
3616 struct hash_table comp_table = HASH_TABLE_INITIALIZER;
3617 icalcomponent *ical = obj, *comp, *next;
3618 icalproperty *prop;
3619 const char *uid;
3620 ptrarray_t *comps;
3621 struct import_rock irock = { txn, ical, mailbox, destdb, root, ns, flags,
3622 NULL/*props*/, 0/*baselen*/, NULL/*xmlbuf*/ };
3623
3624 if (!root) {
3625 /* Validate the iCal data */
3626 if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) {
3627 txn->error.precond = CALDAV_VALID_DATA;
3628 return HTTP_FORBIDDEN;
3629 }
3630 cyrus_icalrestriction_check(ical);
3631 if ((txn->error.desc = get_icalcomponent_errstr(ical))) {
3632 buf_setcstr(&txn->buf, txn->error.desc);
3633 txn->error.desc = buf_cstring(&txn->buf);
3634 txn->error.precond = CALDAV_VALID_DATA;
3635 return HTTP_FORBIDDEN;
3636 }
3637
3638 /* Make sure each "real" component has a UID */
3639 for (comp = icalcomponent_get_first_component(ical, ICAL_ANY_COMPONENT);
3640 comp;
3641 comp = icalcomponent_get_next_component(ical, ICAL_ANY_COMPONENT)) {
3642
3643 icalcomponent_kind kind = icalcomponent_isa(comp);
3644
3645 if (kind == ICAL_VTIMEZONE_COMPONENT) continue;
3646
3647 uid = icalcomponent_get_uid(comp);
3648 if (!uid) {
3649 buf_reset(&txn->buf);
3650 buf_printf(&txn->buf, "Missing UID property in %s",
3651 icalcomponent_kind_to_string(kind));
3652 txn->error.desc = buf_cstring(&txn->buf);
3653 txn->error.precond = CALDAV_VALID_DATA;
3654 return HTTP_FORBIDDEN;
3655 }
3656 }
3657
3658 return 0;
3659 }
3660
3661 /* Fetch important properties from VCALENDAR */
3662 irock.props = ptrarray_new();
3663 for (prop = icalcomponent_get_first_property(ical, ICAL_ANY_PROPERTY);
3664 prop; prop = icalcomponent_get_next_property(ical, ICAL_ANY_PROPERTY)) {
3665
3666 switch (icalproperty_isa(prop)) {
3667 case ICAL_CALSCALE_PROPERTY:
3668 case ICAL_PRODID_PROPERTY:
3669 case ICAL_VERSION_PROPERTY:
3670 ptrarray_append(irock.props, prop);
3671 break;
3672
3673 default:
3674 break;
3675 }
3676 }
3677
3678 /* Setup for appending resource name to request path */
3679 irock.baselen = strlen(txn->req_tgt.path);
3680 txn->req_tgt.resource = txn->req_tgt.path + irock.baselen;
3681
3682 /* Create hash table of components */
3683 construct_hash_table(&comp_table, 100, 1);
3684
3685 /* Group components by UID (recurrence sets) */
3686 for (comp = icalcomponent_get_first_component(ical, ICAL_ANY_COMPONENT);
3687 comp; comp = next) {
3688
3689 next = icalcomponent_get_next_component(ical, ICAL_ANY_COMPONENT);
3690
3691 if (icalcomponent_isa(comp) == ICAL_VTIMEZONE_COMPONENT) continue;
3692
3693 icalcomponent_remove_component(ical, comp);
3694
3695 uid = icalcomponent_get_uid(comp);
3696 comps = hash_lookup(uid, &comp_table);
3697
3698 if (!comps) {
3699 /* Haven't seen this UID yet - create new recurrence set */
3700 comps = ptrarray_new();
3701 hash_insert(uid, comps, &comp_table);
3702 }
3703
3704 /* Add component to recurrence set */
3705 if (icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
3706 ptrarray_append(comps, comp);
3707 }
3708 else {
3709 /* Master component - always place first */
3710 ptrarray_insert(comps, 0, comp);
3711 }
3712 }
3713
3714 /* Process the recurrence sets */
3715 hash_enumerate(&comp_table, &import_resource, &irock);
3716 free_hash_table(&comp_table, NULL);
3717 ptrarray_free(irock.props);
3718
3719 /* End XML response */
3720 xml_partial_response(txn, root->doc, NULL /* end */, 0, &irock.xmlbuf);
3721 xmlBufferFree(irock.xmlbuf);
3722
3723 return 0;
3724 }
3725
3726
caldav_post(struct transaction_t * txn)3727 static int caldav_post(struct transaction_t *txn)
3728 {
3729 int ret, rights;
3730
3731 /* Get rights for current user */
3732 rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
3733
3734 if (txn->req_tgt.resource) {
3735 if (txn->req_tgt.flags) {
3736 /* Don't allow POST on resources in special collections */
3737 ret = HTTP_NOT_ALLOWED;
3738 }
3739 else if (txn->req_tgt.mbentry->server) {
3740 /* Remote mailbox */
3741 struct backend *be;
3742
3743 be = proxy_findserver(txn->req_tgt.mbentry->server,
3744 &http_protocol, httpd_userid,
3745 &backend_cached, NULL, NULL, httpd_in);
3746 if (!be) ret = HTTP_UNAVAILABLE;
3747 else ret = http_pipe_req_resp(be, txn);
3748 }
3749 else {
3750 /* Local Mailbox */
3751 ret = caldav_post_attach(txn, rights);
3752 }
3753 }
3754 else if (txn->req_tgt.flags == TGT_SCHED_OUTBOX) {
3755 /* POST to schedule-outbox */
3756 ret = caldav_post_outbox(txn, rights);
3757 }
3758 else if (txn->req_tgt.flags) {
3759 /* Don't allow POST to special collections */
3760 ret = HTTP_NOT_ALLOWED;
3761 }
3762 else if (!txn->req_tgt.collection) {
3763 /* POST to calendar-home-set */
3764 ret = notify_post(txn);
3765 }
3766 else {
3767 /* POST to regular calendar collection */
3768 ret = HTTP_CONTINUE;
3769 }
3770
3771 return ret;
3772 }
3773
3774
3775 /* Perform a PATCH request
3776 *
3777 * preconditions:
3778 */
caldav_patch(struct transaction_t * txn,void * obj)3779 static int caldav_patch(struct transaction_t *txn, void *obj)
3780 {
3781 icalcomponent *ical = (icalcomponent *) obj;
3782 icalcomponent *pdoc, *vpatch;
3783 icalproperty *prop;
3784 int num_changes = 0;
3785 int ret = 0;
3786
3787 /* Validate the iCal patch */
3788 pdoc = ical_string_as_icalcomponent(&txn->req_body.payload);
3789 if (!pdoc || (icalcomponent_isa(pdoc) != ICAL_VCALENDAR_COMPONENT)) {
3790 txn->error.desc = "Missing VCALENDAR";
3791 txn->error.precond = CALDAV_VALID_DATA;
3792 ret = HTTP_BAD_REQUEST;
3793 }
3794 else if (!icalrestriction_check(pdoc) || icalcomponent_count_errors(pdoc)) {
3795 if ((txn->error.desc = get_icalcomponent_errstr(pdoc)) ||
3796 (txn->error.desc =
3797 get_icalcomponent_errstr(icalcomponent_get_first_real_component(pdoc)))) {
3798 buf_setcstr(&txn->buf, txn->error.desc);
3799 txn->error.desc = buf_cstring(&txn->buf);
3800 }
3801 else txn->error.desc = "Error in VPATCH";
3802 txn->error.precond = CALDAV_VALID_DATA;
3803 ret = HTTP_BAD_REQUEST;
3804 }
3805 else if (!(vpatch = icalcomponent_get_first_real_component(pdoc)) ||
3806 icalcomponent_isa(vpatch) != ICAL_VPATCH_COMPONENT) {
3807 txn->error.desc = "Missing VPATCH";
3808 txn->error.precond = CALDAV_VALID_DATA;
3809 ret = HTTP_BAD_REQUEST;
3810 }
3811 else if ((prop =
3812 icalcomponent_get_first_property(vpatch,
3813 ICAL_PATCHVERSION_PROPERTY)) &&
3814 strcmp(icalproperty_get_patchversion(prop), "1")) {
3815 txn->error.desc = "Unsupported PATCH-VERSION";
3816 txn->error.precond = CALDAV_SUPP_DATA;
3817 ret = HTTP_BAD_REQUEST;
3818 }
3819
3820 if (!ret) ret = icalcomponent_apply_vpatch(ical, vpatch,
3821 &num_changes, &txn->error.desc);
3822
3823 icalcomponent_free(pdoc);
3824
3825 if (ret) return ret;
3826 else if (!num_changes) {
3827 /* If no changes are made,
3828 return HTTP_NO_CONTENT to suppress storing of resource */
3829 return HTTP_NO_CONTENT;
3830 }
3831 else return 0;
3832 }
3833
3834
3835 /*
3836 * Compare two components and extract per-user data (alarms, transparency).
3837 *
3838 * NOTE: This function assumes that both components has been normalized
3839 */
extract_personal_data(icalcomponent * ical,icalcomponent * oldical,icalcomponent * vpatch,struct buf * path,int read_only,unsigned * num_changes)3840 static int extract_personal_data(icalcomponent *ical, icalcomponent *oldical,
3841 icalcomponent *vpatch, struct buf *path,
3842 int read_only, unsigned *num_changes)
3843 {
3844 icalcomponent *comp, *nextcomp, *oldcomp = NULL, *patch = NULL;
3845 icalproperty *prop, *nextprop, *oldprop = NULL;
3846 int r;
3847
3848 /* Add this component to path */
3849 size_t path_len = buf_len(path);
3850 buf_printf(path, "/%s",
3851 icalcomponent_kind_to_string(icalcomponent_isa(ical)));
3852
3853 prop = icalcomponent_get_first_property(ical, ICAL_UID_PROPERTY);
3854 if (prop) {
3855 buf_printf(path, "[UID=%s]", icalproperty_get_uid(prop));
3856 prop = icalcomponent_get_first_property(ical,
3857 ICAL_RECURRENCEID_PROPERTY);
3858 buf_printf(path, "[RID=%s]",
3859 prop ? icalproperty_get_value_as_string(prop) : "M");
3860 }
3861
3862 if (oldical) {
3863 oldprop = icalcomponent_get_first_property(oldical, ICAL_ANY_PROPERTY);
3864 oldcomp = icalcomponent_get_first_component(oldical, ICAL_ANY_COMPONENT);
3865 }
3866
3867 for (prop = icalcomponent_get_first_property(ical, ICAL_ANY_PROPERTY);
3868 prop; prop = nextprop) {
3869 const char *xname = NULL, *oldxname;
3870 icalproperty_kind kind = icalproperty_isa(prop);
3871 icalproperty_kind oldkind =
3872 oldprop ? icalproperty_isa(oldprop) : ICAL_NO_PROPERTY;
3873
3874 nextprop = icalcomponent_get_next_property(ical, ICAL_ANY_PROPERTY);
3875
3876 if (oldkind == ICAL_NO_PROPERTY) {
3877 /* No more components in old component */
3878 r = -1;
3879 }
3880 else if (kind == oldkind) {
3881 if (kind == ICAL_X_PROPERTY) {
3882 /* Compare property names alphabetically */
3883 xname = icalproperty_get_x_name(prop);
3884 oldxname = icalproperty_get_x_name(oldprop);
3885 r = strcmp(xname, oldxname);
3886 }
3887 else r = 0;
3888 }
3889 else {
3890 /* Compare property names alphabetically */
3891 r = strcmp(icalproperty_kind_to_string(kind),
3892 icalproperty_kind_to_string(oldkind));
3893 }
3894
3895 if (r == 0) {
3896 switch (kind) {
3897 case ICAL_CALSCALE_PROPERTY:
3898 case ICAL_PRODID_PROPERTY:
3899 case ICAL_DTSTAMP_PROPERTY:
3900 case ICAL_LASTMODIFIED_PROPERTY:
3901 /* Ok to modify these - ignore */
3902 break;
3903
3904 case ICAL_X_PROPERTY:
3905 if (!strcmpsafe(xname, "X-MOZ-GENERATION")) {
3906 /* Ok to modify these - ignore */
3907 break;
3908 }
3909
3910 GCC_FALLTHROUGH
3911
3912 default:
3913 /* Compare entire properties (names, values, parameters) */
3914 if (strcmp(icalproperty_as_ical_string(prop),
3915 icalproperty_as_ical_string(oldprop))) {
3916 /* Property has been updated in ical */
3917 if (read_only) return HTTP_FORBIDDEN;
3918 if (num_changes) (*num_changes)++;
3919 }
3920 break;
3921 }
3922 }
3923 else if (r < 0) {
3924 /* Property has been added to ical */
3925 switch (kind) {
3926 case ICAL_CALSCALE_PROPERTY:
3927 case ICAL_PRODID_PROPERTY:
3928 case ICAL_DTSTAMP_PROPERTY:
3929 case ICAL_LASTMODIFIED_PROPERTY:
3930 /* Ok to add these - ignore */
3931 break;
3932
3933 case ICAL_X_PROPERTY:
3934 xname = icalproperty_get_x_name(prop);
3935 if (strncmp(xname, "X-MOZ-", 6) ||
3936 (strcmp(xname+6, "LASTACK") &&
3937 strcmp(xname+6, "SNOOZE-TIME"))) {
3938 if (read_only) return HTTP_FORBIDDEN;
3939 if (num_changes) (*num_changes)++;
3940 break;
3941 }
3942
3943 GCC_FALLTHROUGH
3944
3945 case ICAL_TRANSP_PROPERTY:
3946 /* Add per-user property to VPATCH */
3947 if (!patch) {
3948 patch = icalcomponent_vanew(ICAL_XPATCH_COMPONENT,
3949 icalproperty_new_patchtarget(
3950 buf_cstring(path)),
3951 0);
3952 icalcomponent_add_component(vpatch, patch);
3953 }
3954
3955 icalcomponent_remove_property(ical, prop);
3956 icalcomponent_add_property(patch, prop);
3957 break;
3958
3959 default:
3960 if (read_only) return HTTP_FORBIDDEN;
3961 if (num_changes) (*num_changes)++;
3962 break;
3963 }
3964
3965 continue; /* Do NOT increment to next old property */
3966 }
3967 else {
3968 /* Property has been removed from ical */
3969 switch (oldkind) {
3970 case ICAL_CALSCALE_PROPERTY:
3971 case ICAL_PRODID_PROPERTY:
3972 case ICAL_DTSTAMP_PROPERTY:
3973 case ICAL_LASTMODIFIED_PROPERTY:
3974 /* Ok to remove these - ignore */
3975 break;
3976
3977 default:
3978 if (read_only) return HTTP_FORBIDDEN;
3979 if (num_changes) (*num_changes)++;
3980 break;
3981 }
3982 }
3983
3984 oldprop = icalcomponent_get_next_property(oldical, ICAL_ANY_PROPERTY);
3985 }
3986
3987 for (comp = icalcomponent_get_first_component(ical, ICAL_ANY_COMPONENT);
3988 comp; comp = nextcomp) {
3989 icalcomponent_kind kind = icalcomponent_isa(comp);
3990 icalcomponent_kind oldkind =
3991 oldcomp ? icalcomponent_isa(oldcomp) : ICAL_NO_COMPONENT;
3992
3993 nextcomp = icalcomponent_get_next_component(ical, ICAL_ANY_COMPONENT);
3994
3995 if (oldkind == ICAL_NO_COMPONENT) {
3996 /* No more components in old component */
3997 r = -1;
3998 }
3999 else if (kind == oldkind) {
4000 if (kind == ICAL_X_COMPONENT) {
4001 /* Compare component names alphabetically */
4002
4003 /* XXX Need a new libical function */
4004 r = 0;
4005 }
4006 else r = 0;
4007 }
4008 else {
4009 /* Compare component names alphabetically */
4010 r = strcmp(icalcomponent_kind_to_string(kind),
4011 icalcomponent_kind_to_string(oldkind));
4012 }
4013
4014 if (r == 0) {
4015 r = extract_personal_data(comp, oldcomp, vpatch,
4016 path, read_only, num_changes);
4017 if (r) return r;
4018 }
4019 else if (r < 0) {
4020 /* Component has been added to ical */
4021 switch (kind) {
4022 case ICAL_VALARM_COMPONENT:
4023 /* Add per-user component to VPATCH */
4024 if (!patch) {
4025 patch = icalcomponent_vanew(ICAL_XPATCH_COMPONENT,
4026 icalproperty_new_patchtarget(
4027 buf_cstring(path)),
4028 0);
4029 icalcomponent_add_component(vpatch, patch);
4030 }
4031
4032 icalcomponent_remove_component(ical, comp);
4033 icalcomponent_add_component(patch, comp);
4034 break;
4035
4036 default:
4037 if (read_only) return HTTP_FORBIDDEN;
4038 if (num_changes) (*num_changes)++;
4039
4040 r = extract_personal_data(comp, oldcomp, vpatch,
4041 path, read_only, num_changes);
4042 if (r) return r;
4043 break;
4044 }
4045
4046 continue; /* Do NOT increment to next old component */
4047 }
4048 else if (read_only) {
4049 return HTTP_FORBIDDEN;
4050 }
4051 else {
4052 /* Component has been removed from ical */
4053 if (num_changes) (*num_changes)++;
4054 }
4055
4056 oldcomp = icalcomponent_get_next_component(oldical, ICAL_ANY_COMPONENT);
4057 }
4058
4059 /* Trim this component from path */
4060 buf_truncate(path, path_len);
4061
4062 return 0;
4063 }
4064
4065
write_personal_data(const char * userid,struct mailbox * mailbox,uint32_t uid,modseq_t modseq,icalcomponent * vpatch)4066 static int write_personal_data(const char *userid,
4067 struct mailbox *mailbox,
4068 uint32_t uid,
4069 modseq_t modseq,
4070 icalcomponent *vpatch)
4071 {
4072 struct message_guid guid;
4073 struct buf value = BUF_INITIALIZER;
4074 const char *icalstr = icalcomponent_as_ical_string(vpatch);
4075 struct dlist *dl = dlist_newkvlist(NULL, "CALDATA");
4076 int ret;
4077
4078 ret = mailbox_get_annotate_state(mailbox, uid, NULL);
4079 if (ret) return ret;
4080
4081 dlist_setdate(dl, "LASTMOD", time(0));
4082 dlist_setnum64(dl, "MODSEQ", modseq);
4083 message_guid_generate(&guid, icalstr, strlen(icalstr));
4084 dlist_setguid(dl, "GUID", &guid);
4085 dlist_setatom(dl, "VPATCH", icalstr);
4086 dlist_printbuf(dl, 1, &value);
4087 dlist_free(&dl);
4088
4089 ret = mailbox_annotation_write(mailbox, uid,
4090 PER_USER_CAL_DATA, userid, &value);
4091 buf_free(&value);
4092
4093 return ret;
4094 }
4095
4096
4097 /*
4098 * Handle stripping per-user data from existing and/or new shared resource.
4099 *
4100 * Logic is as follows:
4101 *
4102 * Owner R/W Exists Shared EO SO EU SU PD
4103 * ------------------------------------------------
4104 * 0 0 Y
4105 * 0 0 1 0 Y Y Y
4106 * 0 0 1 1 Y
4107 * 0 1 0 Y Y
4108 * 0 1 1 0 Y Y ?
4109 * 0 1 1 1 Y ?
4110 * 1 0 1 Y
4111 * 1 1 0 Y
4112 * 1 1 1 0 Y
4113 * 1 1 1 1 Y ?
4114 *
4115 * EO = Extract Owner Data
4116 * SO = Store Owner Resource
4117 * EU = Extract User Data
4118 * SU = Store User Resource
4119 * PD = Permission Denied
4120 */
personalize_resource(struct transaction_t * txn,struct mailbox * mailbox,icalcomponent * ical,struct caldav_data * cdata,const char * userid,icalcomponent ** store_me,icalcomponent ** userdata)4121 static int personalize_resource(struct transaction_t *txn,
4122 struct mailbox *mailbox,
4123 icalcomponent *ical,
4124 struct caldav_data *cdata,
4125 const char *userid,
4126 icalcomponent **store_me,
4127 icalcomponent **userdata)
4128 {
4129 int is_owner, rights, read_only, ret = 0;
4130 mbname_t *mbname;
4131 const char *owner;
4132 icalcomponent *oldical = NULL;
4133 unsigned num_changes = 0;
4134 struct auth_state *authstate = auth_newstate(userid);
4135 char *resource = xstrdupnull(cdata->dav.resource);
4136
4137 *store_me = ical;
4138
4139 /* Check ownership and ACL for current user */
4140 mbname = mbname_from_intname(mailbox->name);
4141 owner = mbname_userid(mbname);
4142 is_owner = !strcmpsafe(owner, userid);
4143
4144 rights = cyrus_acl_myrights(authstate, mailbox->acl);
4145 auth_freestate(authstate);
4146
4147 if (rights & DACL_WRITECONT) {
4148 /* User has read-write access */
4149 read_only = 0;
4150 }
4151 else if (cdata->dav.imap_uid &&
4152 !(mailbox->i.options & OPT_IMAP_SHAREDSEEN)) {
4153 /* User has read-only access to existing resource */
4154 read_only = 1;
4155 }
4156 else {
4157 /* DAV:need-privileges */
4158 txn->error.precond = DAV_NEED_PRIVS;
4159 txn->error.resource = txn->req_tgt.path;
4160 txn->error.rights = DACL_WRITECONT;
4161 ret = HTTP_NO_PRIVS;
4162 goto done;
4163 }
4164
4165 if (cdata->dav.imap_uid &&
4166 (!is_owner || read_only || cdata->comp_flags.shared)) {
4167 syslog(LOG_NOTICE, "LOADING ICAL %u", cdata->dav.imap_uid);
4168
4169 /* Load message containing the existing resource and parse iCal data */
4170 oldical = caldav_record_to_ical(mailbox, cdata, NULL, NULL);
4171 if (!oldical) {
4172 txn->error.desc = "Failed to read record";
4173 ret = HTTP_SERVER_ERROR;
4174 goto done;
4175 }
4176 }
4177
4178 if (cdata->dav.imap_uid && !is_owner && !cdata->comp_flags.shared) {
4179 /* Split owner's personal data from resource */
4180
4181 /* Create UID for owner VPATCH */
4182 assert(!buf_len(&txn->buf));
4183 buf_printf(&txn->buf, "%x-%x-%x", strhash(mailbox->name),
4184 strhash(resource), strhash(owner));
4185
4186 *userdata =
4187 icalcomponent_vanew(ICAL_VPATCH_COMPONENT,
4188 icalproperty_new_version("1"),
4189 icalproperty_new_dtstamp(
4190 icaltime_from_timet_with_zone(time(0),
4191 0,
4192 utc_zone)),
4193 icalproperty_new_uid(buf_cstring(&txn->buf)),
4194 0);
4195 buf_reset(&txn->buf);
4196
4197 /* Extract personal info from owner's resource and create vpatch */
4198 ret = extract_personal_data(oldical, NULL, *userdata,
4199 &txn->buf /* path */, 0 /* read_only */,
4200 &num_changes);
4201 buf_reset(&txn->buf);
4202
4203 if (!ret) ret = write_personal_data(owner, mailbox, cdata->dav.imap_uid,
4204 cdata->dav.modseq, *userdata);
4205
4206 if (ret) goto done;
4207
4208 if (read_only) {
4209 /* Resource to store is the existing resource just stripped */
4210 *store_me = oldical;
4211 }
4212
4213 icalcomponent_free(*userdata);
4214 *userdata = NULL;
4215 }
4216
4217 if (!is_owner || read_only ||
4218 (cdata->dav.imap_uid && cdata->comp_flags.shared)) {
4219 /* Extract personal info from user's resource and create vpatch */
4220 if (oldical) {
4221 /* Normalize existing resource for comparison */
4222 icalcomponent_normalize(oldical);
4223
4224 /* Normalize new resource for comparison */
4225 icalcomponent_normalize(ical);
4226 }
4227
4228 /* Create UID for sharee VPATCH */
4229 assert(!buf_len(&txn->buf));
4230 buf_printf(&txn->buf, "%x-%x-%x", strhash(mailbox->name),
4231 strhash(resource), strhash(userid));
4232
4233 *userdata =
4234 icalcomponent_vanew(ICAL_VPATCH_COMPONENT,
4235 icalproperty_new_version("1"),
4236 icalproperty_new_dtstamp(
4237 icaltime_from_timet_with_zone(time(0),
4238 0,
4239 utc_zone)),
4240 icalproperty_new_uid(buf_cstring(&txn->buf)),
4241 0);
4242 buf_reset(&txn->buf);
4243
4244 /* Extract personal info from new resource and add to vpatch */
4245 /* XXX DO NOT reinitialize num_changes. We need the changes
4246 from rewriting owner resource to force storage of that resource */
4247 ret = extract_personal_data(ical, oldical, *userdata,
4248 &txn->buf /* path */, read_only,
4249 &num_changes);
4250 buf_reset(&txn->buf);
4251
4252 if (ret) goto done;
4253
4254 if (cdata->dav.imap_uid && !num_changes) {
4255 /* No resource to store (per-user data change only) */
4256 ret = HTTP_NO_CONTENT;
4257 *store_me = NULL;
4258 goto done;
4259 }
4260
4261 cdata->comp_flags.shared = 1;
4262 }
4263
4264 done:
4265 if (oldical && (*store_me != oldical)) icalcomponent_free(oldical);
4266 mbname_free(&mbname);
4267 free(resource);
4268
4269 return ret;
4270 }
4271
4272
4273 /* Perform a PUT request
4274 *
4275 * preconditions:
4276 * CALDAV:valid-calendar-data
4277 * CALDAV:valid-calendar-object-resource
4278 * CALDAV:supported-calendar-component
4279 * CALDAV:no-uid-conflict (DAV:href)
4280 * CALDAV:max-resource-size
4281 * CALDAV:min-date-time
4282 * CALDAV:max-date-time
4283 * CALDAV:max-instances
4284 * CALDAV:max-attendees-per-instance
4285 */
caldav_put(struct transaction_t * txn,void * obj,struct mailbox * mailbox,const char * resource,void * destdb,unsigned flags)4286 static int caldav_put(struct transaction_t *txn, void *obj,
4287 struct mailbox *mailbox, const char *resource,
4288 void *destdb, unsigned flags)
4289 {
4290 int ret = 0;
4291 struct caldav_db *db = (struct caldav_db *)destdb;
4292 icalcomponent *ical = (icalcomponent *)obj;
4293 icalcomponent *oldical = NULL;
4294 icalcomponent *comp, *nextcomp;
4295 icalcomponent_kind kind;
4296 icaltimetype dtstart, dtend;
4297 icalproperty *prop, *rrule = NULL;
4298 const char *uid, *organizer = NULL;
4299 strarray_t schedule_addresses = STRARRAY_INITIALIZER;
4300 struct buf buf = BUF_INITIALIZER;
4301 struct caldav_data *cdata;
4302
4303 /* Validate the iCal data */
4304 if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) {
4305 txn->error.desc = "Resource is not an iCalendar object";
4306 txn->error.precond = CALDAV_VALID_DATA;
4307 ret = HTTP_FORBIDDEN;
4308 goto done;
4309 }
4310
4311 cyrus_icalrestriction_check(ical);
4312 if ((txn->error.desc = get_icalcomponent_errstr(ical))) {
4313 buf_setcstr(&txn->buf, txn->error.desc);
4314 txn->error.desc = buf_cstring(&txn->buf);
4315 txn->error.precond = CALDAV_VALID_DATA;
4316 ret = HTTP_FORBIDDEN;
4317 goto done;
4318 }
4319
4320 comp = icalcomponent_get_first_real_component(ical);
4321 if (rscale_calendars) {
4322 /* Grab RRULE to check RSCALE */
4323 rrule = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
4324 }
4325
4326 /* Make sure iCal UIDs [and ORGANIZERs] in all components are the same */
4327 kind = icalcomponent_isa(comp);
4328 uid = icalcomponent_get_uid(comp);
4329 if (!uid) {
4330 txn->error.desc = "Missing UID property";
4331 txn->error.precond = CALDAV_VALID_OBJECT;
4332 ret = HTTP_FORBIDDEN;
4333 goto done;
4334 }
4335 prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
4336 if (prop) organizer = icalproperty_get_organizer(prop);
4337
4338 /* Also make sure DTEND > DTSTART, and both values have value same type */
4339 dtend = icalcomponent_get_dtend(comp);
4340 if (!icaltime_is_null_time(dtend)) {
4341 dtstart = icalcomponent_get_dtstart(comp);
4342
4343 if (icaltime_is_date(dtend) != icaltime_is_date(dtstart) ||
4344 // !icaltime_get_timezone(dtend) != !icaltime_get_timezone(dtstart) ||
4345 icaltime_compare(dtend, dtstart) < 0) {
4346 txn->error.desc = "DTEND occurs before DTSTART";
4347 txn->error.precond = CALDAV_VALID_DATA;
4348 ret = HTTP_FORBIDDEN;
4349 goto done;
4350 }
4351 }
4352
4353 while ((nextcomp =
4354 icalcomponent_get_next_component(ical, kind))) {
4355 const char *nextuid = icalcomponent_get_uid(nextcomp);
4356
4357 if (!nextuid || strcmp(uid, nextuid)) {
4358 txn->error.desc = "Mismatched UIDs";
4359 txn->error.precond = CALDAV_VALID_OBJECT;
4360 ret = HTTP_FORBIDDEN;
4361 goto done;
4362 }
4363
4364 const char *nextorg = NULL;
4365
4366 prop = icalcomponent_get_first_property(nextcomp,
4367 ICAL_ORGANIZER_PROPERTY);
4368 if (prop) nextorg = icalproperty_get_organizer(prop);
4369 /* if no toplevel organizer, use the one from here */
4370 if (!organizer && nextorg) organizer = nextorg;
4371 if (nextorg && strcmp(organizer, nextorg)) {
4372 txn->error.precond = CALDAV_SAME_ORGANIZER;
4373 ret = HTTP_FORBIDDEN;
4374 goto done;
4375 }
4376
4377 dtend = icalcomponent_get_dtend(nextcomp);
4378 if (!icaltime_is_null_time(dtend)) {
4379 dtstart = icalcomponent_get_dtstart(nextcomp);
4380
4381 if (icaltime_compare(dtend, dtstart) < 0) {
4382 txn->error.desc = "DTEND occurs before DTSTART";
4383 txn->error.precond = CALDAV_VALID_OBJECT;
4384 ret = HTTP_FORBIDDEN;
4385 goto done;
4386 }
4387 }
4388
4389 if (rscale_calendars && !rrule) {
4390 /* Grab RRULE to check RSCALE */
4391 rrule = icalcomponent_get_first_property(nextcomp,
4392 ICAL_RRULE_PROPERTY);
4393 }
4394 }
4395
4396 #ifdef HAVE_RSCALE
4397 /* Make sure we support the provided RSCALE in an RRULE */
4398 if (rrule && rscale_calendars) {
4399 struct icalrecurrencetype rt = icalproperty_get_rrule(rrule);
4400
4401 if (rt.rscale && *rt.rscale) {
4402 /* Perform binary search on sorted icalarray */
4403 unsigned found = 0, start = 0, end = rscale_calendars->num_elements;
4404
4405 ucase((char *) rt.rscale);
4406 while (!found && start < end) {
4407 unsigned mid = start + (end - start) / 2;
4408 const char **rscale =
4409 icalarray_element_at(rscale_calendars, mid);
4410 int r = strcmp(rt.rscale, *rscale);
4411
4412 if (r == 0) found = 1;
4413 else if (r < 0) end = mid;
4414 else start = mid + 1;
4415 }
4416
4417 if (!found) {
4418 txn->error.precond = CALDAV_SUPP_RSCALE;
4419 ret = HTTP_FORBIDDEN;
4420 goto done;
4421 }
4422 }
4423 }
4424 #endif /* HAVE_RSCALE */
4425
4426 /* Check for changed UID */
4427 caldav_lookup_resource(db, mailbox->name, resource, &cdata, 0);
4428 if (cdata->dav.imap_uid && strcmpsafe(cdata->ical_uid, uid)) {
4429 /* CALDAV:no-uid-conflict */
4430 txn->error.precond = CALDAV_UID_CONFLICT;
4431 ret = HTTP_FORBIDDEN;
4432 }
4433 else {
4434 /* Check for duplicate iCalendar UID */
4435 caldav_lookup_uid(db, uid, &cdata);
4436 if (cdata->dav.imap_uid && (strcmp(cdata->dav.mailbox, mailbox->name) ||
4437 strcmp(cdata->dav.resource, resource))) {
4438 /* CALDAV:unique-scheduling-object-resource */
4439 txn->error.precond = CALDAV_UNIQUE_OBJECT;
4440 ret = HTTP_FORBIDDEN;
4441 }
4442 }
4443 if (ret) {
4444 char *owner = mboxname_to_userid(cdata->dav.mailbox);
4445
4446 buf_reset(&txn->buf);
4447 buf_printf(&txn->buf, "%s/%s/%s/%s/%s",
4448 namespace_calendar.prefix, USER_COLLECTION_PREFIX, owner,
4449 strrchr(cdata->dav.mailbox, '.')+1, cdata->dav.resource);
4450 txn->error.resource = buf_cstring(&txn->buf);
4451 free(owner);
4452 ret = HTTP_FORBIDDEN;
4453 goto done;
4454 }
4455
4456 if (namespace_calendar.allow & ALLOW_CAL_ATTACH) {
4457 ret = manage_attachments(txn, mailbox, ical,
4458 cdata, &oldical, &schedule_addresses);
4459 if (ret) goto done;
4460 }
4461
4462 switch (kind) {
4463 case ICAL_VEVENT_COMPONENT:
4464 case ICAL_VTODO_COMPONENT:
4465 case ICAL_VPOLL_COMPONENT:
4466 if (organizer) {
4467 /* Scheduling object resource */
4468
4469 syslog(LOG_DEBUG, "caldav_put: organizer: %s", organizer);
4470
4471 if (!strncasecmp(organizer, "mailto:", 7)) organizer += 7;
4472
4473 if (cdata->organizer &&
4474 !spool_getheader(txn->req_hdrs, "Allow-Organizer-Change")) {
4475 /* Don't allow ORGANIZER to be changed */
4476 if (strcmp(cdata->organizer, organizer)) {
4477 txn->error.desc = "Can not change organizer address";
4478 ret = HTTP_FORBIDDEN;
4479 }
4480 }
4481
4482 /* existing record? */
4483 if (cdata->dav.imap_uid && !oldical) {
4484 /* Update existing object */
4485 syslog(LOG_NOTICE, "LOADING ICAL %u", cdata->dav.imap_uid);
4486
4487 /* Load message containing the resource and parse iCal data */
4488 oldical = caldav_record_to_ical(mailbox, cdata,
4489 NULL, NULL);
4490 if (!oldical) {
4491 txn->error.desc = "Failed to read record";
4492 ret = HTTP_SERVER_ERROR;
4493 goto done;
4494 }
4495 }
4496
4497 get_schedule_addresses(txn->req_hdrs, txn->req_tgt.mbentry->name,
4498 txn->req_tgt.userid, &schedule_addresses);
4499
4500 char *userid = (txn->req_tgt.flags == TGT_DAV_SHARED) ?
4501 xstrdup(txn->req_tgt.userid) :
4502 mboxname_to_userid(txn->req_tgt.mbentry->name);
4503 if (strarray_find_case(&schedule_addresses, organizer, 0) >= 0) {
4504 /* Organizer scheduling object resource */
4505 if (ret) {
4506 txn->error.precond = CALDAV_ALLOWED_ORG_CHANGE;
4507 }
4508 else {
4509 if (_scheduling_enabled(txn, mailbox))
4510 sched_request(userid, &schedule_addresses, organizer, oldical, ical);
4511 }
4512 }
4513 else {
4514 /* Attendee scheduling object resource */
4515 if (ret) {
4516 txn->error.precond = CALDAV_ALLOWED_ATT_CHANGE;
4517 }
4518 #if 0
4519 else if (!oldical) {
4520 /* Can't reply to a non-existent invitation */
4521 /* XXX But what about invites over iMIP? */
4522 txn->error.desc = "Can not reply to non-existent resource";
4523 ret = HTTP_FORBIDDEN;
4524 }
4525 #endif
4526 else {
4527 if (_scheduling_enabled(txn, mailbox) && strarray_size(&schedule_addresses))
4528 sched_reply(userid, &schedule_addresses, oldical, ical);
4529 }
4530 }
4531 free(userid);
4532
4533 if (ret) goto done;
4534
4535 flags |= NEW_STAG;
4536 }
4537 break;
4538
4539 case ICAL_VJOURNAL_COMPONENT:
4540 case ICAL_VFREEBUSY_COMPONENT:
4541 case ICAL_VAVAILABILITY_COMPONENT:
4542 /* Nothing else to do */
4543 break;
4544
4545 default:
4546 txn->error.precond = CALDAV_SUPP_COMP;
4547 ret = HTTP_FORBIDDEN;
4548 goto done;
4549 }
4550
4551 /* Store resource at target */
4552 if (!ret) {
4553 ret = caldav_store_resource(txn, ical, mailbox, resource, cdata->dav.createdmodseq,
4554 db, flags, httpd_userid, &schedule_addresses);
4555 }
4556
4557 done:
4558 if (oldical) icalcomponent_free(oldical);
4559 strarray_fini(&schedule_addresses);
4560 buf_free(&buf);
4561
4562 return ret;
4563 }
4564
4565
4566 struct comp_filter {
4567 unsigned comp_type;
4568 xmlChar *name; /* need this for X- components */
4569 unsigned depth;
4570 icalcomponent_kind kind;
4571 unsigned allof : 1;
4572 unsigned not_defined : 1;
4573 struct icalperiodtype *range;
4574 struct prop_filter *prop;
4575 struct comp_filter *comp;
4576 struct comp_filter *next;
4577 };
4578
4579 struct calquery_filter {
4580 unsigned flags; /* mask of flags controlling filter */
4581 unsigned comp_types; /* mask of "real" component types in filter */
4582 icaltimezone *tz; /* time zone to use for floating time */
4583 struct comp_filter *comp;
4584 };
4585
4586 /* Bitmask of calquery flags */
4587 enum {
4588 PARSE_ICAL = (1<<0),
4589 NEED_TZ = (1<<1),
4590 };
4591
is_valid_timerange(const struct icaltimetype start,const struct icaltimetype end)4592 static int is_valid_timerange(const struct icaltimetype start,
4593 const struct icaltimetype end)
4594 {
4595 return (icaltime_is_valid_time(start) && icaltime_is_valid_time(end) &&
4596 !icaltime_is_date(start) && !icaltime_is_date(end) &&
4597 (icaltime_is_utc(start) || start.zone) &&
4598 (icaltime_is_utc(end) || end.zone));
4599 }
4600
parse_timerange(xmlNodePtr node,struct icalperiodtype ** range,struct error_t * error)4601 static void parse_timerange(xmlNodePtr node,
4602 struct icalperiodtype **range, struct error_t *error)
4603 {
4604 xmlChar *attr;
4605 int count = 0;
4606
4607 *range = xzmalloc(sizeof(struct icalperiodtype));
4608
4609 attr = xmlGetProp(node, BAD_CAST "start");
4610 if (attr) {
4611 count++;
4612 (*range)->start = icaltime_from_string((char *) attr);
4613 xmlFree(attr);
4614 }
4615 else {
4616 (*range)->start =
4617 icaltime_from_timet_with_zone(caldav_epoch, 0, utc_zone);
4618 }
4619
4620 attr = xmlGetProp(node, BAD_CAST "end");
4621 if (attr) {
4622 count++;
4623 (*range)->end = icaltime_from_string((char *) attr);
4624 xmlFree(attr);
4625 }
4626 else {
4627 (*range)->end =
4628 icaltime_from_timet_with_zone(caldav_eternity, 0, utc_zone);
4629 }
4630
4631 if (!count || !is_valid_timerange((*range)->start, (*range)->end)) {
4632 error->precond = CALDAV_VALID_FILTER;
4633 error->desc = "Invalid time-range";
4634 error->node = xmlCopyNode(node->parent, 1);
4635 }
4636 }
4637
cal_parse_propfilter(xmlNodePtr node,struct prop_filter * prop,struct error_t * error)4638 static void cal_parse_propfilter(xmlNodePtr node, struct prop_filter *prop,
4639 struct error_t *error)
4640 {
4641 if (!xmlStrcmp(node->name, BAD_CAST "time-range")) {
4642 if (prop->other) {
4643 error->precond = CALDAV_SUPP_FILTER;
4644 error->desc = "Multiple time-range";
4645 error->node = xmlCopyNode(node->parent, 1);
4646 }
4647 else {
4648 struct icalperiodtype *range = NULL;
4649 icalvalue_kind kind =
4650 icalproperty_kind_to_value_kind(prop->kind);
4651
4652 switch (kind) {
4653 case ICAL_DATE_VALUE:
4654 case ICAL_DATETIME_VALUE:
4655 case ICAL_DATETIMEPERIOD_VALUE:
4656 case ICAL_PERIOD_VALUE:
4657 parse_timerange(node, &range, error);
4658 prop->other = range;
4659 break;
4660
4661 default:
4662 error->precond = CALDAV_SUPP_FILTER;
4663 error->desc = "Property does not support time-range";
4664 error->node = xmlCopyNode(node->parent, 1);
4665 break;
4666 }
4667 }
4668 }
4669 else {
4670 error->precond = CALDAV_SUPP_FILTER;
4671 error->desc = "Unsupported element in prop-filter";
4672 error->node = xmlCopyNode(node->parent, 1);
4673 }
4674 }
4675
4676 /* This handles calendar-query-extended per draft-daboo-caldav-extensions */
parse_compfilter(xmlNodePtr root,unsigned depth,struct comp_filter ** comp,unsigned * flags,unsigned * comp_types,struct error_t * error)4677 static void parse_compfilter(xmlNodePtr root, unsigned depth,
4678 struct comp_filter **comp, unsigned *flags,
4679 unsigned *comp_types, struct error_t *error)
4680 {
4681 xmlChar *attr;
4682 xmlNodePtr node;
4683 struct filter_profile_t profile =
4684 { 1 /* allof */, COLLATION_ASCII,
4685 CALDAV_SUPP_FILTER, CALDAV_SUPP_COLLATION,
4686 &icalproperty_string_to_kind, ICAL_NO_PROPERTY,
4687 &icalparameter_string_to_kind, ICAL_NO_PARAMETER,
4688 &cal_parse_propfilter };
4689
4690 /* Parse elements of comp-filter */
4691 attr = xmlGetProp(root, BAD_CAST "name");
4692 if (!attr) {
4693 error->precond = CALDAV_SUPP_FILTER;
4694 error->desc = "Missing 'name' attribute";
4695 error->node = xmlCopyNode(root, 2);
4696 }
4697 else {
4698 icalcomponent_kind kind;
4699
4700 if (!xmlStrcmp(attr, BAD_CAST "*")) kind = ICAL_ANY_COMPONENT;
4701 else kind = icalcomponent_string_to_kind((const char *) attr);
4702
4703 *comp = xzmalloc(sizeof(struct comp_filter));
4704 (*comp)->name = attr;
4705 (*comp)->depth = depth;
4706 (*comp)->kind = kind;
4707 (*comp)->allof = 1;
4708
4709 if (kind == ICAL_NO_COMPONENT) {
4710 error->precond = CALDAV_SUPP_FILTER;
4711 error->desc = "Unsupported component";
4712 error->node = xmlCopyNode(root, 2);
4713 }
4714 else {
4715 switch (depth) {
4716 case 0:
4717 /* VCALENDAR */
4718 if (kind != ICAL_VCALENDAR_COMPONENT) {
4719 /* All other components MUST be a descendent of VCALENDAR */
4720 error->precond = CALDAV_VALID_FILTER;
4721 error->desc = "VCALENDAR must be toplevel component";
4722 }
4723 break;
4724
4725 case 1:
4726 /* Child of VCALENDAR */
4727 switch (kind) {
4728 case ICAL_VCALENDAR_COMPONENT:
4729 /* VCALENDAR MUST only appear at toplevel */
4730 error->precond = CALDAV_VALID_FILTER;
4731 error->desc = "VCALENDAR can only be toplevel component";
4732 break;
4733 case ICAL_VEVENT_COMPONENT:
4734 (*comp)->comp_type = CAL_COMP_VEVENT;
4735 break;
4736 case ICAL_VTODO_COMPONENT:
4737 (*comp)->comp_type = CAL_COMP_VTODO;
4738 break;
4739 case ICAL_VJOURNAL_COMPONENT:
4740 (*comp)->comp_type = CAL_COMP_VJOURNAL;
4741 break;
4742 case ICAL_VFREEBUSY_COMPONENT:
4743 (*comp)->comp_type = CAL_COMP_VFREEBUSY;
4744 break;
4745 case ICAL_VAVAILABILITY_COMPONENT:
4746 (*comp)->comp_type = CAL_COMP_VAVAILABILITY;
4747 break;
4748 case ICAL_VPOLL_COMPONENT:
4749 (*comp)->comp_type = CAL_COMP_VPOLL;
4750 break;
4751 default:
4752 *flags |= PARSE_ICAL;
4753 break;
4754 }
4755
4756 *comp_types |= (*comp)->comp_type;
4757 break;
4758
4759 default:
4760 /* [Great*] grandchild of VCALENDAR */
4761 if (kind == ICAL_VCALENDAR_COMPONENT) {
4762 /* VCALENDAR MUST only appear at toplevel */
4763 error->precond = CALDAV_VALID_FILTER;
4764 error->desc = "VCALENDAR can only be toplevel component";
4765 }
4766 else *flags |= PARSE_ICAL;
4767 break;
4768 }
4769 }
4770
4771 if (!error->precond) {
4772 attr = xmlGetProp(root, BAD_CAST "test");
4773 if (attr) {
4774 if (!xmlStrcmp(attr, BAD_CAST "anyof")) (*comp)->allof = 0;
4775 else if (xmlStrcmp(attr, BAD_CAST "allof")) {
4776 error->precond = CALDAV_SUPP_FILTER;
4777 error->desc = "Unsupported test";
4778 error->node = xmlCopyNode(root, 2);
4779 }
4780 xmlFree(attr);
4781 }
4782 }
4783 }
4784
4785 for (node = xmlFirstElementChild(root); node && !error->precond;
4786 node = xmlNextElementSibling(node)) {
4787
4788 if ((*comp)->not_defined) {
4789 error->precond = CALDAV_SUPP_FILTER;
4790 error->desc = DAV_FILTER_ISNOTDEF_ERR;
4791 error->node = xmlCopyNode(root, 1);
4792 }
4793 else if (!xmlStrcmp(node->name, BAD_CAST "is-not-defined")) {
4794 if ((*comp)->range || (*comp)->prop || (*comp)->comp) {
4795 error->precond = CALDAV_SUPP_FILTER;
4796 error->desc = DAV_FILTER_ISNOTDEF_ERR;
4797 error->node = xmlCopyNode(root, 1);
4798 }
4799 else {
4800 *flags |= PARSE_ICAL;
4801 (*comp)->not_defined = 1;
4802 if ((*comp)->comp_type) {
4803 *comp_types &= ~(*comp)->comp_type;
4804 (*comp)->comp_type = 0;
4805 }
4806 }
4807 }
4808 else if (!xmlStrcmp(node->name, BAD_CAST "time-range")) {
4809 if ((*comp)->range) {
4810 error->precond = CALDAV_SUPP_FILTER;
4811 error->desc = "Multiple time-range";
4812 error->node = xmlCopyNode(root, 1);
4813 }
4814 else {
4815 switch ((*comp)->kind) {
4816 case ICAL_ANY_COMPONENT:
4817 case ICAL_VEVENT_COMPONENT:
4818 case ICAL_VTODO_COMPONENT:
4819 case ICAL_VJOURNAL_COMPONENT:
4820 case ICAL_VFREEBUSY_COMPONENT:
4821 case ICAL_VAVAILABILITY_COMPONENT:
4822 case ICAL_VPOLL_COMPONENT:
4823 parse_timerange(node, &(*comp)->range, error);
4824 *flags |= NEED_TZ;
4825 break;
4826
4827 default:
4828 error->precond = CALDAV_SUPP_FILTER;
4829 error->desc = "time-range unsupported for this component";
4830 error->node = xmlCopyNode(root, 1);
4831 break;
4832 }
4833 }
4834
4835 if (depth != 1) *flags |= PARSE_ICAL;
4836 }
4837 else if (!xmlStrcmp(node->name, BAD_CAST "prop-filter")) {
4838 struct prop_filter *prop = NULL;
4839
4840 *flags |= PARSE_ICAL;
4841
4842 dav_parse_propfilter(node, &prop, &profile, error);
4843 if (prop) {
4844 if ((*comp)->prop) prop->next = (*comp)->prop;
4845 (*comp)->prop = prop;
4846 if (prop->match) {
4847 if (prop->other || prop->match->next) {
4848 error->precond = CALDAV_SUPP_FILTER;
4849 error->desc = prop->match->next ? "Multiple text-match" :
4850 "time-range can NOT be combined with text-match";
4851 error->node = xmlCopyNode(node, 1);
4852 }
4853 }
4854 else if (prop->other) {
4855 /* CALDAV:time-range */
4856 *flags |= NEED_TZ;
4857 }
4858 }
4859 }
4860 else if (!xmlStrcmp(node->name, BAD_CAST "comp-filter")) {
4861 struct comp_filter *subcomp = NULL;
4862
4863 parse_compfilter(node, depth + 1, &subcomp,
4864 flags, comp_types, error);
4865 if (subcomp) {
4866 if ((*comp)->comp) subcomp->next = (*comp)->comp;
4867 (*comp)->comp = subcomp;
4868 }
4869 }
4870 else {
4871 error->precond = CALDAV_SUPP_FILTER;
4872 error->desc = "Unsupported element in comp-filter";
4873 error->node = xmlCopyNode(root, 1);
4874 }
4875 }
4876 }
4877
parse_calfilter(xmlNodePtr root,struct calquery_filter * filter,struct error_t * error)4878 static int parse_calfilter(xmlNodePtr root, struct calquery_filter *filter,
4879 struct error_t *error)
4880 {
4881 xmlNodePtr node;
4882
4883 /* Parse elements of filter */
4884 node = xmlFirstElementChild(root);
4885 if (node && !xmlStrcmp(node->name, BAD_CAST "comp-filter")) {
4886 parse_compfilter(node, 0, &filter->comp,
4887 &filter->flags, &filter->comp_types, error);
4888 }
4889 else {
4890 error->precond = CALDAV_VALID_FILTER;
4891 error->desc = "missing comp-filter element";
4892 }
4893
4894 return error->precond ? HTTP_FORBIDDEN : 0;
4895 }
4896
4897
apply_paramfilter(struct param_filter * paramfilter,icalproperty * prop)4898 static int apply_paramfilter(struct param_filter *paramfilter,
4899 icalproperty *prop)
4900 {
4901 int pass = 1;
4902 icalparameter *param =
4903 icalproperty_get_first_parameter(prop, paramfilter->kind);
4904
4905 if (paramfilter->kind == ICAL_X_PARAMETER) {
4906 /* Find the first X- parameter with matching name */
4907 for (; param && strcmp((const char *) paramfilter->name,
4908 icalparameter_get_xname(param));
4909 param = icalproperty_get_next_parameter(prop, paramfilter->kind));
4910 }
4911
4912 if (!param) return paramfilter->not_defined;
4913 if (paramfilter->not_defined) return 0;
4914 if (!paramfilter->match) return 1;
4915
4916 /* Test each instance of this parameter (logical OR) */
4917 do {
4918 const char *text;
4919
4920 if (!pass && (paramfilter->kind == ICAL_X_PARAMETER) &&
4921 strcmp((const char *) paramfilter->name,
4922 icalparameter_get_xname(param))) {
4923 /* Skip X- parameter if name doesn't match */
4924 continue;
4925 }
4926
4927 text = icalparameter_get_iana_value(param);
4928 pass = dav_apply_textmatch(BAD_CAST text, paramfilter->match);
4929
4930 } while (!pass &&
4931 (param = icalproperty_get_next_parameter(prop, paramfilter->kind)));
4932
4933 return pass;
4934 }
4935
apply_prop_timerange(struct icalperiodtype * range,icaltimezone * floating_tz,icalproperty * prop)4936 static int apply_prop_timerange(struct icalperiodtype *range,
4937 icaltimezone *floating_tz, icalproperty *prop)
4938 {
4939 icalvalue *value = icalproperty_get_value(prop);
4940 struct icalperiodtype period = icalperiodtype_null_period();
4941 icalparameter *param;
4942
4943 switch (icalvalue_isa(value)) {
4944 case ICAL_DATE_VALUE:
4945 period.start = icalvalue_get_date(value);
4946 period.start.is_date = 0; /* MUST be DATE-TIME */
4947 break;
4948
4949 case ICAL_DATETIME_VALUE:
4950 period.start = icalvalue_get_datetime(value);
4951 break;
4952
4953 case ICAL_PERIOD_VALUE:
4954 period = icalvalue_get_period(value);
4955 break;
4956
4957 default:
4958 /* Should never get here */
4959 break;
4960 }
4961
4962 /* Set the timezone, if any, on the start time */
4963 if ((param = icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER))) {
4964 const char *tzid = icalparameter_get_tzid(param);
4965 icaltimezone *tz = NULL;
4966 icalcomponent *comp;
4967
4968 for (comp = icalproperty_get_parent(prop); comp;
4969 comp = icalcomponent_get_parent(comp)) {
4970 tz = icalcomponent_get_timezone(comp, tzid);
4971 if (tz) break;
4972 }
4973
4974 if (!tz) tz = icaltimezone_get_builtin_timezone_from_tzid(tzid);
4975
4976 if (tz) period.start = icaltime_set_timezone(&period.start, tz);
4977 }
4978
4979 if (!icaldurationtype_is_null_duration(period.duration)) {
4980 /* Calculate end time from duration */
4981 period.end = icaltime_add(period.start, period.duration);
4982 }
4983 else if (icaltime_is_null_time(period.end)) period.end = period.start;
4984
4985 /* Convert to UTC for comparison with range */
4986 period.start = icaltime_convert_to_utc(period.start, floating_tz);
4987 period.end = icaltime_convert_to_utc(period.end, floating_tz);
4988
4989 if (icaltime_compare(period.start, range->end) >= 0 ||
4990 icaltime_compare(period.end, range->start) <= 0) {
4991 /* Starts later or ends earlier than range */
4992 return 0;
4993 }
4994
4995 return 1;
4996 }
4997
apply_propfilter(struct prop_filter * propfilter,icaltimezone * floating_tz,icalcomponent * comp)4998 static int apply_propfilter(struct prop_filter *propfilter,
4999 icaltimezone *floating_tz, icalcomponent *comp)
5000 {
5001 int pass = 1;
5002 icalproperty *prop =
5003 icalcomponent_get_first_property(comp, propfilter->kind);
5004
5005 if (propfilter->kind == ICAL_X_PROPERTY) {
5006 /* Find the first X- property with matching name */
5007 for (; prop && strcmp((const char *) propfilter->name,
5008 icalproperty_get_property_name(prop));
5009 prop = icalcomponent_get_next_property(comp, propfilter->kind));
5010 }
5011
5012 if (!prop) return propfilter->not_defined;
5013 if (propfilter->not_defined) return 0;
5014 if (!(propfilter->other || propfilter->match || propfilter->param)) return 1;
5015
5016 /* Test each instance of this property (logical OR) */
5017 do {
5018 struct param_filter *paramfilter;
5019
5020 if (!pass && (propfilter->kind == ICAL_X_PROPERTY) &&
5021 strcmp((const char *) propfilter->name,
5022 icalproperty_get_property_name(prop))) {
5023 /* Skip X- property if name doesn't match */
5024 continue;
5025 }
5026
5027 pass = propfilter->allof;
5028
5029 if (propfilter->other) {
5030 pass = apply_prop_timerange(propfilter->other, floating_tz, prop);
5031 }
5032 else if (propfilter->match) {
5033 const char *text = icalproperty_get_value_as_string(prop);
5034
5035 pass = dav_apply_textmatch(BAD_CAST text, propfilter->match);
5036 }
5037
5038 /* Apply each param-filter, breaking if allof fails or anyof succeeds */
5039 for (paramfilter = propfilter->param;
5040 paramfilter && (pass == propfilter->allof);
5041 paramfilter = paramfilter->next) {
5042
5043 pass = apply_paramfilter(paramfilter, prop);
5044 }
5045
5046 } while (!pass &&
5047 (prop = icalcomponent_get_next_property(comp, propfilter->kind)));
5048
5049 return pass;
5050 }
5051
in_range(icalcomponent * comp,struct icaltime_span * span,void * rock)5052 static void in_range(icalcomponent *comp __attribute__((unused)),
5053 struct icaltime_span *span __attribute__((unused)),
5054 void *rock)
5055 {
5056 int *pass = (int *) rock;
5057
5058 *pass = 1;
5059 }
5060
apply_comp_timerange(struct comp_filter * compfilter,icaltimezone * floating_tz,icalcomponent * comp,struct caldav_data * cdata,struct propfind_ctx * fctx)5061 static int apply_comp_timerange(struct comp_filter *compfilter,
5062 icaltimezone *floating_tz,
5063 icalcomponent *comp, struct caldav_data *cdata,
5064 struct propfind_ctx *fctx)
5065 {
5066 struct icalperiodtype *range = compfilter->range;
5067 struct icaltimetype dtstart;
5068 struct icaltimetype dtend;
5069 int pass = 0;
5070
5071 if (compfilter->depth == 1) {
5072 /* Use period from cdata */
5073 dtstart = icaltime_convert_to_utc(icaltime_from_string(cdata->dtstart),
5074 floating_tz);
5075 dtend = icaltime_convert_to_utc(icaltime_from_string(cdata->dtend),
5076 floating_tz);
5077
5078 if (icaltime_compare(dtstart, range->end) >= 0 ||
5079 icaltime_compare(dtend, range->start) <= 0) {
5080 /* All occurrences start later or end earlier than range */
5081 return 0;
5082 }
5083 if (compfilter->kind == ICAL_VAVAILABILITY_COMPONENT) {
5084 /* Don't try to expand VAVAILABILITY, just mark it as in range */
5085 return 1;
5086 }
5087 if (cdata->comp_flags.recurring) {
5088 if (!(compfilter->prop || compfilter->comp) &&
5089 (icaltime_compare(dtstart, range->start) >= 0 ||
5090 icaltime_compare(dtend, range->end) <= 0)) {
5091 /* An occurrence (possibly override) starts or ends within range
5092 and we don't need to do further filtering of the comp */
5093 return 1;
5094 }
5095
5096 /* Load message containing the resource and parse iCal data */
5097 if (!comp) {
5098 if (!fctx->msg_buf.len) {
5099 mailbox_map_record(fctx->mailbox,
5100 fctx->record, &fctx->msg_buf);
5101 }
5102 if (fctx->msg_buf.len && !fctx->obj) {
5103 fctx->obj =
5104 icalparser_parse_string(buf_cstring(&fctx->msg_buf) +
5105 fctx->record->header_size);
5106 }
5107 if (!fctx->obj) return 0;
5108 comp = icalcomponent_get_first_component(fctx->obj,
5109 compfilter->kind);
5110 }
5111 }
5112 else {
5113 /* Non-recurring component overlaps range */
5114 return 1;
5115 }
5116 }
5117
5118 /* Process component */
5119 if (!comp) return 0;
5120
5121 icalcomponent_foreach_recurrence(comp, range->start, range->end,
5122 in_range, &pass);
5123
5124 return pass;
5125 }
5126
5127 /* See if the current resource matches the specified filter.
5128 * Returns 1 if match, 0 otherwise.
5129 */
apply_compfilter(struct comp_filter * compfilter,icaltimezone * floating_tz,icalcomponent * ical,struct caldav_data * cdata,struct propfind_ctx * fctx)5130 static int apply_compfilter(struct comp_filter *compfilter,
5131 icaltimezone *floating_tz, icalcomponent *ical,
5132 struct caldav_data *cdata, struct propfind_ctx *fctx)
5133 {
5134 int pass = 0;
5135 icalcomponent *comp = NULL;
5136
5137 if (compfilter->comp_type &&
5138 (compfilter->comp_type != cdata->comp_type)) return 0;
5139 if (ical) {
5140 if (compfilter->kind == ICAL_VCALENDAR_COMPONENT) comp = ical;
5141 else comp = icalcomponent_get_first_component(ical, compfilter->kind);
5142 }
5143
5144 /* XXX Do we need to handle X- components?
5145 It doesn't appear that libical currently deals with them.
5146 */
5147
5148 if (compfilter->not_defined) return (comp == NULL);
5149
5150 if (!(compfilter->range || compfilter->prop || compfilter->comp)) return 1;
5151
5152 /* Test each instance of this component (logical OR) */
5153 do {
5154 struct prop_filter *propfilter;
5155 struct comp_filter *subfilter;
5156
5157 pass = compfilter->allof;
5158
5159 if (compfilter->range) {
5160 pass = apply_comp_timerange(compfilter, floating_tz, comp, cdata, fctx);
5161 }
5162
5163 /* Apply each prop-filter, breaking if allof fails or anyof succeeds */
5164 for (propfilter = compfilter->prop;
5165 propfilter && (pass == compfilter->allof);
5166 propfilter = propfilter->next) {
5167
5168 pass = apply_propfilter(propfilter, floating_tz, comp);
5169 }
5170
5171 /* Apply each comp-filter, breaking if allof fails or anyof succeeds */
5172 for (subfilter = compfilter->comp;
5173 subfilter && (pass == compfilter->allof);
5174 subfilter = subfilter->next) {
5175
5176 pass = apply_compfilter(subfilter, floating_tz, comp, cdata, fctx);
5177 }
5178
5179 } while (!pass &&
5180 (comp = icalcomponent_get_next_component(ical, compfilter->kind)));
5181
5182 return pass;
5183 }
5184
5185 /* See if the current resource matches the specified filter.
5186 * Returns 1 if match, 0 otherwise.
5187 */
apply_calfilter(struct propfind_ctx * fctx,void * data)5188 static int apply_calfilter(struct propfind_ctx *fctx, void *data)
5189 {
5190 struct calquery_filter *calfilter =
5191 (struct calquery_filter *) fctx->filter_crit;
5192 struct caldav_data *cdata = (struct caldav_data *) data;
5193 icalcomponent *ical = fctx->obj;
5194
5195 if (calfilter->comp_types) {
5196 /* Check if we can short-circuit based on
5197 comp-filter(s) vs component type of resource */
5198 if (!(cdata->comp_type & calfilter->comp_types)) return 0;
5199 if (calfilter->comp->allof &&
5200 (cdata->comp_type & CAL_COMP_REAL) !=
5201 (calfilter->comp_types & CAL_COMP_REAL)) {
5202 return 0;
5203 }
5204 }
5205
5206 if ((calfilter->flags & PARSE_ICAL) || cdata->comp_flags.recurring) {
5207 /* Load message containing the resource and parse iCal data */
5208 if (!ical) {
5209 if (!fctx->msg_buf.len)
5210 mailbox_map_record(fctx->mailbox, fctx->record, &fctx->msg_buf);
5211 if (!fctx->msg_buf.len) return 0;
5212
5213 ical = fctx->obj =
5214 icalparser_parse_string(buf_cstring(&fctx->msg_buf) +
5215 fctx->record->header_size);
5216 }
5217 if (!ical) return 0;
5218 }
5219
5220 return apply_compfilter(calfilter->comp, calfilter->tz, ical, cdata, fctx);
5221 }
5222
5223
free_compfilter(struct comp_filter * comp)5224 static void free_compfilter(struct comp_filter *comp)
5225 {
5226 struct comp_filter *subcomp, *nextc;
5227 struct prop_filter *prop, *nextp;
5228
5229 if (!comp) return;
5230
5231 xmlFree(comp->name);
5232 if (comp->range) free(comp->range);
5233
5234 for (prop = comp->prop; prop; prop = nextp) {
5235 nextp = prop->next;
5236
5237 if (prop->other) free(prop->other);
5238 dav_free_propfilter(prop);
5239 }
5240 for (subcomp = comp->comp; subcomp; subcomp = nextc) {
5241 nextc = subcomp->next;
5242
5243 free_compfilter(subcomp);
5244 }
5245
5246 free(comp);
5247 }
5248
5249
5250 /* dav_foreach() callback to find props on a CalDAV resource
5251 *
5252 * This function will strip any known VTIMEZONEs from the existing resource
5253 * and store as a new resource before returning properties
5254 */
caldav_propfind_by_resource(void * rock,void * data)5255 static int caldav_propfind_by_resource(void *rock, void *data)
5256 {
5257 struct propfind_ctx *fctx = (struct propfind_ctx *) rock;
5258 struct caldav_data *cdata = (struct caldav_data *) data;
5259
5260 if (sqlite3_libversion_number() < 3003008) {
5261 /* Can't write to a table while a SELECT is active */
5262 goto done;
5263 }
5264
5265 if (cdata->dav.imap_uid && !cdata->comp_flags.tzbyref) {
5266 struct index_record record;
5267 int r;
5268
5269 /* Relock index for writing */
5270 if (!mailbox_index_islocked(fctx->mailbox, 1)) {
5271 mailbox_unlock_index(fctx->mailbox, NULL);
5272 r = mailbox_lock_index(fctx->mailbox, LOCK_EXCLUSIVE);
5273 if (r) {
5274 syslog(LOG_ERR, "relock index(%s) failed: %s",
5275 fctx->mailbox->name, error_message(r));
5276 goto done;
5277 }
5278 }
5279
5280 if (!fctx->record) {
5281 /* Fetch index record for the resource */
5282 r = mailbox_find_index_record(fctx->mailbox,
5283 cdata->dav.imap_uid, &record);
5284 /* XXX Check errors */
5285
5286 fctx->record = r ? NULL : &record;
5287 }
5288
5289 if (fctx->record) {
5290 strarray_t schedule_addresses = STRARRAY_INITIALIZER;
5291 icalcomponent *ical =
5292 record_to_ical(fctx->mailbox, fctx->record, &schedule_addresses);
5293 struct transaction_t txn;
5294
5295 if (!ical) {
5296 syslog(LOG_NOTICE,
5297 "Unable to parse iCal %s:%u prior to stripping TZ",
5298 fctx->mailbox->name, fctx->record->uid);
5299 strarray_fini(&schedule_addresses);
5300 goto done;
5301 }
5302
5303 memset(&txn, 0, sizeof(struct transaction_t));
5304 txn.req_hdrs = spool_new_hdrcache();
5305
5306 caldav_store_resource(&txn, ical, fctx->mailbox,
5307 cdata->dav.resource, cdata->dav.createdmodseq,
5308 fctx->davdb,
5309 TZ_STRIP | (!cdata->sched_tag ? NEW_STAG : 0),
5310 NULL, &schedule_addresses);
5311 spool_free_hdrcache(txn.req_hdrs);
5312 buf_free(&txn.buf);
5313 strarray_fini(&schedule_addresses);
5314
5315 icalcomponent_free(ical);
5316
5317 caldav_lookup_resource(fctx->davdb, fctx->mailbox->name,
5318 cdata->dav.resource, &cdata, 0);
5319 fctx->record = NULL;
5320 }
5321 }
5322
5323 done:
5324 return propfind_by_resource(rock, data);
5325 }
5326
5327
5328 /* Callback to fetch DAV:getcontenttype */
propfind_getcontenttype(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)5329 static int propfind_getcontenttype(const xmlChar *name, xmlNsPtr ns,
5330 struct propfind_ctx *fctx,
5331 xmlNodePtr prop __attribute__((unused)),
5332 xmlNodePtr resp __attribute__((unused)),
5333 struct propstat propstat[],
5334 void *rock __attribute__((unused)))
5335 {
5336 buf_setcstr(&fctx->buf, ICALENDAR_CONTENT_TYPE);
5337
5338 if (fctx->data) {
5339 struct caldav_data *cdata = (struct caldav_data *) fctx->data;
5340 const char *comp = NULL;
5341
5342 switch (cdata->comp_type) {
5343 case CAL_COMP_VEVENT: comp = "VEVENT"; break;
5344 case CAL_COMP_VTODO: comp = "VTODO"; break;
5345 case CAL_COMP_VJOURNAL: comp = "VJOURNAL"; break;
5346 case CAL_COMP_VFREEBUSY: comp = "VFREEBUSY"; break;
5347 case CAL_COMP_VAVAILABILITY: comp = "VAVAILABILITY"; break;
5348 case CAL_COMP_VPOLL: comp = "VPOLL"; break;
5349 }
5350
5351 if (comp) buf_printf(&fctx->buf, "; component=%s", comp);
5352 }
5353
5354 xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
5355 name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);
5356
5357 return 0;
5358 }
5359
5360
5361 /* Callback to fetch DAV:resourcetype */
propfind_restype(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)5362 static int propfind_restype(const xmlChar *name, xmlNsPtr ns,
5363 struct propfind_ctx *fctx,
5364 xmlNodePtr prop __attribute__((unused)),
5365 xmlNodePtr resp,
5366 struct propstat propstat[],
5367 void *rock)
5368 {
5369 if (!propstat) {
5370 /* Prescreen "property" request */
5371 if (fctx->req_tgt->collection ||
5372 (fctx->req_tgt->userid && fctx->depth >= 1) || fctx->depth >= 2) {
5373 /* Add namespaces for possible resource types */
5374 ensure_ns(fctx->ns, NS_CALDAV, fctx->root, XML_NS_CALDAV, "C");
5375 ensure_ns(fctx->ns, NS_CS, fctx->root, XML_NS_CS, "CS");
5376 }
5377
5378 return 0;
5379 }
5380
5381 xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
5382 &propstat[PROPSTAT_OK], name, ns, NULL, 0);
5383
5384 if (!fctx->record) {
5385 xmlNewChild(node, NULL, BAD_CAST "collection", NULL);
5386
5387 if (fctx->req_tgt->collection &&
5388 fctx->mbentry->mbtype == MBTYPE_CALENDAR) {
5389 ensure_ns(fctx->ns, NS_CALDAV,
5390 resp ? resp->parent : node->parent, XML_NS_CALDAV, "C");
5391 if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX)) {
5392 xmlNewChild(node, fctx->ns[NS_CALDAV],
5393 BAD_CAST "schedule-inbox", NULL);
5394 }
5395 else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX)) {
5396 xmlNewChild(node, fctx->ns[NS_CALDAV],
5397 BAD_CAST "schedule-outbox", NULL);
5398 }
5399 else {
5400 xmlNewChild(node, fctx->ns[NS_CALDAV],
5401 BAD_CAST "calendar", NULL);
5402 if (rock) {
5403 /* Called from PROPFIND - include "shared[-owner]" type */
5404 xml_add_shareaccess(fctx, resp, node, 1 /* legacy */);
5405 }
5406 }
5407 }
5408 }
5409
5410 return 0;
5411 }
5412
5413 #define PROP_NOVALUE (1<<31)
5414
parse_partial_comp(xmlNodePtr node)5415 static struct partial_comp_t *parse_partial_comp(xmlNodePtr node)
5416 {
5417 xmlChar *prop;
5418 struct partial_comp_t *pcomp;
5419
5420 prop = xmlGetProp(node, BAD_CAST "name");
5421 if (!prop) return NULL;
5422
5423 pcomp = xzmalloc(sizeof(struct partial_comp_t));
5424 pcomp->kind = icalcomponent_string_to_kind((char *) prop);
5425 xmlFree(prop);
5426
5427 for (node = xmlFirstElementChild(node); node;
5428 node = xmlNextElementSibling(node)) {
5429 if (!xmlStrcmp(node->name, BAD_CAST "allprop")) {
5430 arrayu64_add(&pcomp->props, ICAL_ANY_PROPERTY);
5431 }
5432 else if (!xmlStrcmp(node->name, BAD_CAST "prop")) {
5433 uint64_t kind = ICAL_NO_PROPERTY;
5434
5435 prop = xmlGetProp(node, BAD_CAST "name");
5436 if (prop) {
5437 kind = icalproperty_string_to_kind((char *) prop);
5438 xmlFree(prop);
5439
5440 prop = xmlGetProp(node, BAD_CAST "novalue");
5441 if (prop) {
5442 if (!xmlStrcmp(prop, BAD_CAST "yes")) {
5443 /* Set highest order bit to encode "novalue" */
5444 kind |= PROP_NOVALUE;
5445 }
5446 xmlFree(prop);
5447 }
5448 }
5449 arrayu64_add(&pcomp->props, kind);
5450 }
5451 else if (!xmlStrcmp(node->name, BAD_CAST "allcomp")) {
5452 pcomp->child = xzmalloc(sizeof(struct partial_comp_t));
5453 pcomp->child->kind = ICAL_ANY_COMPONENT;
5454 break;
5455 }
5456 else if (!xmlStrcmp(node->name, BAD_CAST "comp")) {
5457 struct partial_comp_t *child = parse_partial_comp(node);
5458 child->sibling = pcomp->child;
5459 pcomp->child = child;
5460 }
5461 }
5462
5463 return pcomp;
5464 }
5465
prune_properties(icalcomponent * parent,struct partial_comp_t * pcomp)5466 static void prune_properties(icalcomponent *parent,
5467 struct partial_comp_t *pcomp)
5468 {
5469 icalcomponent *comp, *next;
5470 int n, size = arrayu64_size(&pcomp->props);
5471
5472 if (!size || arrayu64_nth(&pcomp->props, 0) != ICAL_ANY_PROPERTY) {
5473 /* Strip unwanted properties from component */
5474 icalproperty *prop, *nextprop;
5475
5476 for (prop = icalcomponent_get_first_property(parent, ICAL_ANY_PROPERTY);
5477 prop; prop = nextprop) {
5478 nextprop =
5479 icalcomponent_get_next_property(parent, ICAL_ANY_PROPERTY);
5480
5481 uint64_t kind;
5482
5483 for (n = 0; n < size; n++) {
5484 kind = arrayu64_nth(&pcomp->props, n);
5485 /* Highest order bit encodes "novalue" */
5486 if ((kind & ~PROP_NOVALUE) == icalproperty_isa(prop)) break;
5487 }
5488
5489 if (n >= size) {
5490 /* Don't want this property, remove it */
5491 icalcomponent_remove_property(parent, prop);
5492 icalproperty_free(prop);
5493 }
5494 else if (kind & PROP_NOVALUE) {
5495 /* Don't want value for this property, remove it */
5496 /* XXX This requires libical (<= 2.x) to be compiled with
5497 ICAL_ALLOW_EMPTY_PROPERTIES=true
5498 otherwise icalproperty_as_ical_string() will output
5499 "ERROR: No Value" as the property value */
5500 icalproperty_set_value(prop, icalvalue_new(ICAL_NO_VALUE));
5501 }
5502 }
5503 }
5504
5505 if (pcomp->child && pcomp->child->kind == ICAL_ANY_COMPONENT) return;
5506
5507 /* Strip unwanted components from component */
5508 for (comp = icalcomponent_get_first_component(parent, ICAL_ANY_COMPONENT);
5509 comp; comp = next) {
5510 icalcomponent_kind kind = icalcomponent_isa(comp);
5511 struct partial_comp_t *child;
5512
5513 next = icalcomponent_get_next_component(parent, ICAL_ANY_COMPONENT);
5514
5515 for (child = pcomp->child; child; child = child->sibling) {
5516 if (child->kind == kind) break;
5517 }
5518 if (child) prune_properties(comp, child);
5519 else {
5520 icalcomponent_remove_component(parent, comp);
5521 icalcomponent_free(comp);
5522 }
5523 }
5524 }
5525
expand_cb(icalcomponent * comp,icaltimetype start,icaltimetype end,void * rock)5526 static int expand_cb(icalcomponent *comp,
5527 icaltimetype start, icaltimetype end, void *rock)
5528 {
5529 icalcomponent *ical = icalcomponent_get_parent(comp);
5530 icalcomponent *expanded_ical = (icalcomponent *) rock;
5531 icalproperty *prop, *nextprop, *recurid = NULL;
5532 struct icaldatetimeperiodtype dtp;
5533 icaltimetype dtstart;
5534
5535 start = icaltime_convert_to_zone(start, utc_zone);
5536 end = icaltime_convert_to_zone(end, utc_zone);
5537
5538 /* Fetch/set/remove interesting properties */
5539 for (prop = icalcomponent_get_first_property(comp, ICAL_ANY_PROPERTY);
5540 prop; prop = nextprop) {
5541 nextprop = icalcomponent_get_next_property(comp, ICAL_ANY_PROPERTY);
5542
5543 switch (icalproperty_isa(prop)) {
5544 case ICAL_DTSTART_PROPERTY:
5545 /* Fetch exiting DTSTART (might be master) */
5546 dtp = icalproperty_get_datetimeperiod(prop);
5547 dtstart = icaltime_convert_to_zone(dtp.time, utc_zone);
5548
5549 /* Set DTSTART to be for this occurrence (in UTC) */
5550 icalproperty_set_dtstart(prop, start);
5551 icalproperty_remove_parameter_by_kind(prop, ICAL_TZID_PARAMETER);
5552 break;
5553
5554 case ICAL_DTEND_PROPERTY:
5555 /* Set DTEND to be for this occurrence (in UTC) */
5556 icalproperty_set_dtend(prop, end);
5557 icalproperty_remove_parameter_by_kind(prop, ICAL_TZID_PARAMETER);
5558 break;
5559
5560 case ICAL_DURATION_PROPERTY:
5561 /* Set DURATION to be for this occurrence */
5562 icalproperty_set_duration(prop, icaltime_subtract(end, start));
5563 break;
5564
5565 case ICAL_RECURRENCEID_PROPERTY:
5566 /* Reset RECURRENCE-ID of this override to UTC */
5567 dtp = icalproperty_get_datetimeperiod(prop);
5568 dtp.time = icaltime_convert_to_zone(dtp.time, utc_zone);
5569 icalproperty_set_recurrenceid(prop, dtp.time);
5570 icalproperty_remove_parameter_by_kind(prop, ICAL_TZID_PARAMETER);
5571 recurid = prop;
5572
5573 /* Remove component from existing ical (we can use it as-is) */
5574 icalcomponent_remove_component(ical, comp);
5575 break;
5576
5577 case ICAL_RRULE_PROPERTY:
5578 case ICAL_RDATE_PROPERTY:
5579 case ICAL_EXRULE_PROPERTY:
5580 case ICAL_EXDATE_PROPERTY:
5581 /* We don't want any recurrence rule properties */
5582 icalcomponent_remove_property(comp, prop);
5583 icalproperty_free(prop);
5584 break;
5585
5586 default:
5587 break;
5588 }
5589 }
5590
5591 if (!recurid) {
5592 /* Clone the master component */
5593 comp = icalcomponent_clone(comp);
5594 if (icaltime_compare(start, dtstart)) {
5595 /* Not the first instance - set RECURRENCE-ID */
5596 icalcomponent_set_recurrenceid(comp, start);
5597 }
5598 }
5599
5600 /* Append the component to expanded ical */
5601 icalcomponent_add_component(expanded_ical, comp);
5602
5603 return 1;
5604 }
5605
5606 /* Expand recurrences of ical in range.
5607 NOTE: expand_cb() is destructive of ical as it builds expanded_ical */
expand_caldata(icalcomponent ** ical,struct icalperiodtype range)5608 static icalcomponent *expand_caldata(icalcomponent **ical,
5609 struct icalperiodtype range)
5610 {
5611 icalcomponent *expanded_ical =
5612 icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT,
5613 icalproperty_new_version("2.0"),
5614 icalproperty_new_prodid(ical_prodid),
5615 0);
5616
5617 /* Copy over any CALSCALE property */
5618 icalproperty *prop =
5619 icalcomponent_get_first_property(*ical, ICAL_CALSCALE_PROPERTY);
5620 if (prop)
5621 icalcomponent_add_property(expanded_ical, icalproperty_clone(prop));
5622
5623 icalcomponent_myforeach(*ical, range, NULL, expand_cb, expanded_ical);
5624 icalcomponent_free(*ical);
5625 *ical = expanded_ical;
5626
5627 return *ical;
5628 }
5629
limit_caldata(icalcomponent * ical,struct icalperiodtype * limit)5630 static void limit_caldata(icalcomponent *ical, struct icalperiodtype *limit)
5631 {
5632 icaltime_span limitspan;
5633 icaltimetype dtstart, dtend, recurid;
5634 struct icaldurationtype dtduration = icaldurationtype_null_duration();
5635 icalcomponent *comp, *nextcomp;
5636 icalcomponent_kind kind;
5637
5638 limitspan.start = icaltime_as_timet_with_zone(limit->start, utc_zone);
5639 limitspan.end = icaltime_as_timet_with_zone(limit->end, utc_zone);
5640
5641 comp = icalcomponent_get_first_real_component(ical);
5642 kind = icalcomponent_isa(comp);
5643
5644 /* Find master component */
5645 for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
5646
5647 recurid = icalcomponent_get_recurrenceid_with_zone(comp);
5648
5649 if (icaltime_is_null_time(recurid)) {
5650 /* Calculate duration of master component */
5651 dtstart = icalcomponent_get_dtstart(comp);
5652 dtend = icalcomponent_get_dtend(comp);
5653 dtduration = icaltime_subtract(dtend, dtstart);
5654 break;
5655 }
5656 }
5657
5658 /* Check spans of each override against our limit span */
5659 for (comp = icalcomponent_get_first_component(ical, kind);
5660 comp; comp = nextcomp) {
5661
5662 icaltime_span recurspan;
5663
5664 nextcomp = icalcomponent_get_next_component(ical, kind);
5665 recurid = icalcomponent_get_recurrenceid_with_zone(comp);
5666
5667 /* Skip master component */
5668 if (icaltime_is_null_time(recurid)) continue;
5669
5670 /* Check span of override */
5671 dtstart = icalcomponent_get_dtstart(comp);
5672 dtend = icalcomponent_get_dtend(comp);
5673 recurspan = icaltime_span_new(dtstart, dtend, 1);
5674 if (icaltime_span_overlaps(&recurspan, &limitspan)) continue;
5675
5676 /* Check span of original occurrence */
5677 dtstart = recurid;
5678 dtend = icaltime_add(dtstart, dtduration);
5679 recurspan = icaltime_span_new(dtstart, dtend, 1);
5680 if (icaltime_span_overlaps(&recurspan, &limitspan)) continue;
5681
5682 /* Remove this component (doesn't overlap limit range) */
5683 icalcomponent_remove_component(ical, comp);
5684 icalcomponent_free(comp);
5685 }
5686 }
5687
5688 /* Callback to prescreen/fetch CALDAV:calendar-data */
propfind_scheduser(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)5689 static int propfind_scheduser(const xmlChar *name, xmlNsPtr ns,
5690 struct propfind_ctx *fctx,
5691 xmlNodePtr prop __attribute__((unused)),
5692 xmlNodePtr resp __attribute__((unused)),
5693 struct propstat propstat[],
5694 void *rock __attribute__((unused)))
5695 {
5696 struct buf buf = BUF_INITIALIZER;
5697 int rc = HTTP_NOT_FOUND;
5698
5699 if (propstat && fctx->mailbox && fctx->record) {
5700 message_t *m = message_new_from_record(fctx->mailbox, fctx->record);
5701
5702 message_get_field(m, "x-schedule-user-address",
5703 MESSAGE_DECODED|MESSAGE_TRIM, &buf);
5704
5705 message_unref(&m);
5706 }
5707
5708 if (buf.len) {
5709 rc = 0;
5710 xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
5711 name, ns, NULL, 0);
5712 strarray_t *schedule_addresses = strarray_split(buf_cstring(&buf), ",", STRARRAY_TRIM);
5713 int i;
5714 for (i = strarray_size(schedule_addresses); i; i--) {
5715 const char *address = strarray_nth(schedule_addresses, i-1);
5716 if (!strncasecmp(address, "mailto:", 7)) address += 7;
5717 buf_reset(&fctx->buf);
5718 buf_printf(&fctx->buf, "mailto:%s", address);
5719 xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
5720 }
5721 strarray_free(schedule_addresses);
5722 }
5723
5724 buf_free(&buf);
5725
5726 return rc;
5727 }
5728
5729 /* Callback to prescreen/fetch CALDAV:calendar-data */
propfind_caldata(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)5730 static int propfind_caldata(const xmlChar *name, xmlNsPtr ns,
5731 struct propfind_ctx *fctx,
5732 xmlNodePtr prop,
5733 xmlNodePtr resp __attribute__((unused)),
5734 struct propstat propstat[],
5735 void *rock __attribute__((unused)))
5736 {
5737 static struct mime_type_t *out_type = caldav_mime_types;
5738 static struct partial_caldata_t partial_caldata = { .comp = NULL };
5739 struct partial_caldata_t *partial = &partial_caldata;
5740 static unsigned need_tz = 0;
5741 const char *data = NULL;
5742 size_t datalen = 0;
5743
5744 if (!fctx) {
5745 /* Cleanup "property" request - free partial component structure */
5746 struct partial_comp_t *pcomp, *child, *sibling;
5747
5748 for (pcomp = partial->comp; pcomp; pcomp = child) {
5749 child = pcomp->child;
5750
5751 do {
5752 sibling = pcomp->sibling;
5753 arrayu64_fini(&pcomp->props);
5754 free(pcomp);
5755 } while ((pcomp = sibling));
5756 }
5757
5758 partial->comp = NULL;
5759
5760 return 0;
5761 }
5762
5763 if (!propstat) {
5764 /* Prescreen "property" request - read partial/expand children */
5765 xmlNodePtr node;
5766
5767 /* Check for optional CalDAV-Timezones header */
5768 const char **hdr =
5769 spool_getheader(fctx->txn->req_hdrs, "CalDAV-Timezones");
5770 if (hdr && !strcmp(hdr[0], "T")) need_tz = 1;
5771 else need_tz = 0;
5772
5773 /* Initialize expand to be "empty" */
5774 partial->range.start = icaltime_null_time();
5775 partial->range.end = icaltime_null_time();
5776 partial->comp = NULL;
5777
5778 /* Check for and parse child elements of CALDAV:calendar-data */
5779 for (node = xmlFirstElementChild(prop); node;
5780 node = xmlNextElementSibling(node)) {
5781 xmlChar *prop;
5782
5783 if (!xmlStrcmp(node->name, BAD_CAST "expand") ||
5784 !xmlStrcmp(node->name, BAD_CAST "limit-recurrence-set")) {
5785 partial->expand = (node->name[0] == 'e');
5786 prop = xmlGetProp(node, BAD_CAST "start");
5787 if (!prop) return (*fctx->ret = HTTP_BAD_REQUEST);
5788 partial->range.start = icaltime_from_string((char *) prop);
5789 xmlFree(prop);
5790
5791 prop = xmlGetProp(node, BAD_CAST "end");
5792 if (!prop) return (*fctx->ret = HTTP_BAD_REQUEST);
5793 partial->range.end = icaltime_from_string((char *) prop);
5794 xmlFree(prop);
5795 }
5796 else if (!xmlStrcmp(node->name, BAD_CAST "comp")) {
5797 partial->comp = parse_partial_comp(node);
5798 if (!partial->comp ||
5799 partial->comp->kind != ICAL_VCALENDAR_COMPONENT) {
5800 return (*fctx->ret = HTTP_BAD_REQUEST);
5801 }
5802 }
5803 else if (!xmlStrcmp(node->name, BAD_CAST "limit-freebusy-set")) {
5804 syslog(LOG_NOTICE,
5805 "Client attempted to use CALDAV:limit-freebusy-set");
5806 return (*fctx->ret = HTTP_NOT_IMPLEMENTED);
5807 }
5808 }
5809
5810 if (namespace_calendar.allow & ALLOW_CAL_NOTZ) {
5811 /* We want to strip known VTIMEZONEs */
5812 fctx->proc_by_resource = &caldav_propfind_by_resource;
5813 }
5814 }
5815 else {
5816 struct caldav_data *cdata = (struct caldav_data *) fctx->data;
5817 icalcomponent *ical = NULL;
5818
5819 if (fctx->txn->meth != METH_REPORT) return HTTP_FORBIDDEN;
5820
5821 if (!out_type->content_type) return HTTP_BAD_MEDIATYPE;
5822
5823 if (!fctx->record) return HTTP_NOT_FOUND;
5824
5825 if (!fctx->msg_buf.len)
5826 mailbox_map_record(fctx->mailbox, fctx->record, &fctx->msg_buf);
5827 if (!fctx->msg_buf.len) return HTTP_SERVER_ERROR;
5828
5829 data = fctx->msg_buf.s + fctx->record->header_size;
5830 datalen = fctx->record->size - fctx->record->header_size;
5831
5832 if (cdata->comp_flags.tzbyref) {
5833 if (need_tz) {
5834 /* Add VTIMEZONE components for known TZIDs */
5835 if (!fctx->obj) fctx->obj = icalparser_parse_string(data);
5836 ical = fctx->obj;
5837
5838 icalcomponent_add_required_timezones(ical);
5839 }
5840 }
5841 else if (!need_tz && (namespace_calendar.allow & ALLOW_CAL_NOTZ)) {
5842 /* Strip all VTIMEZONE components for known TZIDs */
5843 if (!fctx->obj) fctx->obj = icalparser_parse_string(data);
5844 ical = fctx->obj;
5845
5846 strip_vtimezones(ical);
5847 }
5848
5849 /* Personalize resource, if necessary */
5850 if (namespace_calendar.allow & ALLOW_USERDATA) {
5851 struct buf userdata = BUF_INITIALIZER;
5852
5853 if (caldav_is_personalized(fctx->mailbox, fctx->data,
5854 httpd_userid, &userdata)) {
5855 if (!fctx->obj) fctx->obj = icalparser_parse_string(data);
5856 ical = fctx->obj;
5857
5858 add_personal_data(ical, &userdata);
5859 buf_free(&userdata);
5860 }
5861 }
5862
5863 if (!icaltime_is_null_time(partial->range.start)) {
5864 /* Expand/limit recurrence set */
5865 if (!fctx->obj) fctx->obj = icalparser_parse_string(data);
5866 ical = fctx->obj;
5867
5868 if (partial->expand) {
5869 fctx->obj = expand_caldata(&ical, partial->range);
5870 }
5871 else limit_caldata(ical, &partial->range);
5872 }
5873
5874 if (partial->comp) {
5875 /* Limit returned properties */
5876 if (!fctx->obj) fctx->obj = icalparser_parse_string(data);
5877 ical = fctx->obj;
5878 prune_properties(ical, partial->comp);
5879 }
5880
5881 if (ical) {
5882 /* Create iCalendar data from new ical component */
5883 data = icalcomponent_as_ical_string(ical);
5884 datalen = strlen(data);
5885 }
5886 }
5887
5888 return propfind_getdata(name, ns, fctx, prop, propstat, caldav_mime_types,
5889 &out_type, data, datalen);
5890 }
5891
5892
5893 /* Callback to fetch CALDAV:calendar-home-set,
5894 * CALDAV:schedule-inbox-URL, CALDAV:schedule-outbox-URL,
5895 * and CALDAV:schedule-default-calendar-URL
5896 */
propfind_calurl(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)5897 int propfind_calurl(const xmlChar *name, xmlNsPtr ns,
5898 struct propfind_ctx *fctx,
5899 xmlNodePtr prop,
5900 xmlNodePtr resp __attribute__((unused)),
5901 struct propstat propstat[], void *rock)
5902 {
5903 const char *cal = (const char *) rock;
5904 xmlNodePtr node;
5905 /* NOTE: calbuf needs to stay in scope until 'cal' is finished with */
5906 struct buf calbuf = BUF_INITIALIZER;
5907 int ret = HTTP_NOT_FOUND; /* error condition if we bail early */
5908
5909 if (!(namespace_calendar.enabled && httpd_userid))
5910 goto done;
5911
5912 if (cal) {
5913 /* named calendars are only used for scheduling */
5914 if (!(namespace_calendar.allow & ALLOW_CAL_SCHED))
5915 goto done;
5916
5917 /* check for renamed calendars - property on the homeset */
5918 const char *annotname = NULL;
5919 if (!strcmp(cal, SCHED_DEFAULT)) {
5920 annotname =
5921 DAV_ANNOT_NS "<" XML_NS_CALDAV ">schedule-default-calendar";
5922 }
5923 else if (!strcmp(cal, SCHED_INBOX))
5924 annotname = DAV_ANNOT_NS "<" XML_NS_CALDAV ">schedule-inbox";
5925 else if (!strcmp(cal, SCHED_OUTBOX))
5926 annotname = DAV_ANNOT_NS "<" XML_NS_CALDAV ">schedule-outbox";
5927
5928 if (annotname) {
5929 char *mailboxname = caldav_mboxname(httpd_userid, NULL);
5930 int r = annotatemore_lookupmask(mailboxname, annotname,
5931 httpd_userid, &calbuf);
5932 free(mailboxname);
5933 if (!r && calbuf.len) {
5934 buf_putc(&calbuf, '/');
5935 cal = buf_cstring(&calbuf);
5936 }
5937 }
5938 }
5939
5940 node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
5941 name, ns, NULL, 0);
5942
5943 buf_reset(&fctx->buf);
5944 if (strchr(httpd_userid, '@') || !httpd_extradomain) {
5945 buf_printf(&fctx->buf, "%s/%s/%s/", namespace_calendar.prefix,
5946 USER_COLLECTION_PREFIX, httpd_userid);
5947 }
5948 else {
5949 buf_printf(&fctx->buf, "%s/%s/%s@%s/", namespace_calendar.prefix,
5950 USER_COLLECTION_PREFIX, httpd_userid, httpd_extradomain);
5951 }
5952 if (cal) buf_appendcstr(&fctx->buf, cal);
5953
5954 if ((fctx->mode == PROPFIND_EXPAND) && xmlFirstElementChild(prop)) {
5955 /* Return properties for this URL */
5956 expand_property(prop, fctx, &namespace_calendar, buf_cstring(&fctx->buf),
5957 &caldav_parse_path, caldav_props, node, cal ? 1 : 0);
5958 }
5959 else {
5960 /* Return just the URL */
5961 xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
5962 }
5963
5964 ret = 0;
5965 done:
5966 buf_free(&calbuf);
5967
5968 return ret;
5969 }
5970
5971
5972 /* Callback to fetch CALDAV:schedule-default-calendar-URL */
propfind_scheddefault(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)5973 static int propfind_scheddefault(const xmlChar *name, xmlNsPtr ns,
5974 struct propfind_ctx *fctx,
5975 xmlNodePtr prop, xmlNodePtr resp,
5976 struct propstat propstat[],
5977 void *rock __attribute__((unused)))
5978 {
5979 /* Only defined on CALDAV:schedule-inbox-URL */
5980 if (fctx->req_tgt->flags != TGT_SCHED_INBOX) return HTTP_NOT_FOUND;
5981
5982 return propfind_calurl(name, ns, fctx,
5983 prop, resp, propstat, SCHED_DEFAULT);
5984 }
5985
5986
5987 /* Callback to fetch CALDAV:supported-calendar-component-set[s] */
propfind_calcompset(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)5988 static int propfind_calcompset(const xmlChar *name, xmlNsPtr ns,
5989 struct propfind_ctx *fctx,
5990 xmlNodePtr prop __attribute__((unused)),
5991 xmlNodePtr resp __attribute__((unused)),
5992 struct propstat propstat[],
5993 void *rock __attribute__((unused)))
5994 {
5995 struct buf attrib = BUF_INITIALIZER;
5996 const char *prop_annot =
5997 DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
5998 unsigned long types = 0;
5999 xmlNodePtr set, node;
6000 const struct cal_comp_t *comp;
6001 int r = 0;
6002
6003 if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;
6004
6005 r = annotatemore_lookupmask(fctx->mbentry->name, prop_annot,
6006 httpd_userid, &attrib);
6007 if (r) return HTTP_SERVER_ERROR;
6008
6009 if (attrib.len) {
6010 types = strtoul(buf_cstring(&attrib), NULL, 10);
6011 }
6012 else {
6013 types = -1; /* ALL components types */
6014
6015 #ifndef IOPTEST /* CalConnect ioptest */
6016 /* Apple clients don't like VPOLL */
6017 types &= ~CAL_COMP_VPOLL;
6018 #endif
6019 }
6020
6021 buf_free(&attrib);
6022
6023 if (!types) return HTTP_NOT_FOUND;
6024
6025 set = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
6026 name, ns, NULL, 0);
6027 if (!resp) {
6028 ensure_ns(fctx->ns, NS_CALDAV, set->parent, XML_NS_CALDAV, "C");
6029 xmlSetNs(set, fctx->ns[NS_CALDAV]);
6030 }
6031
6032 /* Create "comp" elements from the stored bitmask */
6033 for (comp = cal_comps; comp->name; comp++) {
6034 if (types & comp->type) {
6035 node = xmlNewChild(set, fctx->ns[NS_CALDAV],
6036 BAD_CAST "comp", NULL);
6037 xmlNewProp(node, BAD_CAST "name", BAD_CAST comp->name);
6038 }
6039 }
6040
6041 return 0;
6042 }
6043
6044
6045 /* Callback to write supported-calendar-component-set property */
proppatch_calcompset(xmlNodePtr prop,unsigned set,struct proppatch_ctx * pctx,struct propstat propstat[],void * rock)6046 static int proppatch_calcompset(xmlNodePtr prop, unsigned set,
6047 struct proppatch_ctx *pctx,
6048 struct propstat propstat[],
6049 void *rock __attribute__((unused)))
6050 {
6051 unsigned precond = 0, force = 0;
6052 xmlChar *attr = xmlGetProp(prop, BAD_CAST "force");
6053
6054 /* Check if we want to force changing of a protected property.
6055 This is mainly for our list_calendars() JavaScript client. */
6056 if (attr) {
6057 if (!xmlStrcmp(attr, BAD_CAST "yes") &&
6058 mboxname_userownsmailbox(httpd_userid, pctx->mailbox->name)) {
6059 force = 1;
6060 }
6061 xmlFree(attr);
6062 }
6063
6064 if (set && (force || (pctx->txn->meth != METH_PROPPATCH))) {
6065 /* "Writeable" for MKCOL/MKCALENDAR only */
6066 xmlNodePtr cur;
6067 unsigned long types = 0;
6068
6069 /* Work through the given list of components */
6070 for (cur = prop->children; cur; cur = cur->next) {
6071 xmlChar *name;
6072 const struct cal_comp_t *comp;
6073
6074 /* Make sure its a "comp" element with a "name" */
6075 if (cur->type != XML_ELEMENT_NODE) continue;
6076 if (xmlStrcmp(cur->name, BAD_CAST "comp") ||
6077 !(name = xmlGetProp(cur, BAD_CAST "name"))) break;
6078
6079 /* Make sure we have a valid component type */
6080 for (comp = cal_comps;
6081 comp->name && xmlStrcmp(name, BAD_CAST comp->name); comp++);
6082 xmlFree(name);
6083
6084 if (comp->name) types |= comp->type; /* found match in our list */
6085 else break; /* no match - invalid type */
6086 }
6087
6088 if (!cur) {
6089 /* All component types are valid */
6090 char typestr[(sizeof(unsigned long) * 8) / 3 + 1];
6091 sprintf(typestr, "%lu", types);
6092
6093 proppatch_todb(prop, set, pctx, propstat, (void *) typestr);
6094
6095 return 0;
6096 }
6097
6098 /* Invalid component type */
6099 precond = CALDAV_SUPP_COMP;
6100 }
6101 else {
6102 /* Protected property */
6103 precond = DAV_PROT_PROP;
6104 }
6105
6106 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID],
6107 prop->name, prop->ns, NULL, precond);
6108
6109 *pctx->ret = HTTP_FORBIDDEN;
6110
6111 return 0;
6112 }
6113
6114 /* Callback to fetch CALDAV:supported-calendar-data */
propfind_suppcaldata(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6115 static int propfind_suppcaldata(const xmlChar *name, xmlNsPtr ns,
6116 struct propfind_ctx *fctx,
6117 xmlNodePtr prop __attribute__((unused)),
6118 xmlNodePtr resp __attribute__((unused)),
6119 struct propstat propstat[],
6120 void *rock __attribute__((unused)))
6121 {
6122 xmlNodePtr node;
6123 struct mime_type_t *mime;
6124
6125 if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;
6126
6127 node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
6128 name, ns, NULL, 0);
6129
6130 for (mime = caldav_mime_types; mime->content_type; mime++) {
6131 xmlNodePtr type = xmlNewChild(node, fctx->ns[NS_CALDAV],
6132 BAD_CAST "calendar-data", NULL);
6133
6134 /* Trim any charset from content-type */
6135 buf_reset(&fctx->buf);
6136 buf_printf(&fctx->buf, "%.*s",
6137 (int) strcspn(mime->content_type, ";"), mime->content_type);
6138
6139 xmlNewProp(type, BAD_CAST "content-type",
6140 BAD_CAST buf_cstring(&fctx->buf));
6141
6142 if (mime->version)
6143 xmlNewProp(type, BAD_CAST "version", BAD_CAST mime->version);
6144 }
6145
6146 return 0;
6147 }
6148
6149
6150 /* Callback to fetch CALDAV:max-resource-size */
propfind_maxsize(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6151 static int propfind_maxsize(const xmlChar *name, xmlNsPtr ns,
6152 struct propfind_ctx *fctx,
6153 xmlNodePtr prop __attribute__((unused)),
6154 xmlNodePtr resp __attribute__((unused)),
6155 struct propstat propstat[],
6156 void *rock __attribute__((unused)))
6157 {
6158 static int maxsize = 0;
6159
6160 if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;
6161
6162 if (!maxsize) {
6163 maxsize = config_getint(IMAPOPT_MAXMESSAGESIZE);
6164 if (!maxsize) maxsize = INT_MAX;
6165 }
6166
6167 buf_reset(&fctx->buf);
6168 buf_printf(&fctx->buf, "%d", maxsize);
6169 xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
6170 name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);
6171
6172 return 0;
6173 }
6174
6175
6176 /* Callback to fetch CALDAV:min/max-date-time */
propfind_minmaxdate(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6177 static int propfind_minmaxdate(const xmlChar *name, xmlNsPtr ns,
6178 struct propfind_ctx *fctx,
6179 xmlNodePtr prop __attribute__((unused)),
6180 xmlNodePtr resp __attribute__((unused)),
6181 struct propstat propstat[],
6182 void *rock)
6183 {
6184 struct icaltimetype date;
6185
6186 if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;
6187
6188 date = icaltime_from_timet_with_zone(*((time_t *) rock), 0, utc_zone);
6189
6190 xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
6191 name, ns, BAD_CAST icaltime_as_ical_string(date), 0);
6192
6193 return 0;
6194 }
6195
6196
6197 /* Callback to fetch CALDAV:schedule-tag */
propfind_schedtag(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6198 static int propfind_schedtag(const xmlChar *name, xmlNsPtr ns,
6199 struct propfind_ctx *fctx,
6200 xmlNodePtr prop __attribute__((unused)),
6201 xmlNodePtr resp __attribute__((unused)),
6202 struct propstat propstat[],
6203 void *rock __attribute__((unused)))
6204 {
6205 struct caldav_data *cdata = (struct caldav_data *) fctx->data;
6206
6207 if (!cdata->organizer || !cdata->sched_tag) return HTTP_NOT_FOUND;
6208
6209 /* add DQUOTEs */
6210 buf_reset(&fctx->buf);
6211 buf_printf(&fctx->buf, "\"%s\"", cdata->sched_tag);
6212
6213 xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
6214 name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);
6215
6216 return 0;
6217 }
6218
propfind_caluseraddr_all(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock,int isemail)6219 static int propfind_caluseraddr_all(const xmlChar *name, xmlNsPtr ns,
6220 struct propfind_ctx *fctx,
6221 xmlNodePtr prop __attribute__((unused)),
6222 xmlNodePtr resp __attribute__((unused)),
6223 struct propstat propstat[],
6224 void *rock __attribute__((unused)),
6225 int isemail)
6226 {
6227 const char *annotname =
6228 DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-user-address-set";
6229 xmlNodePtr node;
6230 int r, ret = HTTP_NOT_FOUND;
6231
6232 if (!(namespace_calendar.enabled && fctx->req_tgt->userid))
6233 return HTTP_NOT_FOUND;
6234
6235 if (fctx->req_tgt->namespace->id == URL_NS_PRINCIPAL) {
6236 char *mailboxname = caldav_mboxname(fctx->req_tgt->userid, NULL);
6237
6238 node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
6239 name, ns, NULL, 0);
6240
6241 buf_reset(&fctx->buf);
6242 r = annotatemore_lookupmask(mailboxname, annotname,
6243 fctx->req_tgt->userid, &fctx->buf);
6244 free(mailboxname);
6245 if (!r && fctx->buf.len) {
6246 strarray_t *items = strarray_split(buf_cstring(&fctx->buf), ",", STRARRAY_TRIM);
6247 if (isemail) {
6248 xml_add_href(node, fctx->ns[NS_DAV], strarray_nth(items, 0));
6249 }
6250 else {
6251 int i;
6252 for (i = strarray_size(items); i; i--) {
6253 xmlNodePtr href = xml_add_href(node, fctx->ns[NS_DAV], strarray_nth(items, i-1));
6254 /* apple will use the alphabetically first href, and Thunderbird will use the
6255 * last one in order, so we set preferred for Apple, and put the preferred one
6256 * last for Thunderbird (and maybe others) */
6257 if (i == 1) xmlNewProp(href, BAD_CAST "preferred", BAD_CAST "1");
6258 }
6259 strarray_free(items);
6260 }
6261 }
6262
6263 /* XXX This needs to be done via an LDAP/DB lookup */
6264 else if (strchr(fctx->req_tgt->userid, '@')) {
6265 buf_reset(&fctx->buf);
6266 buf_printf(&fctx->buf, "mailto:%s", fctx->req_tgt->userid);
6267 xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
6268 }
6269
6270 else if (httpd_extradomain) {
6271 buf_reset(&fctx->buf);
6272 buf_printf(&fctx->buf, "mailto:%s@%s",
6273 fctx->req_tgt->userid, httpd_extradomain);
6274 xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
6275 }
6276
6277 else {
6278 struct strlist *domains;
6279 for (domains = cua_domains; domains; domains = domains->next) {
6280 buf_reset(&fctx->buf);
6281 buf_printf(&fctx->buf, "mailto:%s@%s",
6282 fctx->req_tgt->userid, domains->s);
6283
6284 xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
6285 }
6286 }
6287
6288 ret = 0;
6289 }
6290 else {
6291 buf_reset(&fctx->buf);
6292 r = annotatemore_lookupmask(fctx->mbentry->name, annotname,
6293 fctx->req_tgt->userid, &fctx->buf);
6294 if (!r && fctx->buf.len) {
6295 strarray_t *addr =
6296 strarray_split(buf_cstring(&fctx->buf), ",", STRARRAY_TRIM);
6297
6298 node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
6299 &propstat[PROPSTAT_OK], name, ns, NULL, 0);
6300 int i;
6301 for (i = strarray_size(addr); i; i--) {
6302 xml_add_href(node, fctx->ns[NS_DAV], strarray_nth(addr, i-1));
6303 }
6304 strarray_free(addr);
6305 ret = 0;
6306 }
6307 }
6308
6309 return ret;
6310 }
6311
6312 /* Callback to fetch CALDAV:calendar-user-address-set */
propfind_caluseraddr(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6313 EXPORTED int propfind_caluseraddr(const xmlChar *name, xmlNsPtr ns,
6314 struct propfind_ctx *fctx,
6315 xmlNodePtr prop,
6316 xmlNodePtr resp,
6317 struct propstat propstat[],
6318 void *rock)
6319 {
6320 return propfind_caluseraddr_all(name, ns, fctx, prop, resp, propstat, rock, 0);
6321 }
6322
6323 /* Callback to fetch APPLE:calendar-email-set */
propfind_caluseremail(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6324 EXPORTED int propfind_caluseremail(const xmlChar *name, xmlNsPtr ns,
6325 struct propfind_ctx *fctx,
6326 xmlNodePtr prop,
6327 xmlNodePtr resp,
6328 struct propstat propstat[],
6329 void *rock)
6330 {
6331 return propfind_caluseraddr_all(name, ns, fctx, prop, resp, propstat, rock, 1);
6332 }
6333
6334
6335 /* Callback to write CALDAV:calendar-user-address-set */
proppatch_caluseraddr(xmlNodePtr prop,unsigned set,struct proppatch_ctx * pctx,struct propstat propstat[],void * rock)6336 int proppatch_caluseraddr(xmlNodePtr prop, unsigned set,
6337 struct proppatch_ctx *pctx,
6338 struct propstat propstat[],
6339 void *rock __attribute__((unused)))
6340 {
6341 struct mailbox *mailbox = pctx->mailbox;
6342 struct mailbox *calhomeset = NULL;
6343
6344 if (pctx->txn->req_tgt.namespace->id == URL_NS_PRINCIPAL) {
6345 /* We have been storing CUAS on cal-home-set, NOT INBOX */
6346 char *mboxname = caldav_mboxname(pctx->txn->req_tgt.userid, NULL);
6347 int r = 0;
6348
6349 if (!mailbox || strcmp(mboxname, mailbox->name)) {
6350 r = mailbox_open_iwl(mboxname, &calhomeset);
6351 if (!r) pctx->mailbox = calhomeset;
6352 }
6353 free(mboxname);
6354
6355 if (r) {
6356 xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
6357 &propstat[PROPSTAT_ERROR],
6358 prop->name, prop->ns, NULL, 0);
6359 *pctx->ret = HTTP_SERVER_ERROR;
6360 return 0;
6361 }
6362 }
6363
6364 /* Make sure this is on a collection and the user has admin rights */
6365 if (pctx->txn->req_tgt.resource ||
6366 !(cyrus_acl_myrights(httpd_authstate, pctx->mailbox->acl) & DACL_ADMIN)) {
6367 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6368 &propstat[PROPSTAT_FORBID],
6369 prop->name, prop->ns, NULL, 0);
6370
6371 *pctx->ret = HTTP_FORBIDDEN;
6372 }
6373 else {
6374 char *value = NULL;
6375
6376 if (set) {
6377 xmlNodePtr node = xmlFirstElementChild(prop);
6378
6379 /* Find the value */
6380 if (!node) {
6381 /* single text value */
6382 value = (char *) xmlNodeGetContent(prop);
6383 }
6384 else {
6385 /* href(s) */
6386 strarray_t addr = STRARRAY_INITIALIZER;
6387
6388 for (; node; node = xmlNextElementSibling(node)) {
6389 /* Make sure its a value we understand */
6390 if (!xmlStrcmp(node->name, BAD_CAST "href")) {
6391 /* because clients look for the last item, we put the default last,
6392 * but we want it first in the internal data structure because that
6393 * makes iterating to look for matches more sensible, so reverse it
6394 * right here! */
6395 strarray_unshiftm(&addr, (char *) xmlNodeGetContent(node));
6396 }
6397 else {
6398 /* Unknown value */
6399 xml_add_prop(HTTP_CONFLICT, pctx->ns[NS_DAV],
6400 &propstat[PROPSTAT_CONFLICT],
6401 prop->name, prop->ns, NULL, 0);
6402
6403 strarray_fini(&addr);
6404 *pctx->ret = HTTP_FORBIDDEN;
6405
6406 return 0;
6407 }
6408 }
6409
6410 value = strarray_join(&addr, ",");
6411 strarray_fini(&addr);
6412 }
6413 }
6414
6415 proppatch_todb(prop, set, pctx, propstat, (void *) value);
6416 free(value);
6417 }
6418
6419 if (calhomeset) {
6420 mailbox_close(&calhomeset);
6421 pctx->mailbox = mailbox;
6422 }
6423
6424 return 0;
6425 }
6426
6427 /* Callback to fetch CALDAV:calendar-user-type */
propfind_calusertype(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6428 int propfind_calusertype(const xmlChar *name, xmlNsPtr ns,
6429 struct propfind_ctx *fctx,
6430 xmlNodePtr prop __attribute__((unused)),
6431 xmlNodePtr resp __attribute__((unused)),
6432 struct propstat propstat[],
6433 void *rock __attribute__((unused)))
6434 {
6435 const char *type = fctx->req_tgt->userid ? "INDIVIDUAL" : NULL;
6436
6437 if (!namespace_calendar.enabled || !type) return HTTP_NOT_FOUND;
6438
6439 xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
6440 name, ns, BAD_CAST type, 0);
6441
6442 return 0;
6443 }
6444
6445
6446 /* Callback to write schedule-calendar-transp property */
proppatch_caltransp(xmlNodePtr prop,unsigned set,struct proppatch_ctx * pctx,struct propstat propstat[],void * rock)6447 static int proppatch_caltransp(xmlNodePtr prop, unsigned set,
6448 struct proppatch_ctx *pctx,
6449 struct propstat propstat[],
6450 void *rock __attribute__((unused)))
6451 {
6452 if (pctx->txn->req_tgt.collection && !pctx->txn->req_tgt.resource) {
6453 const xmlChar *value = NULL;
6454
6455 if (set) {
6456 xmlNodePtr cur;
6457
6458 /* Find the value */
6459 for (cur = prop->children; cur; cur = cur->next) {
6460
6461 /* Make sure its a value we understand */
6462 if (cur->type != XML_ELEMENT_NODE) continue;
6463 if (!xmlStrcmp(cur->name, BAD_CAST "opaque") ||
6464 !xmlStrcmp(cur->name, BAD_CAST "transparent")) {
6465 value = cur->name;
6466 break;
6467 }
6468 else {
6469 /* Unknown value */
6470 xml_add_prop(HTTP_CONFLICT, pctx->ns[NS_DAV],
6471 &propstat[PROPSTAT_CONFLICT],
6472 prop->name, prop->ns, NULL, 0);
6473
6474 *pctx->ret = HTTP_FORBIDDEN;
6475
6476 return 0;
6477 }
6478 }
6479 }
6480
6481 proppatch_todb(prop, set, pctx, propstat, (void *) value);
6482 }
6483 else {
6484 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6485 &propstat[PROPSTAT_FORBID],
6486 prop->name, prop->ns, NULL, 0);
6487
6488 *pctx->ret = HTTP_FORBIDDEN;
6489 }
6490
6491 return 0;
6492 }
6493
6494
6495 /* Callback to prescreen/fetch CALDAV:calendar-timezone */
propfind_timezone(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6496 static int propfind_timezone(const xmlChar *name, xmlNsPtr ns,
6497 struct propfind_ctx *fctx,
6498 xmlNodePtr prop,
6499 xmlNodePtr resp __attribute__((unused)),
6500 struct propstat propstat[],
6501 void *rock __attribute__((unused)))
6502 {
6503 static struct mime_type_t *out_type = caldav_mime_types;
6504 struct buf attrib = BUF_INITIALIZER;
6505 const char *data = NULL;
6506 size_t datalen = 0;
6507 int r = 0;
6508
6509 if (propstat) {
6510 const char *prop_annot =
6511 DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone";
6512
6513 if (!out_type->content_type) return HTTP_BAD_MEDIATYPE;
6514
6515 if (fctx->mailbox && !fctx->record) {
6516 r = annotatemore_lookupmask(fctx->mailbox->name, prop_annot,
6517 httpd_userid, &attrib);
6518 }
6519
6520 if (r) r = HTTP_SERVER_ERROR;
6521 else if (attrib.len) {
6522 data = buf_cstring(&attrib);
6523 datalen = attrib.len;
6524 }
6525 else if ((namespace_calendar.allow & ALLOW_CAL_NOTZ)) {
6526 /* Check for CALDAV:calendar-timezone-id */
6527 prop_annot = DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone-id";
6528
6529 buf_free(&attrib);
6530 r = annotatemore_lookupmask(fctx->mailbox->name, prop_annot,
6531 httpd_userid, &attrib);
6532
6533 if (r) r = HTTP_SERVER_ERROR;
6534 else if (!attrib.len) r = HTTP_NOT_FOUND;
6535 else {
6536 /* Fetch tz from builtin repository */
6537 icaltimezone *tz =
6538 icaltimezone_get_builtin_timezone(buf_cstring(&attrib));
6539
6540 if (tz) {
6541 icalcomponent *vtz = icaltimezone_get_component(tz);
6542 icalcomponent *ical =
6543 icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT,
6544 icalproperty_new_version("2.0"),
6545 icalproperty_new_prodid(ical_prodid),
6546 vtz,
6547 0);
6548
6549 data = icalcomponent_as_ical_string(ical);
6550 datalen = strlen(data);
6551
6552 icalcomponent_remove_component(ical, vtz);
6553 icalcomponent_free(ical);
6554 }
6555 else r = HTTP_SERVER_ERROR;
6556 }
6557 }
6558 else r = HTTP_NOT_FOUND;
6559 }
6560
6561 if (!r) r = propfind_getdata(name, ns, fctx, prop, propstat,
6562 caldav_mime_types, &out_type,
6563 data, datalen);
6564
6565 buf_free(&attrib);
6566
6567 return r;
6568 }
6569
6570
6571 /* Callback to write calendar-timezone property */
proppatch_timezone(xmlNodePtr prop,unsigned set,struct proppatch_ctx * pctx,struct propstat propstat[],void * rock)6572 static int proppatch_timezone(xmlNodePtr prop, unsigned set,
6573 struct proppatch_ctx *pctx,
6574 struct propstat propstat[],
6575 void *rock __attribute__((unused)))
6576 {
6577 if (pctx->txn->req_tgt.collection && !pctx->txn->req_tgt.resource) {
6578 xmlChar *type, *ver = NULL, *freeme = NULL;
6579 const char *tz = NULL;
6580 struct mime_type_t *mime;
6581 unsigned valid = 1;
6582
6583 type = xmlGetProp(prop, BAD_CAST "content-type");
6584 if (type) ver = xmlGetProp(prop, BAD_CAST "version");
6585
6586 /* Check/find requested MIME type */
6587 for (mime = caldav_mime_types; type && mime->content_type; mime++) {
6588 if (is_mediatype(mime->content_type, (const char *) type)) {
6589 if (ver &&
6590 (!mime->version || xmlStrcmp(ver, BAD_CAST mime->version))) {
6591 continue;
6592 }
6593 break;
6594 }
6595 }
6596
6597 if (!mime || !mime->content_type) {
6598 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6599 &propstat[PROPSTAT_FORBID],
6600 prop->name, prop->ns, NULL,
6601 CALDAV_SUPP_DATA);
6602 *pctx->ret = HTTP_FORBIDDEN;
6603 valid = 0;
6604 }
6605 else if (set) {
6606 icalcomponent *ical = NULL;
6607 struct buf buf = BUF_INITIALIZER;
6608
6609 freeme = xmlNodeGetContent(prop);
6610 tz = (const char *) freeme;
6611
6612 /* Parse and validate the iCal data */
6613 buf_init_ro_cstr(&buf, tz);
6614 ical = mime->to_object(&buf);
6615 if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) {
6616 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6617 &propstat[PROPSTAT_FORBID],
6618 prop->name, prop->ns, NULL,
6619 CALDAV_VALID_DATA);
6620 *pctx->ret = HTTP_FORBIDDEN;
6621 valid = 0;
6622 }
6623 else if (!icalcomponent_get_first_component(ical,
6624 ICAL_VTIMEZONE_COMPONENT)
6625 || icalcomponent_get_first_real_component(ical)) {
6626 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6627 &propstat[PROPSTAT_FORBID],
6628 prop->name, prop->ns, NULL,
6629 CALDAV_VALID_OBJECT);
6630 *pctx->ret = HTTP_FORBIDDEN;
6631 valid = 0;
6632 }
6633 else if (mime != caldav_mime_types) {
6634 tz = icalcomponent_as_ical_string(ical);
6635 }
6636
6637 if (ical) icalcomponent_free(ical);
6638 buf_free(&buf);
6639 }
6640
6641 if (valid) {
6642 /* Remove CALDAV:calendar-timezone-id */
6643 const char *prop_annot =
6644 DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone-id";
6645 annotate_state_t *astate = NULL;
6646 int r;
6647
6648 buf_reset(&pctx->buf);
6649 r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate);
6650 if (!r) r = annotate_state_writemask(astate, prop_annot,
6651 httpd_userid, &pctx->buf);
6652 if (!r) {
6653 /* Set CALDAV:calendar-timezone */
6654 proppatch_todb(prop, set, pctx, propstat, (void *) tz);
6655 }
6656 else {
6657 xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
6658 &propstat[PROPSTAT_ERROR],
6659 prop->name, prop->ns, NULL, 0);
6660 *pctx->ret = HTTP_SERVER_ERROR;
6661 }
6662 }
6663
6664 if (freeme) xmlFree(freeme);
6665 if (type) xmlFree(type);
6666 if (ver) xmlFree(ver);
6667 }
6668 else {
6669 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6670 &propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, 0);
6671
6672 *pctx->ret = HTTP_FORBIDDEN;
6673 }
6674
6675 return 0;
6676 }
6677
6678
6679 /* Callback to prescreen/fetch CALDAV/CS:calendar-availability */
propfind_availability(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6680 static int propfind_availability(const xmlChar *name, xmlNsPtr ns,
6681 struct propfind_ctx *fctx,
6682 xmlNodePtr prop,
6683 xmlNodePtr resp __attribute__((unused)),
6684 struct propstat propstat[],
6685 void *rock __attribute__((unused)))
6686 {
6687 static struct mime_type_t *out_type = caldav_mime_types;
6688 struct buf attrib = BUF_INITIALIZER;
6689 const char *data = NULL;
6690 unsigned long datalen = 0;
6691 int r = 0;
6692
6693 if (propstat) {
6694 if (!out_type->content_type) return HTTP_BAD_MEDIATYPE;
6695
6696 buf_reset(&fctx->buf);
6697 buf_printf(&fctx->buf, DAV_ANNOT_NS "<%s>%s",
6698 (const char *) ns->href, name);
6699
6700 if (fctx->mailbox && !fctx->record) {
6701 r = annotatemore_lookupmask(fctx->mailbox->name,
6702 buf_cstring(&fctx->buf),
6703 httpd_userid, &attrib);
6704 }
6705
6706 if (!attrib.len && xmlStrcmp(ns->href, BAD_CAST XML_NS_CALDAV)) {
6707 /* Check for CALDAV:calendar-availability */
6708 buf_reset(&fctx->buf);
6709 buf_printf(&fctx->buf, DAV_ANNOT_NS "<%s>%s", XML_NS_CALDAV, name);
6710
6711 if (fctx->mailbox && !fctx->record) {
6712 r = annotatemore_lookupmask(fctx->mailbox->name,
6713 buf_cstring(&fctx->buf),
6714 httpd_userid, &attrib);
6715 }
6716 }
6717
6718 if (r) r = HTTP_SERVER_ERROR;
6719 else if (!attrib.len) r = HTTP_NOT_FOUND;
6720 else {
6721 data = buf_cstring(&attrib);
6722 datalen = attrib.len;
6723 }
6724 }
6725
6726 if (!r) r = propfind_getdata(name, ns, fctx, prop, propstat,
6727 caldav_mime_types, &out_type,
6728 data, datalen);
6729 buf_free(&attrib);
6730
6731 return r;
6732 }
6733
6734
6735
6736 /* Callback to write calendar-availability property */
proppatch_availability(xmlNodePtr prop,unsigned set,struct proppatch_ctx * pctx,struct propstat propstat[],void * rock)6737 static int proppatch_availability(xmlNodePtr prop, unsigned set,
6738 struct proppatch_ctx *pctx,
6739 struct propstat propstat[],
6740 void *rock __attribute__((unused)))
6741 {
6742 if (config_allowsched && pctx->txn->req_tgt.flags == TGT_SCHED_INBOX) {
6743 const char *avail = NULL;
6744 xmlChar *type, *ver = NULL, *freeme = NULL;
6745 struct mime_type_t *mime;
6746 unsigned valid = 1;
6747
6748 type = xmlGetProp(prop, BAD_CAST "content-type");
6749 if (type) ver = xmlGetProp(prop, BAD_CAST "version");
6750
6751 /* Check/find requested MIME type */
6752 for (mime = caldav_mime_types; type && mime->content_type; mime++) {
6753 if (is_mediatype(mime->content_type, (const char *) type)) {
6754 if (ver &&
6755 (!mime->version || xmlStrcmp(ver, BAD_CAST mime->version))) {
6756 continue;
6757 }
6758 break;
6759 }
6760 }
6761
6762 if (!mime || !mime->content_type) {
6763 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6764 &propstat[PROPSTAT_FORBID],
6765 prop->name, prop->ns, NULL,
6766 CALDAV_SUPP_DATA);
6767 *pctx->ret = HTTP_FORBIDDEN;
6768 valid = 0;
6769 }
6770 else if (set) {
6771 icalcomponent *ical = NULL;
6772 struct buf buf = BUF_INITIALIZER;
6773
6774 freeme = xmlNodeGetContent(prop);
6775 avail = (const char *) freeme;
6776
6777 /* Parse and validate the iCal data */
6778 buf_init_ro_cstr(&buf, avail);
6779 ical = mime->to_object(&buf);
6780 if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) {
6781 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6782 &propstat[PROPSTAT_FORBID],
6783 prop->name, prop->ns, NULL,
6784 CALDAV_VALID_DATA);
6785 *pctx->ret = HTTP_FORBIDDEN;
6786 valid = 0;
6787 }
6788 else if (!icalcomponent_get_first_component(ical,
6789 ICAL_VAVAILABILITY_COMPONENT)) {
6790 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6791 &propstat[PROPSTAT_FORBID],
6792 prop->name, prop->ns, NULL,
6793 CALDAV_VALID_OBJECT);
6794 *pctx->ret = HTTP_FORBIDDEN;
6795 valid = 0;
6796 }
6797 else if (mime != caldav_mime_types) {
6798 avail = icalcomponent_as_ical_string(ical);
6799 }
6800
6801 if (ical) icalcomponent_free(ical);
6802 }
6803
6804 if (valid) {
6805 proppatch_todb(prop, set, pctx, propstat, (void *) avail);
6806 }
6807
6808 if (freeme) xmlFree(freeme);
6809 if (type) xmlFree(type);
6810 if (ver) xmlFree(ver);
6811 }
6812 else {
6813 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6814 &propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, 0);
6815
6816 *pctx->ret = HTTP_FORBIDDEN;
6817 }
6818
6819 return 0;
6820 }
6821
6822
6823 /* Callback to fetch CALDAV:timezone-service-set */
propfind_tzservset(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6824 static int propfind_tzservset(const xmlChar *name, xmlNsPtr ns,
6825 struct propfind_ctx *fctx,
6826 xmlNodePtr prop __attribute__((unused)),
6827 xmlNodePtr resp __attribute__((unused)),
6828 struct propstat propstat[],
6829 void *rock __attribute__((unused)))
6830 {
6831 assert(name && ns && fctx && propstat);
6832
6833 if (fctx->req_tgt->resource) return HTTP_NOT_FOUND;
6834
6835 if (namespace_calendar.allow & ALLOW_CAL_NOTZ) {
6836 xmlNodePtr node;
6837 const char *proto = NULL, *host = NULL;
6838
6839 node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
6840 name, ns, NULL, 0);
6841
6842 http_proto_host(fctx->txn->req_hdrs, &proto, &host);
6843
6844 buf_reset(&fctx->buf);
6845 buf_printf(&fctx->buf, "%s://%s%s",
6846 proto, host, namespace_tzdist.prefix);
6847
6848 xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
6849
6850 return 0;
6851 }
6852
6853 return HTTP_NOT_FOUND;
6854 }
6855
6856
6857 /* Callback to fetch CALDAV:calendar-timezone-id property */
propfind_tzid(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6858 static int propfind_tzid(const xmlChar *name, xmlNsPtr ns,
6859 struct propfind_ctx *fctx,
6860 xmlNodePtr prop __attribute__((unused)),
6861 xmlNodePtr resp __attribute__((unused)),
6862 struct propstat propstat[],
6863 void *rock __attribute__((unused)))
6864 {
6865 const char *prop_annot =
6866 DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone-id";
6867 struct buf attrib = BUF_INITIALIZER;
6868 const char *value = NULL;
6869 int r = 0;
6870
6871 if (!(namespace_calendar.allow & ALLOW_CAL_NOTZ) ||
6872 !fctx->req_tgt->collection || fctx->req_tgt->resource)
6873 return HTTP_NOT_FOUND;
6874
6875 r = annotatemore_lookupmask(fctx->mailbox->name, prop_annot,
6876 httpd_userid, &attrib);
6877
6878 if (r) r = HTTP_SERVER_ERROR;
6879 else if (attrib.len) {
6880 value = buf_cstring(&attrib);
6881 }
6882 else {
6883 /* Check for CALDAV:calendar-timezone */
6884 prop_annot = DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone";
6885
6886 if (fctx->mailbox && !fctx->record) {
6887 r = annotatemore_lookupmask(fctx->mailbox->name, prop_annot,
6888 httpd_userid, &attrib);
6889 }
6890
6891 if (r) r = HTTP_SERVER_ERROR;
6892 else if (!attrib.len) r = HTTP_NOT_FOUND;
6893 else {
6894 icalcomponent *ical, *vtz;
6895 icalproperty *tzid;
6896
6897 ical = icalparser_parse_string(buf_cstring(&attrib));
6898 vtz = icalcomponent_get_first_component(ical,
6899 ICAL_VTIMEZONE_COMPONENT);
6900 tzid = icalcomponent_get_first_property(vtz, ICAL_TZID_PROPERTY);
6901 value = icalmemory_tmp_copy(icalproperty_get_tzid(tzid));
6902 icalcomponent_free(ical);
6903 }
6904 }
6905
6906 if (!r) xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
6907 name, ns, BAD_CAST value, 0);
6908
6909 buf_free(&attrib);
6910
6911 return r;
6912 }
6913
6914
6915 /* Callback to write CALDAV:calendar-timezone-id property */
proppatch_tzid(xmlNodePtr prop,unsigned set,struct proppatch_ctx * pctx,struct propstat propstat[],void * rock)6916 static int proppatch_tzid(xmlNodePtr prop, unsigned set,
6917 struct proppatch_ctx *pctx,
6918 struct propstat propstat[],
6919 void *rock __attribute__((unused)))
6920 {
6921 if ((namespace_calendar.allow & ALLOW_CAL_NOTZ) &&
6922 pctx->txn->req_tgt.collection && !pctx->txn->req_tgt.resource) {
6923 xmlChar *freeme = NULL;
6924 const char *tzid = NULL;
6925 unsigned valid = 1;
6926 int r;
6927
6928 if (set) {
6929 freeme = xmlNodeGetContent(prop);
6930 tzid = (const char *) freeme;
6931
6932 /* Verify we have tzid record in the database */
6933 r = zoneinfo_lookup(tzid, NULL);
6934 if (r) {
6935 if (r == CYRUSDB_NOTFOUND) {
6936 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6937 &propstat[PROPSTAT_FORBID],
6938 prop->name, prop->ns, NULL,
6939 CALDAV_VALID_TIMEZONE);
6940 }
6941 else {
6942 xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
6943 &propstat[PROPSTAT_ERROR],
6944 prop->name, prop->ns, NULL, 0);
6945 }
6946 *pctx->ret = HTTP_FORBIDDEN;
6947 valid = 0;
6948 }
6949 }
6950
6951 if (valid) {
6952 /* Remove CALDAV:calendar-timezone */
6953 const char *prop_annot =
6954 DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone";
6955 annotate_state_t *astate = NULL;
6956
6957 buf_reset(&pctx->buf);
6958 r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate);
6959 if (!r) r = annotate_state_writemask(astate, prop_annot,
6960 httpd_userid, &pctx->buf);
6961
6962 if (r) {
6963 xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
6964 &propstat[PROPSTAT_ERROR],
6965 prop->name, prop->ns, NULL, 0);
6966 *pctx->ret = HTTP_SERVER_ERROR;
6967 }
6968 else {
6969 /* Set CALDAV:calendar-timezone-id */
6970 proppatch_todb(prop, set, pctx, propstat, (void *) tzid);
6971 }
6972 }
6973
6974 if (freeme) xmlFree(freeme);
6975
6976 return 0;
6977 }
6978
6979 xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
6980 &propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, 0);
6981
6982 *pctx->ret = HTTP_FORBIDDEN;
6983
6984 return 0;
6985 }
6986
6987
6988 /* Callback to fetch CALDAV:supported-rscale-set */
propfind_rscaleset(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)6989 static int propfind_rscaleset(const xmlChar *name, xmlNsPtr ns,
6990 struct propfind_ctx *fctx,
6991 xmlNodePtr prop __attribute__((unused)),
6992 xmlNodePtr resp __attribute__((unused)),
6993 struct propstat propstat[],
6994 void *rock __attribute__((unused)))
6995 {
6996 assert(name && ns && fctx && propstat);
6997
6998 if (fctx->req_tgt->resource) return HTTP_NOT_FOUND;
6999
7000 if (rscale_calendars) {
7001 xmlNodePtr top;
7002 int i, n;
7003
7004 top = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
7005 name, ns, NULL, 0);
7006
7007 for (i = 0, n = rscale_calendars->num_elements; i < n; i++) {
7008 const char **rscale = icalarray_element_at(rscale_calendars, i);
7009
7010 xmlNewChild(top, fctx->ns[NS_CALDAV],
7011 BAD_CAST "supported-rscale", BAD_CAST *rscale);
7012 }
7013
7014 return 0;
7015 }
7016
7017 return HTTP_NOT_FOUND;
7018 }
7019
7020
7021 /* Callback to fetch CS:allowed-sharing-modes */
propfind_sharingmodes(const xmlChar * name,xmlNsPtr ns,struct propfind_ctx * fctx,xmlNodePtr prop,xmlNodePtr resp,struct propstat propstat[],void * rock)7022 static int propfind_sharingmodes(const xmlChar *name, xmlNsPtr ns,
7023 struct propfind_ctx *fctx,
7024 xmlNodePtr prop __attribute__((unused)),
7025 xmlNodePtr resp __attribute__((unused)),
7026 struct propstat propstat[],
7027 void *rock __attribute__((unused)))
7028 {
7029 fctx->flags.cs_sharing = 1;
7030
7031 if (fctx->req_tgt->collection && !fctx->req_tgt->flags &&
7032 !fctx->req_tgt->resource &&
7033 mboxname_userownsmailbox(fctx->req_tgt->userid, fctx->mbentry->name)) {
7034 xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
7035 &propstat[PROPSTAT_OK],
7036 name, ns, NULL, 0);
7037
7038 xmlNewChild(node, NULL, BAD_CAST "can-be-shared", NULL);
7039 #if 0 /* XXX this is probably iCloud specific */
7040 xmlNewChild(node, NULL, BAD_CAST "can-be-published", NULL);
7041 #endif
7042 return 0;
7043 }
7044
7045 return HTTP_NOT_FOUND;
7046 }
7047
7048
7049 static icaltimezone *get_calendar_tz(const char *mboxname, const char *userid);
7050
7051 /* mboxlist_findall() callback to run calendar-query on a collection */
calquery_by_collection(const mbentry_t * mbentry,void * rock)7052 static int calquery_by_collection(const mbentry_t *mbentry, void *rock)
7053 {
7054 const char *mboxname = mbentry->name;
7055 struct propfind_ctx *fctx = (struct propfind_ctx *) rock;
7056 struct calquery_filter *calfilter =
7057 (struct calquery_filter *) fctx->filter_crit;
7058 icaltimezone *cal_tz = NULL;
7059
7060 if (!calfilter->tz && (calfilter->flags & NEED_TZ)) {
7061 /* Determine which time zone to use for floating time */
7062 calfilter->tz = cal_tz = get_calendar_tz(mboxname, httpd_userid);
7063 }
7064
7065 int r = propfind_by_collection(mbentry, rock);
7066
7067 if (cal_tz) {
7068 icaltimezone_free(cal_tz, 1 /* free_struct */);
7069 calfilter->tz = NULL;
7070 }
7071
7072 return r;
7073 }
7074
7075
report_cal_query(struct transaction_t * txn,struct meth_params * rparams,xmlNodePtr inroot,struct propfind_ctx * fctx)7076 static int report_cal_query(struct transaction_t *txn,
7077 struct meth_params *rparams __attribute__((unused)),
7078 xmlNodePtr inroot, struct propfind_ctx *fctx)
7079 {
7080 int ret = 0;
7081 xmlNodePtr node;
7082 struct calquery_filter calfilter;
7083
7084 memset(&calfilter, 0, sizeof(struct calquery_filter));
7085
7086 fctx->filter_crit = &calfilter;
7087 fctx->open_db = (db_open_proc_t) &caldav_open_mailbox;
7088 fctx->close_db = (db_close_proc_t) &caldav_close;
7089 fctx->lookup_resource = (db_lookup_proc_t) &caldav_lookup_resource;
7090 fctx->foreach_resource = (db_foreach_proc_t) &caldav_foreach;
7091 fctx->proc_by_resource = &propfind_by_resource;
7092 fctx->davdb = NULL;
7093
7094 /* Parse children element of report */
7095 for (node = inroot->children; node; node = node->next) {
7096 if (node->type == XML_ELEMENT_NODE) {
7097 if (!xmlStrcmp(node->name, BAD_CAST "filter")) {
7098 ret = parse_calfilter(node, &calfilter, &txn->error);
7099 if (ret) goto done;
7100 else fctx->filter = apply_calfilter;
7101 }
7102 else if (!xmlStrcmp(node->name, BAD_CAST "timezone")) {
7103 xmlChar *tzdata = NULL;
7104 icalcomponent *ical = NULL, *tz = NULL;
7105
7106 tzdata = xmlNodeGetContent(node);
7107 if (tzdata) {
7108 ical = icalparser_parse_string((const char *) tzdata);
7109 xmlFree(tzdata);
7110 }
7111
7112 /* Validate the iCal data */
7113 if (!ical || !icalrestriction_check(ical) ||
7114 icalcomponent_get_first_real_component(ical) ||
7115 !(tz = icalcomponent_get_first_component(ical,
7116 ICAL_VTIMEZONE_COMPONENT))) {
7117 txn->error.precond = CALDAV_VALID_DATA;
7118 ret = HTTP_FORBIDDEN;
7119 }
7120 else {
7121 icalcomponent_remove_component(ical, tz);
7122 calfilter.tz = icaltimezone_new();
7123 icaltimezone_set_component(calfilter.tz, tz);
7124 }
7125
7126 if (ical) icalcomponent_free(ical);
7127 if (ret) return ret;
7128 }
7129 else if (!xmlStrcmp(node->name, BAD_CAST "timezone-id")) {
7130 xmlChar *tzid = NULL;
7131
7132 /* XXX Need to pass this to query for floating time */
7133 syslog(LOG_WARNING, "REPORT calendar-query w/tzid");
7134 tzid = xmlNodeGetContent(node);
7135
7136 if (tzid) {
7137 icaltimezone *tz =
7138 icaltimezone_get_builtin_timezone_from_tzid((const char *) tzid);
7139 if (tz) calfilter.tz = icaltimezone_copy(tz);
7140 xmlFree(tzid);
7141
7142 if (!calfilter.tz) {
7143 txn->error.precond = CALDAV_VALID_TIMEZONE;
7144 return HTTP_FORBIDDEN;
7145 }
7146 }
7147 }
7148 }
7149 }
7150
7151 /* Setup for chunked response */
7152 txn->flags.te |= TE_CHUNKED;
7153
7154 /* Begin XML response */
7155 xml_response(HTTP_MULTI_STATUS, txn, fctx->root->doc);
7156
7157 if (fctx->depth++ > 0) {
7158 /* Calendar collection(s) */
7159 if (txn->req_tgt.collection) {
7160 /* Add response for target calendar collection */
7161 calquery_by_collection(txn->req_tgt.mbentry, fctx);
7162 }
7163 else {
7164 /* Add responses for all contained calendar collections */
7165 mboxlist_mboxtree(txn->req_tgt.mbentry->name,
7166 calquery_by_collection, fctx,
7167 MBOXTREE_SKIP_ROOT);
7168
7169 /* Add responses for all shared calendar collections */
7170 mboxlist_usersubs(txn->req_tgt.userid,
7171 calquery_by_collection, fctx,
7172 MBOXTREE_SKIP_PERSONAL);
7173 }
7174 }
7175
7176 /* End XML response */
7177 xml_partial_response(txn, fctx->root->doc, NULL /* end */, 0, &fctx->xmlbuf);
7178 xmlBufferFree(fctx->xmlbuf);
7179
7180 /* End of output */
7181 write_body(0, txn, NULL, 0);
7182
7183 done:
7184 /* Free filter structure */
7185 if (calfilter.tz) icaltimezone_free(calfilter.tz, 1);
7186 free_compfilter(calfilter.comp);
7187
7188 if (fctx->davdb) {
7189 caldav_close(fctx->davdb);
7190 fctx->davdb = NULL;
7191 }
7192
7193 return ret;
7194 }
7195
7196
is_busytime(icalcomponent * comp)7197 static int is_busytime(icalcomponent *comp)
7198 {
7199 /* Check TRANSP and STATUS per RFC 4791, section 7.10 */
7200 const icalproperty *prop;
7201
7202 /* Skip transparent events */
7203 prop = icalcomponent_get_first_property(comp, ICAL_TRANSP_PROPERTY);
7204 if (prop && icalproperty_get_transp(prop) == ICAL_TRANSP_TRANSPARENT)
7205 return 0;
7206
7207 /* Skip cancelled events */
7208 if (icalcomponent_get_status(comp) == ICAL_STATUS_CANCELLED) return 0;
7209
7210 return 1;
7211 }
7212
7213
7214 /* Append a new busytime period to the busytime array */
add_freebusy(struct icaltimetype * recurid,struct icaltimetype * start,struct icaltimetype * end,icalparameter_fbtype fbtype,struct freebusy_filter * fbfilter)7215 static void add_freebusy(struct icaltimetype *recurid,
7216 struct icaltimetype *start,
7217 struct icaltimetype *end,
7218 icalparameter_fbtype fbtype,
7219 struct freebusy_filter *fbfilter)
7220 {
7221 struct freebusy_array *freebusy = &fbfilter->freebusy;
7222 struct freebusy *newfb;
7223
7224 /* Grow the array, if necessary */
7225 if (freebusy->len == freebusy->alloc) {
7226 freebusy->alloc += 100; /* XXX arbitrary */
7227 freebusy->fb = xrealloc(freebusy->fb,
7228 freebusy->alloc * sizeof(struct freebusy));
7229 }
7230
7231 /* Add new freebusy */
7232 newfb = &freebusy->fb[freebusy->len++];
7233 memset(newfb, 0, sizeof(struct freebusy));
7234
7235 if (recurid) newfb->recurid = *recurid;
7236
7237 if (icaltime_is_date(*start)) {
7238 newfb->per.duration = icaltime_subtract(*end, *start);
7239 newfb->per.end = icaltime_null_time();
7240 start->is_date = 0; /* MUST be DATE-TIME */
7241 newfb->per.start = icaltime_convert_to_zone(*start, utc_zone);
7242 }
7243 else {
7244 newfb->per.duration = icaldurationtype_null_duration();
7245 if (icaltime_compare(fbfilter->end, *end) < 0) {
7246 newfb->per.end = fbfilter->end;
7247 }
7248 else newfb->per.end = *end;
7249
7250 if (icaltime_compare(fbfilter->start, *start) > 0) {
7251 newfb->per.start = fbfilter->start;
7252 }
7253 else newfb->per.start = *start;
7254 }
7255 newfb->type = fbtype;
7256 }
7257
7258
7259 /* Append a new busytime period for recurring comp to the busytime array */
add_freebusy_comp(icalcomponent * comp,icaltimetype start,icaltimetype end,void * rock)7260 static int add_freebusy_comp(icalcomponent *comp,
7261 icaltimetype start, icaltimetype end,
7262 void *rock)
7263 {
7264 struct freebusy_filter *fbfilter = (struct freebusy_filter *) rock;
7265 struct icaltimetype recurid;
7266 icalparameter_fbtype fbtype;
7267
7268 if (!is_busytime(comp)) return 1;
7269
7270 /* Set start and end times */
7271 start = icaltime_convert_to_zone(start, utc_zone);
7272 end = icaltime_convert_to_zone(end, utc_zone);
7273
7274 /* Set recurid */
7275 recurid = icalcomponent_get_recurrenceid_with_zone(comp);
7276 if (icaltime_is_null_time(recurid)) recurid = start;
7277 else {
7278 recurid = icaltime_convert_to_zone(recurid, utc_zone);
7279 recurid.is_date = 0; /* make DATE-TIME for comparison */
7280 }
7281
7282 /* Set FBTYPE */
7283 switch (icalcomponent_isa(comp)) {
7284 case ICAL_VEVENT_COMPONENT:
7285 fbtype = icalcomponent_get_status(comp) == ICAL_STATUS_TENTATIVE ?
7286 ICAL_FBTYPE_BUSYTENTATIVE : ICAL_FBTYPE_BUSY;
7287 break;
7288
7289 case ICAL_VFREEBUSY_COMPONENT:
7290 /* XXX Need to do something better here */
7291 fbtype = ICAL_FBTYPE_BUSY;
7292 break;
7293
7294 case ICAL_VAVAILABILITY_COMPONENT: {
7295 enum icalproperty_busytype busytype = ICAL_BUSYTYPE_BUSYUNAVAILABLE;
7296 icalproperty *prop =
7297 icalcomponent_get_first_property(comp, ICAL_BUSYTYPE_PROPERTY);
7298
7299 if (prop) busytype = icalproperty_get_busytype(prop);
7300
7301 switch (busytype) {
7302 case ICAL_BUSYTYPE_BUSYUNAVAILABLE:
7303 fbtype = ICAL_FBTYPE_BUSYUNAVAILABLE;
7304 break;
7305 case ICAL_BUSYTYPE_BUSYTENTATIVE:
7306 fbtype = ICAL_FBTYPE_BUSYTENTATIVE;
7307 break;
7308 default:
7309 fbtype = ICAL_FBTYPE_BUSY;
7310 break;
7311 }
7312 break;
7313 }
7314
7315 case ICAL_XAVAILABLE_COMPONENT:
7316 fbtype = ICAL_FBTYPE_FREE;
7317 break;
7318
7319 default:
7320 fbtype = ICAL_FBTYPE_NONE;
7321 break;
7322 }
7323
7324 add_freebusy(&recurid, &start, &end, fbtype, fbfilter);
7325
7326 return 1;
7327 }
7328
7329
expand_occurrences(icalcomponent * ical,struct freebusy_filter * fbfilter)7330 static void expand_occurrences(icalcomponent *ical,
7331 struct freebusy_filter *fbfilter)
7332 {
7333 /* Create a span for the given time-range */
7334 struct icalperiodtype rangespan =
7335 { fbfilter->start, fbfilter->end, icaldurationtype_null_duration() };
7336
7337 icalcomponent_myforeach(ical, rangespan, NULL, add_freebusy_comp, fbfilter);
7338 }
7339
7340
7341 /* Append a new vavailability period to the vavail array */
7342 static void
add_vavailability(struct vavailability_array * vavail,icalcomponent * ical)7343 add_vavailability(struct vavailability_array *vavail, icalcomponent *ical)
7344 {
7345 struct vavailability *newav;
7346 icalcomponent *vav;
7347 icalproperty *prop;
7348
7349 /* Grow the array, if necessary */
7350 if (vavail->len == vavail->alloc) {
7351 vavail->alloc += 10; /* XXX arbitrary */
7352 vavail->vav = xrealloc(vavail->vav,
7353 vavail->alloc * sizeof(struct vavailability));
7354 }
7355
7356 /* Add new vavailability */
7357 newav = &vavail->vav[vavail->len++];
7358 newav->ical = ical;
7359
7360 vav = icalcomponent_get_first_real_component(ical);
7361
7362 /* Set period */
7363 newav->per.start = icalcomponent_get_dtstart(vav);
7364 if (icaltime_is_null_time(newav->per.start)) {
7365 newav->per.start =
7366 icaltime_from_timet_with_zone(caldav_epoch, 0, utc_zone);
7367 }
7368 else {
7369 newav->per.start = icaltime_convert_to_zone(newav->per.start, utc_zone);
7370 }
7371 newav->per.end = icalcomponent_get_dtend(vav);
7372 if (icaltime_is_null_time(newav->per.end)) {
7373 newav->per.end =
7374 icaltime_from_timet_with_zone(caldav_eternity, 0, utc_zone);
7375 }
7376 else {
7377 newav->per.end = icaltime_convert_to_zone(newav->per.end, utc_zone);
7378 }
7379 newav->per.duration = icaldurationtype_null_duration();
7380
7381 /* Set PRIORITY - 0 (or none) has lower priority than 9 */
7382 prop = icalcomponent_get_first_property(vav, ICAL_PRIORITY_PROPERTY);
7383 if (prop) newav->priority = icalproperty_get_priority(prop);
7384 if (!prop || !newav->priority) newav->priority = 10;
7385 }
7386
7387
7388 /* caldav_foreach() callback to find busytime of a resource */
busytime_by_resource(void * rock,void * data)7389 static int busytime_by_resource(void *rock, void *data)
7390 {
7391 struct propfind_ctx *fctx = (struct propfind_ctx *) rock;
7392 struct caldav_data *cdata = (struct caldav_data *) data;
7393 struct freebusy_filter *fbfilter =
7394 (struct freebusy_filter *) fctx->filter_crit;
7395
7396 keepalive_response(fctx->txn);
7397
7398 if (!cdata->dav.imap_uid) return 0;
7399
7400 /* Perform component filtering */
7401 if (!(cdata->comp_type &
7402 (CAL_COMP_VEVENT | CAL_COMP_VFREEBUSY | CAL_COMP_VAVAILABILITY))) {
7403 return 0;
7404 }
7405
7406 /* Perform time-range filtering */
7407 struct icaltimetype dtstart = icaltime_from_string(cdata->dtstart);
7408 struct icaltimetype dtend = icaltime_from_string(cdata->dtend);
7409
7410 dtstart = icaltime_convert_to_utc(dtstart, fbfilter->tz);
7411 dtend = icaltime_convert_to_utc(dtend, fbfilter->tz);
7412
7413 if (icaltime_compare(dtend, fbfilter->start) <= 0) {
7414 /* Component ends earlier than range */
7415 return 0;
7416 }
7417 if (icaltime_compare(dtstart, fbfilter->end) >= 0) {
7418 /* Component starts later than range */
7419 return 0;
7420 }
7421
7422 if (cdata->comp_flags.recurring ||
7423 cdata->comp_type == CAL_COMP_VAVAILABILITY) {
7424 /* Need to mmap() and parse iCalendar object */
7425 icalcomponent *ical = NULL;
7426
7427 /* Fetch index record for the resource */
7428 ical = caldav_record_to_ical(fctx->mailbox, cdata, NULL, NULL);
7429 if (!ical) return 0;
7430
7431 if (cdata->comp_flags.recurring) {
7432 /* Component is recurring - process each recurrence */
7433 expand_occurrences(ical, fbfilter);
7434
7435 icalcomponent_free(ical);
7436 }
7437 else {
7438 /* VAVAILABILITY - add to our array for later use */
7439 add_vavailability(&fbfilter->vavail, ical);
7440 }
7441 }
7442 else {
7443 /* Component is non-recurring */
7444 icalparameter_fbtype fbtype;
7445
7446 if (cdata->comp_flags.transp) {
7447 /* Don't include transparent events in freebusy */
7448 return 0;
7449 }
7450 if (cdata->comp_flags.status == CAL_STATUS_CANCELED) {
7451 /* Don't include canceled events in freebusy */
7452 return 0;
7453 }
7454
7455 switch (cdata->comp_flags.status) {
7456 case CAL_STATUS_UNAVAILABLE:
7457 fbtype = ICAL_FBTYPE_BUSYUNAVAILABLE; break;
7458
7459 case CAL_STATUS_TENTATIVE:
7460 fbtype = ICAL_FBTYPE_BUSYTENTATIVE; break;
7461
7462 default:
7463 fbtype = ICAL_FBTYPE_BUSY; break;
7464 }
7465
7466 add_freebusy(&dtstart, &dtstart, &dtend, fbtype, fbfilter);
7467 }
7468
7469 return 0;
7470 }
7471
7472
get_calendar_tz(const char * mboxname,const char * userid)7473 static icaltimezone *get_calendar_tz(const char *mboxname, const char *userid)
7474 {
7475 struct buf attrib = BUF_INITIALIZER;
7476 icaltimezone *tz = NULL;
7477
7478 /* Check for CALDAV:calendar-timezone-id */
7479 const char *prop_annot =
7480 DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone-id";
7481
7482 int r = annotatemore_lookupmask(mboxname, prop_annot, userid, &attrib);
7483 if (!r && buf_len(&attrib)) {
7484 tz = icaltimezone_get_builtin_timezone(buf_cstring(&attrib));
7485 buf_free(&attrib);
7486 if (tz) return icaltimezone_copy(tz);
7487 }
7488
7489 /* Check for CALDAV:calendar-timezone */
7490 prop_annot = DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone";
7491
7492 r = annotatemore_lookupmask(mboxname, prop_annot, httpd_userid, &attrib);
7493 if (!r && buf_len(&attrib)) {
7494 icalcomponent *ical, *vtz;
7495
7496 ical = icalparser_parse_string(buf_cstring(&attrib));
7497 vtz = icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT);
7498 icalcomponent_remove_component(ical, vtz);
7499 icalcomponent_free(ical);
7500 buf_free(&attrib);
7501
7502 tz = icaltimezone_new();
7503 icaltimezone_set_component(tz, vtz);
7504 return tz;
7505 }
7506
7507 /* Default to UTC */
7508 return icaltimezone_copy(icaltimezone_get_utc_timezone());
7509 }
7510
7511 /* mboxlist_findall() callback to find busytime of a collection */
busytime_by_collection(const mbentry_t * mbentry,void * rock)7512 static int busytime_by_collection(const mbentry_t *mbentry, void *rock)
7513 {
7514 const char *mboxname = mbentry->name;
7515 struct propfind_ctx *fctx = (struct propfind_ctx *) rock;
7516 struct freebusy_filter *fbfilter =
7517 (struct freebusy_filter *) fctx->filter_crit;
7518
7519 if (fbfilter->flags & CHECK_CAL_TRANSP) {
7520 /* Check if the collection is marked as transparent */
7521 struct buf attrib = BUF_INITIALIZER;
7522 const char *prop_annot =
7523 DAV_ANNOT_NS "<" XML_NS_CALDAV ">schedule-calendar-transp";
7524
7525 if (!annotatemore_lookupmask(mboxname, prop_annot,
7526 httpd_userid, &attrib)) {
7527 if (!strcmp(buf_cstring(&attrib), "transparent")) {
7528 buf_free(&attrib);
7529 return 0;
7530 }
7531 buf_free(&attrib);
7532 }
7533 }
7534
7535 /* Determine which time zone to use for floating time */
7536 fbfilter->tz = get_calendar_tz(mboxname, httpd_userid);
7537
7538 int r = propfind_by_collection(mbentry, rock);
7539
7540 if (fbfilter->tz) icaltimezone_free(fbfilter->tz, 1 /* free_struct */);
7541 fbfilter->tz = NULL;
7542
7543 return r;
7544 }
7545
7546
7547 /* Compare start/end times of freebusy periods -- used for sorting */
compare_freebusy(const void * fb1,const void * fb2)7548 static int compare_freebusy(const void *fb1, const void *fb2)
7549 {
7550 struct freebusy *a = (struct freebusy *) fb1;
7551 struct freebusy *b = (struct freebusy *) fb2;
7552
7553 int r = icaltime_compare(a->per.start, b->per.start);
7554
7555 if (r == 0) r = icaltime_compare(a->per.end, b->per.end);
7556
7557 return r;
7558 }
7559
7560
7561 /* Compare type and start/end times of freebusy periods -- used for sorting */
compare_freebusy_with_type(const void * fb1,const void * fb2)7562 static int compare_freebusy_with_type(const void *fb1, const void *fb2)
7563 {
7564 struct freebusy *a = (struct freebusy *) fb1;
7565 struct freebusy *b = (struct freebusy *) fb2;
7566
7567 int r = a->type - b->type;
7568
7569 if (r == 0) r = compare_freebusy(fb1, fb2);
7570
7571 return r;
7572 }
7573
7574
7575 /* Compare priority, start/end times, and type of vavail periods for sorting */
compare_vavail(const void * v1,const void * v2)7576 static int compare_vavail(const void *v1, const void *v2)
7577 {
7578 struct vavailability *a = (struct vavailability *) v1;
7579 struct vavailability *b = (struct vavailability *) v2;
7580
7581 int r = a->priority - b->priority;
7582
7583 if (r == 0) {
7584 r = icaltime_compare(a->per.start, b->per.start);
7585
7586 if (r == 0) r = icaltime_compare(a->per.end, b->per.end);
7587 }
7588
7589 return r;
7590 }
7591
7592
combine_vavailability(struct freebusy_filter * fbfilter)7593 static void combine_vavailability(struct freebusy_filter *fbfilter)
7594 {
7595 struct vavailability_array *vavail = &fbfilter->vavail;
7596 struct freebusy_filter availfilter;
7597 struct query_range {
7598 struct icalperiodtype per;
7599 struct query_range *next;
7600 } *ranges, *range, *prev, *next;
7601 unsigned i, j;
7602
7603 memset(&availfilter, 0, sizeof(struct freebusy_filter));
7604
7605 /* Sort VAVAILABILITY periods by priority and start time */
7606 qsort(vavail->vav, vavail->len,
7607 sizeof(struct vavailability), compare_vavail);
7608
7609 /* Maintain a linked list of remaining time ranges
7610 * to be filled in by lower priority VAV components
7611 * starting with the time range in the freebusy query.
7612 * Ranges are removed, clipped, or split as they get filled.
7613 * Quit when no ranges or VAVAILABILITY components remain.
7614 */
7615 ranges = xmalloc(sizeof(struct query_range));
7616 ranges->per.start = fbfilter->start;
7617 ranges->per.end = fbfilter->end;
7618 ranges->next = NULL;
7619
7620 for (i = 0; i < vavail->len; i++) {
7621 struct vavailability *vav = &vavail->vav[i];
7622 icalcomponent *comp;
7623
7624 comp = icalcomponent_get_first_component(vav->ical,
7625 ICAL_VAVAILABILITY_COMPONENT);
7626
7627 for (range = ranges, prev = NULL; range; prev = range, range = next) {
7628 struct icalperiodtype period;
7629
7630 next = range->next;
7631
7632 if (icaltime_compare(vav->per.end, range->per.start) <= 0 ||
7633 icaltime_compare(vav->per.start, range->per.end) >= 0) {
7634 /* Skip VAVAILABILITY outside our range */
7635 continue;
7636 }
7637
7638 /* Set filter range (maximum start time and minimum end time)
7639 and adjust current range as necessary */
7640 if (icaltime_compare(vav->per.start, range->per.start) <= 0) {
7641 /* VAV starts before range - filter using range start */
7642 availfilter.start = range->per.start;
7643
7644 if (icaltime_compare(vav->per.end, range->per.end) >= 0) {
7645 /* VAV ends after range - filter using range end */
7646 availfilter.end = range->per.end;
7647
7648 /* Filling entire range - remove it */
7649 if (prev) prev->next = next;
7650 else ranges = NULL;
7651
7652 free(range);
7653 range = NULL;
7654 }
7655 else {
7656 /* VAV ends before range - filter using VAV end */
7657 availfilter.end = vav->per.end;
7658
7659 /* Filling front part - adjust start to back remainder */
7660 range->per.start = vav->per.end;
7661 }
7662 }
7663 else {
7664 /* VAV starts after range - filter using VAV start */
7665 availfilter.start = vav->per.start;
7666
7667 if (icaltime_compare(vav->per.end, range->per.end) >= 0) {
7668 /* VAV ends after range - filter using range end */
7669 availfilter.end = range->per.end;
7670 }
7671 else {
7672 /* VAV ends before range - filter using VAV end */
7673 availfilter.end = vav->per.end;
7674
7675 /* Splitting range - insert new range for back remainder */
7676 struct query_range *newr =
7677 xmalloc(sizeof(struct query_range));
7678 newr->per.start = vav->per.end;
7679 newr->per.end = range->per.end;
7680 newr->next = next;
7681 range->next = newr;
7682 }
7683
7684 /* Adjust end to front remainder */
7685 range->per.end = vav->per.start;
7686 }
7687
7688 /* Expand available time occurrences */
7689 expand_occurrences(comp, &availfilter);
7690
7691 /* Calculate unavailable periods and add to busytime */
7692 period.start = availfilter.start;
7693 for (j = 0; j < availfilter.freebusy.len; j++) {
7694 struct freebusy *fb = &availfilter.freebusy.fb[j];
7695
7696 /* Ignore overridden instances */
7697 if (fb->type == ICAL_FBTYPE_NONE) continue;
7698
7699 period.end = fb->per.start;
7700 if (icaltime_compare(period.end, period.start) > 0) {
7701 add_freebusy_comp(comp, period.start, period.end, fbfilter);
7702 }
7703 period.start = fb->per.end;
7704 }
7705 period.end = availfilter.end;
7706 if (icaltime_compare(period.end, period.start) > 0) {
7707 add_freebusy_comp(comp, period.start, period.end, fbfilter);
7708 }
7709 }
7710
7711 /* Done with this ical component */
7712 icalcomponent_free(vav->ical);
7713 }
7714
7715 /* Cleanup the vavailability array */
7716 free(vavail->vav);
7717
7718 /* Cleanup the availability array */
7719 if (availfilter.freebusy.fb) free(availfilter.freebusy.fb);
7720
7721 /* Remove any unfilled ranges */
7722 for (; ranges; ranges = next) {
7723 next = ranges->next;
7724 free(ranges);
7725 }
7726 }
7727
7728
7729 /* Create an iCalendar object containing busytime of all specified resources */
busytime_query_local(struct transaction_t * txn,struct propfind_ctx * fctx,char mailboxname[],icalproperty_method method,const char * uid,const char * organizer,const char * attendee)7730 icalcomponent *busytime_query_local(struct transaction_t *txn,
7731 struct propfind_ctx *fctx,
7732 char mailboxname[],
7733 icalproperty_method method,
7734 const char *uid,
7735 const char *organizer,
7736 const char *attendee)
7737 {
7738 struct freebusy_filter *fbfilter =
7739 (struct freebusy_filter *) fctx->filter_crit;
7740 struct freebusy_array *freebusy = &fbfilter->freebusy;
7741 struct vavailability_array *vavail = &fbfilter->vavail;
7742 icalcomponent *ical = NULL;
7743 icalcomponent *fbcomp;
7744 icalproperty *prop;
7745 unsigned n;
7746
7747 syslog(LOG_DEBUG, "busytime_query_local(mbox: '%s', org: '%s', att: '%s')",
7748 mailboxname, organizer, attendee);
7749
7750 fctx->open_db = (db_open_proc_t) &caldav_open_mailbox;
7751 fctx->close_db = (db_close_proc_t) &caldav_close;
7752 fctx->lookup_resource = (db_lookup_proc_t) &caldav_lookup_resource;
7753 fctx->foreach_resource = (db_foreach_proc_t) &caldav_foreach;
7754 fctx->proc_by_resource = &busytime_by_resource;
7755
7756 /* Gather up all of the busytime and VAVAILABILITY periods */
7757 if (fctx->depth > 0) {
7758 /* Calendar collection(s) */
7759
7760 if (txn->req_tgt.collection) {
7761 /* Get busytime for target calendar collection */
7762 busytime_by_collection(txn->req_tgt.mbentry, fctx);
7763 }
7764 else {
7765 /* Get busytime for all contained calendar collections */
7766 mboxlist_mboxtree(mailboxname, busytime_by_collection,
7767 fctx, MBOXTREE_SKIP_ROOT);
7768
7769 /* XXX Get busytime for all shared calendar collections? */
7770 }
7771
7772 if (fctx->davdb) caldav_close(fctx->davdb);
7773 }
7774
7775 if (*fctx->ret) return NULL;
7776
7777 if (fbfilter->flags & CHECK_USER_AVAIL) {
7778 /* Check for CALDAV:calendar-availability on user's Inbox */
7779 struct buf attrib = BUF_INITIALIZER;
7780 const char *prop_annot =
7781 DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-availability";
7782 char *userid = mboxname_to_userid(mailboxname);
7783 char *mboxname = caldav_mboxname(userid, SCHED_INBOX);
7784 if (!annotatemore_lookupmask(mboxname, prop_annot,
7785 httpd_userid, &attrib) && attrib.len) {
7786 add_vavailability(vavail,
7787 icalparser_parse_string(buf_cstring(&attrib)));
7788 }
7789 else {
7790 prop_annot = DAV_ANNOT_NS "<" XML_NS_CS ">calendar-availability";
7791 if (!annotatemore_lookupmask(mboxname, prop_annot,
7792 httpd_userid, &attrib) && attrib.len) {
7793 add_vavailability(vavail,
7794 icalparser_parse_string(buf_cstring(&attrib)));
7795 }
7796 }
7797 free(mboxname);
7798 free(userid);
7799 }
7800
7801 /* Combine VAVAILABILITY components into busytime */
7802 if (vavail->len) combine_vavailability(fbfilter);
7803
7804 /* Sort busytime periods by type and start/end times for coalescing */
7805 qsort(freebusy->fb, freebusy->len,
7806 sizeof(struct freebusy), compare_freebusy_with_type);
7807
7808 /* Coalesce busytime periods of same type into one */
7809 for (n = 0; n + 1 < freebusy->len; n++) {
7810 struct freebusy *fb, *next_fb;
7811 icaltimetype end, next_end;
7812 int isdur, next_isdur;
7813
7814 fb = &freebusy->fb[n];
7815 next_fb = &freebusy->fb[n+1];
7816
7817 /* Ignore overridden instances */
7818 if (fb->type == ICAL_FBTYPE_NONE) continue;
7819
7820 isdur = !icaldurationtype_is_null_duration(fb->per.duration);
7821 end = !isdur ? fb->per.end :
7822 icaltime_add(fb->per.start, fb->per.duration);
7823
7824 /* Skip periods of different type or that don't overlap */
7825 if ((fb->type != next_fb->type) ||
7826 icaltime_compare(end, next_fb->per.start) < 0) continue;
7827
7828 /* Coalesce into next busytime */
7829 next_isdur = !icaldurationtype_is_null_duration(next_fb->per.duration);
7830 next_end = !next_isdur ? next_fb->per.end :
7831 icaltime_add(next_fb->per.start, next_fb->per.duration);
7832
7833 if (icaltime_compare(end, next_end) >= 0) {
7834 /* Current period subsumes next */
7835 next_fb->per.end = fb->per.end;
7836 next_fb->per.duration = fb->per.duration;
7837 }
7838 else if (isdur && next_isdur) {
7839 /* Both periods are durations */
7840 struct icaldurationtype overlap =
7841 icaltime_subtract(end, next_fb->per.start);
7842
7843 next_fb->per.duration.days += fb->per.duration.days - overlap.days;
7844 }
7845 else {
7846 /* Need to use explicit period */
7847 next_fb->per.end = next_end;
7848 next_fb->per.duration = icaldurationtype_null_duration();
7849 }
7850
7851 next_fb->per.start = fb->per.start;
7852
7853 /* "Remove" the instance
7854 by setting fbtype to NONE (we ignore these later) */
7855 fb->type = ICAL_FBTYPE_NONE;
7856 }
7857
7858 /* Sort busytime periods by start/end times for addition to VFREEBUSY */
7859 qsort(freebusy->fb, freebusy->len,
7860 sizeof(struct freebusy), compare_freebusy);
7861
7862 /* Construct iCalendar object with VFREEBUSY component */
7863 ical = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT,
7864 icalproperty_new_version("2.0"),
7865 icalproperty_new_prodid(ical_prodid),
7866 0);
7867
7868 if (method) icalcomponent_set_method(ical, method);
7869
7870 fbcomp = icalcomponent_vanew(ICAL_VFREEBUSY_COMPONENT,
7871 icalproperty_new_dtstamp(
7872 icaltime_from_timet_with_zone(
7873 time(0), 0, utc_zone)),
7874 icalproperty_new_dtstart(fbfilter->start),
7875 icalproperty_new_dtend(fbfilter->end),
7876 0);
7877
7878 icalcomponent_add_component(ical, fbcomp);
7879
7880 if (uid) icalcomponent_set_uid(fbcomp, uid);
7881 if (organizer) {
7882 prop = icalproperty_new_organizer(organizer);
7883 icalcomponent_add_property(fbcomp, prop);
7884 }
7885 if (attendee) {
7886 prop = icalproperty_new_attendee(attendee);
7887 icalcomponent_add_property(fbcomp, prop);
7888 }
7889
7890 /* Add busytime periods to VFREEBUSY component */
7891 for (n = 0; n < freebusy->len; n++) {
7892 struct freebusy *fb = &freebusy->fb[n];
7893 icalproperty *busy;
7894
7895 /* Ignore overridden instances */
7896 if (fb->type == ICAL_FBTYPE_NONE) continue;
7897
7898 /* Create new FREEBUSY property with FBTYPE and add to component */
7899 busy = icalproperty_new_freebusy(fb->per);
7900 if (fb->type != ICAL_FBTYPE_BUSY)
7901 icalproperty_add_parameter(busy, icalparameter_new_fbtype(fb->type));
7902 icalcomponent_add_property(fbcomp, busy);
7903 }
7904
7905 return ical;
7906 }
7907
7908
report_fb_query(struct transaction_t * txn,struct meth_params * rparams,xmlNodePtr inroot,struct propfind_ctx * fctx)7909 static int report_fb_query(struct transaction_t *txn,
7910 struct meth_params *rparams __attribute__((unused)),
7911 xmlNodePtr inroot, struct propfind_ctx *fctx)
7912 {
7913 int ret = 0;
7914 const char **hdr;
7915 struct mime_type_t *mime;
7916 struct freebusy_filter fbfilter;
7917 xmlNodePtr node;
7918 icalcomponent *cal;
7919
7920 /* Can not be run against a resource */
7921 if (txn->req_tgt.resource) return HTTP_FORBIDDEN;
7922
7923 /* Check requested MIME type:
7924 1st entry in caldav_mime_types array MUST be default MIME type */
7925 if ((hdr = spool_getheader(txn->req_hdrs, "Accept")))
7926 mime = get_accept_type(hdr, caldav_mime_types);
7927 else mime = caldav_mime_types;
7928 if (!mime) return HTTP_NOT_ACCEPTABLE;
7929
7930 memset(&fbfilter, 0, sizeof(struct freebusy_filter));
7931 fbfilter.start = icaltime_from_timet_with_zone(caldav_epoch, 0, utc_zone);
7932 fbfilter.end = icaltime_from_timet_with_zone(caldav_eternity, 0, utc_zone);
7933 fctx->filter_crit = &fbfilter;
7934
7935 /* Parse children element of report */
7936 for (node = inroot->children; node; node = node->next) {
7937 if (node->type == XML_ELEMENT_NODE) {
7938 if (!xmlStrcmp(node->name, BAD_CAST "time-range")) {
7939 xmlChar *start, *end;
7940
7941 start = xmlGetProp(node, BAD_CAST "start");
7942 if (start) {
7943 fbfilter.start = icaltime_from_string((char *) start);
7944 xmlFree(start);
7945 }
7946
7947 end = xmlGetProp(node, BAD_CAST "end");
7948 if (end) {
7949 fbfilter.end = icaltime_from_string((char *) end);
7950 xmlFree(end);
7951 }
7952
7953 if (!is_valid_timerange(fbfilter.start, fbfilter.end)) {
7954 return HTTP_BAD_REQUEST;
7955 }
7956 }
7957 }
7958 }
7959
7960 fctx->depth++;
7961 cal = busytime_query_local(txn, fctx, txn->req_tgt.mbentry->name,
7962 0, NULL, NULL, NULL);
7963
7964 if (fbfilter.freebusy.fb) free(fbfilter.freebusy.fb);
7965
7966 if (cal) {
7967 /* Output the iCalendar object as text/calendar */
7968 struct buf *cal_str = mime->from_object(cal);
7969 icalcomponent_free(cal);
7970
7971 txn->resp_body.type = mime->content_type;
7972
7973 /* iCalendar data in response should not be transformed */
7974 txn->flags.cc |= CC_NOTRANSFORM;
7975
7976 write_body(HTTP_OK, txn, buf_base(cal_str), buf_len(cal_str));
7977 buf_destroy(cal_str);
7978 }
7979 else ret = HTTP_NOT_FOUND;
7980
7981 return ret;
7982 }
7983
7984
7985 /* Replace TZID aliases with the actual TZIDs */
replace_tzid_aliases(icalcomponent * ical,struct hash_table * tzid_table)7986 static void replace_tzid_aliases(icalcomponent *ical,
7987 struct hash_table *tzid_table)
7988 {
7989 icalproperty *prop;
7990 for (prop = icalcomponent_get_first_property(ical, ICAL_ANY_PROPERTY);
7991 prop;
7992 prop = icalcomponent_get_next_property(ical, ICAL_ANY_PROPERTY)) {
7993 icalparameter *param =
7994 icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER);
7995 if (!param) continue;
7996
7997 const char *tzid =
7998 hash_lookup(icalparameter_get_tzid(param), tzid_table);
7999 if (tzid) icalparameter_set_tzid(param, tzid);
8000 }
8001
8002 icalcomponent *comp;
8003 for (comp = icalcomponent_get_first_component(ical, ICAL_ANY_COMPONENT);
8004 comp;
8005 comp = icalcomponent_get_next_component(ical, ICAL_ANY_COMPONENT)) {
8006 replace_tzid_aliases(comp, tzid_table);
8007 }
8008 }
8009
8010
8011 /* Strip all VTIMEZONE components for known TZIDs */
strip_vtimezones(icalcomponent * ical)8012 static void strip_vtimezones(icalcomponent *ical)
8013 {
8014 struct hash_table tzid_table;
8015 icalcomponent *vtz, *next;
8016
8017 /* Create hash table for TZID aliases */
8018 construct_hash_table(&tzid_table, 10, 1);
8019
8020 for (vtz = icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT);
8021 vtz; vtz = next) {
8022
8023 next = icalcomponent_get_next_component(ical, ICAL_VTIMEZONE_COMPONENT);
8024
8025 icalproperty *prop =
8026 icalcomponent_get_first_property(vtz, ICAL_TZID_PROPERTY);
8027 const char *tzid = icalproperty_get_tzid(prop);
8028 struct zoneinfo zi;
8029
8030 if (tzid && !zoneinfo_lookup(tzid, &zi)) {
8031 if (zi.type == ZI_LINK) {
8032 /* Add this alias to our table */
8033 hash_insert(tzid, xstrdup(zi.data->s), &tzid_table);
8034 }
8035 freestrlist(zi.data);
8036
8037 icalcomponent_remove_component(ical, vtz);
8038 icalcomponent_free(vtz);
8039 }
8040 }
8041
8042 if (hash_numrecords(&tzid_table)) {
8043 /* Replace all TZID aliases with actual TZIDs.
8044 Note: This NEEDS to be done, otherwise looking up the
8045 builtin timezone will fail on a TZID mismatch. */
8046 replace_tzid_aliases(ical, &tzid_table);
8047 }
8048 free_hash_table(&tzid_table, free);
8049 }
8050
8051
8052 /* Store the iCal data in the specified calendar/resource */
caldav_store_resource(struct transaction_t * txn,icalcomponent * ical,struct mailbox * mailbox,const char * resource,modseq_t createdmodseq,struct caldav_db * caldavdb,unsigned flags,const char * userid,const strarray_t * schedule_addresses)8053 int caldav_store_resource(struct transaction_t *txn, icalcomponent *ical,
8054 struct mailbox *mailbox, const char *resource,
8055 modseq_t createdmodseq,
8056 struct caldav_db *caldavdb, unsigned flags,
8057 const char *userid, const strarray_t *schedule_addresses)
8058 {
8059 int ret;
8060 icalcomponent *comp, *userdata = NULL, *store_ical = ical;
8061 icalcomponent_kind kind;
8062 icalproperty_method meth;
8063 icalproperty *prop;
8064 unsigned mykind = 0, tzbyref = 0;
8065 const char *organizer = NULL;
8066 const char *prop_annot =
8067 DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
8068 struct buf attrib = BUF_INITIALIZER;
8069 struct caldav_data *cdata;
8070 const char *uid;
8071 struct index_record *oldrecord = NULL, record;
8072 char datestr[80], *mimehdr;
8073 const char *sched_tag;
8074 uint32_t newuid = 0;
8075 strarray_t imapflags = STRARRAY_INITIALIZER;
8076
8077 /* Check for supported component type */
8078 comp = icalcomponent_get_first_real_component(ical);
8079 uid = icalcomponent_get_uid(comp);
8080 kind = icalcomponent_isa(comp);
8081 switch (kind) {
8082 case ICAL_VEVENT_COMPONENT: mykind = CAL_COMP_VEVENT; break;
8083 case ICAL_VTODO_COMPONENT: mykind = CAL_COMP_VTODO; break;
8084 case ICAL_VJOURNAL_COMPONENT: mykind = CAL_COMP_VJOURNAL; break;
8085 case ICAL_VFREEBUSY_COMPONENT: mykind = CAL_COMP_VFREEBUSY; break;
8086 case ICAL_VAVAILABILITY_COMPONENT: mykind = CAL_COMP_VAVAILABILITY; break;
8087 case ICAL_VPOLL_COMPONENT: mykind = CAL_COMP_VPOLL; break;
8088 default:
8089 txn->error.precond = CALDAV_SUPP_COMP;
8090 return HTTP_FORBIDDEN;
8091 }
8092
8093 if (!annotatemore_lookupmask(mailbox->name,
8094 prop_annot, httpd_userid, &attrib)
8095 && attrib.len) {
8096 unsigned long supp_comp = strtoul(buf_cstring(&attrib), NULL, 10);
8097
8098 buf_free(&attrib);
8099
8100 if (!(mykind & supp_comp)) {
8101 txn->error.precond = CALDAV_SUPP_COMP;
8102 return HTTP_FORBIDDEN;
8103 }
8104 }
8105
8106 /* Find message UID for the resource, if exists */
8107 caldav_lookup_resource(caldavdb, mailbox->name, resource, &cdata, 0);
8108
8109 /* does it already exist? */
8110 if (cdata->dav.imap_uid) {
8111 newuid = cdata->dav.imap_uid;
8112 /* Check for change of iCalendar UID */
8113 if (strcmp(cdata->ical_uid, uid)) {
8114 /* CALDAV:no-uid-conflict */
8115 txn->error.precond = CALDAV_UID_CONFLICT;
8116 return HTTP_FORBIDDEN;
8117 }
8118 /* Fetch index record for the resource */
8119 oldrecord = &record;
8120 mailbox_find_index_record(mailbox, cdata->dav.imap_uid, oldrecord);
8121 }
8122
8123 /* Remove all X-LIC-ERROR properties */
8124 icalcomponent_strip_errors(ical);
8125
8126 /* Remove all VTIMEZONE components for known TZIDs */
8127 if (namespace_calendar.allow & ALLOW_CAL_NOTZ) {
8128 strip_vtimezones(ical);
8129 tzbyref = 1;
8130 }
8131
8132 /* Set Schedule-Tag, if any */
8133 if (flags & NEW_STAG) {
8134 if (oldrecord) sched_tag = message_guid_encode(&oldrecord->guid);
8135 else sched_tag = NULL_ETAG;
8136 }
8137 else if (organizer) sched_tag = cdata->sched_tag;
8138 else sched_tag = cdata->sched_tag = NULL;
8139
8140 /* If we are just stripping VTIMEZONEs from resource, flag it */
8141 if (flags & TZ_STRIP) strarray_append(&imapflags, DFLAG_UNCHANGED);
8142 else if (mailbox->i.options & OPT_IMAP_SHAREDSEEN) {
8143 cdata->comp_flags.shared = 0;
8144 }
8145 else if (userid && (namespace_calendar.allow & ALLOW_USERDATA)) {
8146 ret = personalize_resource(txn, mailbox, ical,
8147 cdata, userid, &store_ical, &userdata);
8148 if (ret) goto done;
8149
8150 if (store_ical != ical) {
8151 comp = icalcomponent_get_first_real_component(store_ical);
8152 uid = icalcomponent_get_uid(comp);
8153 kind = icalcomponent_isa(comp);
8154 }
8155 }
8156
8157 /* Create and cache RFC 5322 header fields for resource */
8158 prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
8159 if (prop) {
8160 organizer = icalproperty_get_organizer(prop);
8161 if (organizer) {
8162 if (!strncasecmp(organizer, "mailto:", 7)) organizer += 7;
8163 assert(!buf_len(&txn->buf));
8164 buf_printf(&txn->buf, "<%s>", organizer);
8165 mimehdr = charset_encode_mimeheader(buf_cstring(&txn->buf),
8166 buf_len(&txn->buf), 0);
8167 spool_replace_header(xstrdup("From"), mimehdr, txn->req_hdrs);
8168 buf_reset(&txn->buf);
8169 }
8170 }
8171
8172 prop = icalcomponent_get_first_property(comp, ICAL_SUMMARY_PROPERTY);
8173 if (prop) {
8174 mimehdr = charset_encode_mimeheader(icalproperty_get_summary(prop), 0, 0);
8175 spool_replace_header(xstrdup("Subject"), mimehdr, txn->req_hdrs);
8176 }
8177 else spool_replace_header(xstrdup("Subject"),
8178 xstrdup(icalcomponent_kind_to_string(kind)),
8179 txn->req_hdrs);
8180
8181 if (strarray_size(schedule_addresses)) {
8182 char *value = strarray_join(schedule_addresses, ",");
8183 mimehdr = charset_encode_mimeheader(value, 0, 0);
8184 spool_replace_header(xstrdup("X-Schedule-User-Address"),
8185 mimehdr, txn->req_hdrs);
8186 free(value);
8187 }
8188
8189 time_to_rfc5322(icaltime_as_timet_with_zone(icalcomponent_get_dtstamp(comp),
8190 utc_zone),
8191 datestr, sizeof(datestr));
8192 spool_replace_header(xstrdup("Date"), xstrdup(datestr), txn->req_hdrs);
8193
8194 buf_reset(&txn->buf);
8195
8196 /* Use SHA1(uid)@servername as Message-ID */
8197 struct message_guid uuid;
8198 message_guid_generate(&uuid, uid, strlen(uid));
8199 buf_printf(&txn->buf, "<%s@%s>",
8200 message_guid_encode(&uuid), config_servername);
8201 spool_replace_header(xstrdup("Message-ID"),
8202 buf_release(&txn->buf), txn->req_hdrs);
8203
8204 buf_setcstr(&txn->buf, ICALENDAR_CONTENT_TYPE);
8205 if ((meth = icalcomponent_get_method(store_ical)) != ICAL_METHOD_NONE) {
8206 buf_printf(&txn->buf, "; method=%s",
8207 icalproperty_method_to_string(meth));
8208 }
8209 buf_printf(&txn->buf, "; component=%s", icalcomponent_kind_to_string(kind));
8210 spool_replace_header(xstrdup("Content-Type"),
8211 buf_release(&txn->buf), txn->req_hdrs);
8212
8213 buf_printf(&txn->buf, "attachment;\r\n\tfilename=\"%s\"", resource);
8214 if (sched_tag) buf_printf(&txn->buf, ";\r\n\tschedule-tag=%s", sched_tag);
8215 if (tzbyref) buf_printf(&txn->buf, ";\r\n\ttz-by-ref=true");
8216 if (cdata->comp_flags.shared) {
8217 buf_printf(&txn->buf, ";\r\n\tper-user-data=true");
8218 }
8219 spool_replace_header(xstrdup("Content-Disposition"),
8220 buf_release(&txn->buf), txn->req_hdrs);
8221
8222 spool_remove_header(xstrdup("Content-Description"), txn->req_hdrs);
8223
8224 /* Store the resource */
8225 ret = dav_store_resource(txn, icalcomponent_as_ical_string(store_ical), 0,
8226 mailbox, oldrecord, createdmodseq, &imapflags);
8227 strarray_fini(&imapflags);
8228
8229 newuid = mailbox->i.last_uid;
8230
8231 done:
8232 switch (ret) {
8233 case HTTP_CREATED:
8234 case HTTP_NO_CONTENT:
8235 if ((namespace_calendar.allow & ALLOW_USERDATA) &&
8236 cdata->comp_flags.shared) {
8237
8238 /* either the UID created by dav_store_resource, or if nothing but per-user data
8239 * was changed, the UID of the existing record */
8240 assert(newuid);
8241
8242 /* Ensure we have an astate connected to the mailbox,
8243 * so that the annotation txn will be committed
8244 * when we close the mailbox */
8245 annotate_state_t *astate = NULL;
8246
8247 if (oldrecord && (newuid != oldrecord->uid) &&
8248 !mailbox_get_annotate_state(mailbox, newuid, &astate)) {
8249 /* Copy across all per-message annotations.
8250
8251 XXX Hack until we fix annotation copying in
8252 append_fromstage() to preserve userid of private annots. */
8253 annotate_msg_copy(mailbox, oldrecord->uid,
8254 mailbox, newuid, NULL);
8255 }
8256
8257 int r = write_personal_data(userid, mailbox, newuid,
8258 mailbox->i.highestmodseq+1, userdata);
8259 if (r) {
8260 /* XXX We have already written the stripped resource
8261 so we're pretty screwed. All message annotations
8262 need to be handled (properly) in append_fromstage()
8263 so storing resource and annotations is atomic.
8264 */
8265 txn->error.desc = error_message(r);
8266 ret = HTTP_SERVER_ERROR;
8267 goto done;
8268 }
8269
8270 if (store_ical) {
8271 /* Write shared modseq for resource */
8272 buf_printf(&txn->buf, MODSEQ_FMT, mailbox->i.highestmodseq);
8273 mailbox_get_annotate_state(mailbox, newuid, NULL);
8274 mailbox_annotation_write(mailbox, newuid, SHARED_MODSEQ,
8275 /* shared */ "", &txn->buf);
8276 buf_reset(&txn->buf);
8277 }
8278
8279 if (!cdata->organizer || (flags & PREFER_REP)) {
8280 /* Read index record for new message (always the last one) */
8281 struct index_record newrecord;
8282
8283 cdata->dav.alive = 1;
8284 cdata->dav.imap_uid = newuid;
8285
8286 caldav_get_validators(mailbox, cdata, userid, &newrecord,
8287 &txn->resp_body.etag,
8288 &txn->resp_body.lastmod);
8289
8290 if (flags & PREFER_REP) {
8291 /* Re-insert per-user data */
8292 icalcomponent_apply_vpatch(ical, userdata, NULL, NULL);
8293 }
8294 }
8295 }
8296
8297 if (cdata->organizer) {
8298 if (flags & NEW_STAG) txn->resp_body.stag = sched_tag;
8299
8300 if (!(flags & PREFER_REP)) {
8301 /* iCal data has been rewritten - don't return validators */
8302 txn->resp_body.lastmod = 0;
8303 txn->resp_body.etag = NULL;
8304 }
8305 }
8306 break;
8307 }
8308
8309 if (userdata) icalcomponent_free(userdata);
8310 if (store_ical && (store_ical != ical)) icalcomponent_free(store_ical);
8311
8312 return ret;
8313 }
8314
8315
8316 static struct mime_type_t freebusy_mime_types[] = {
8317 /* First item MUST be the default type */
8318 { ICALENDAR_CONTENT_TYPE, "2.0", "ifb",
8319 (struct buf* (*)(void *)) &my_icalcomponent_as_ical_string,
8320 NULL, NULL, NULL, NULL
8321 },
8322 { "application/calendar+xml; charset=utf-8", NULL, "xfb",
8323 (struct buf* (*)(void *)) &icalcomponent_as_xcal_string,
8324 NULL, NULL, NULL, NULL
8325 },
8326 { "application/calendar+json; charset=utf-8", NULL, "jfb",
8327 (struct buf* (*)(void *)) &icalcomponent_as_jcal_string,
8328 NULL, NULL, NULL, NULL
8329 },
8330 { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
8331 };
8332
8333
8334 /* Execute a free/busy query per
8335 http://www.calconnect.org/pubdocs/CD0903%20Freebusy%20Read%20URL.pdf */
meth_get_head_fb(struct transaction_t * txn,void * params)8336 static int meth_get_head_fb(struct transaction_t *txn, void *params)
8337
8338 {
8339 struct meth_params *gparams = (struct meth_params *) params;
8340 int ret = 0, r, rights;
8341 struct tm *tm;
8342 struct strlist *param;
8343 struct mime_type_t *mime = NULL;
8344 struct propfind_ctx fctx;
8345 struct freebusy_filter fbfilter;
8346 time_t start;
8347 struct icaldurationtype period = icaldurationtype_null_duration();
8348 icalcomponent *cal;
8349
8350 /* Parse the path */
8351 r = dav_parse_req_target(txn, gparams);
8352 if (r) return r;
8353
8354 if (txn->req_tgt.resource ||
8355 !(txn->req_tgt.userid)) {
8356 /* We don't handle GET on a resources or non-calendar collections */
8357 return HTTP_NO_CONTENT;
8358 }
8359
8360 /* Check ACL for current user */
8361 rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
8362 if (!(rights & DACL_READFB)) {
8363 /* DAV:need-privileges */
8364 txn->error.precond = DAV_NEED_PRIVS;
8365 txn->error.resource = txn->req_tgt.path;
8366 txn->error.rights = DACL_READFB;
8367 return HTTP_NO_PRIVS;
8368 }
8369
8370 if (txn->req_tgt.mbentry->server) {
8371 /* Remote mailbox */
8372 struct backend *be;
8373
8374 be = proxy_findserver(txn->req_tgt.mbentry->server,
8375 &http_protocol, httpd_userid,
8376 &backend_cached, NULL, NULL, httpd_in);
8377 if (!be) return HTTP_UNAVAILABLE;
8378
8379 return http_pipe_req_resp(be, txn);
8380 }
8381
8382 /* Local Mailbox */
8383
8384 /* Check/find 'format' */
8385 param = hash_lookup("format", &txn->req_qparams);
8386 if (param) {
8387 if (param->next /* once only */) return HTTP_BAD_REQUEST;
8388
8389 for (mime = freebusy_mime_types; mime->content_type; mime++) {
8390 if (is_mediatype(param->s, mime->content_type)) break;
8391 }
8392 }
8393 else mime = freebusy_mime_types;
8394
8395 if (!mime || !mime->content_type) return HTTP_NOT_ACCEPTABLE;
8396
8397 memset(&fbfilter, 0, sizeof(struct freebusy_filter));
8398 fbfilter.flags = CHECK_CAL_TRANSP | CHECK_USER_AVAIL;
8399
8400 /* Check for 'start' */
8401 param = hash_lookup("start", &txn->req_qparams);
8402 if (param) {
8403 if (param->next /* once only */) return HTTP_BAD_REQUEST;
8404
8405 fbfilter.start = icaltime_from_rfc3339_string(param->s);
8406 if (icaltime_is_null_time(fbfilter.start)) return HTTP_BAD_REQUEST;
8407
8408 /* Default to end of given day */
8409 start = icaltime_as_timet_with_zone(fbfilter.start, utc_zone);
8410 tm = localtime(&start);
8411
8412 period.seconds = 60 - tm->tm_sec;
8413 period.minutes = 59 - tm->tm_min;
8414 period.hours = 23 - tm->tm_hour;
8415 }
8416 else {
8417 /* Default to start of current day */
8418 start = (time(0) / 86400) * 86400;
8419 fbfilter.start = icaltime_from_timet_with_zone(start, 0, utc_zone);
8420
8421 /* Default to 42 day period */
8422 period.days = 42;
8423 }
8424
8425 /* Check for 'period' */
8426 param = hash_lookup("period", &txn->req_qparams);
8427 if (param) {
8428 if (param->next /* once only */ ||
8429 hash_lookup("end", &txn->req_qparams) /* can't use with 'end' */)
8430 return HTTP_BAD_REQUEST;
8431
8432 period = icaldurationtype_from_string(param->s);
8433 if (icaldurationtype_is_bad_duration(period)) return HTTP_BAD_REQUEST;
8434 }
8435
8436 /* Check for 'end' */
8437 param = hash_lookup("end", &txn->req_qparams);
8438 if (param) {
8439 if (param->next /* once only */) return HTTP_BAD_REQUEST;
8440
8441 fbfilter.end = icaltime_from_rfc3339_string(param->s);
8442 if (icaltime_is_null_time(fbfilter.end)) return HTTP_BAD_REQUEST;
8443 }
8444 else {
8445 /* Set end based on period */
8446 fbfilter.end = icaltime_add(fbfilter.start, period);
8447 }
8448
8449
8450 memset(&fctx, 0, sizeof(struct propfind_ctx));
8451 fctx.txn = txn;
8452 fctx.req_tgt = &txn->req_tgt;
8453 fctx.depth = 2;
8454 fctx.userid = httpd_userid;
8455 fctx.userisadmin = httpd_userisadmin;
8456 fctx.authstate = httpd_authstate;
8457 fctx.reqd_privs = 0; /* handled by CALDAV:schedule-deliver on Inbox */
8458 fctx.filter_crit = &fbfilter;
8459 fctx.ret = &ret;
8460
8461 cal = busytime_query_local(txn, &fctx, txn->req_tgt.mbentry->name,
8462 0, NULL, NULL, NULL);
8463
8464 if (fbfilter.freebusy.fb) free(fbfilter.freebusy.fb);
8465
8466 if (cal) {
8467 const char *proto, *host;
8468 icalcomponent *fb;
8469 icalproperty *url;
8470
8471 /* Construct URL */
8472 buf_reset(&txn->buf);
8473 http_proto_host(txn->req_hdrs, &proto, &host);
8474 buf_printf(&txn->buf, "%s://%s%s", proto, host, txn->req_uri->path);
8475 if (URI_QUERY(txn->req_uri))
8476 buf_printf(&txn->buf, "?%s", URI_QUERY(txn->req_uri));
8477
8478 /* Set URL property */
8479 fb = icalcomponent_get_first_component(cal, ICAL_VFREEBUSY_COMPONENT);
8480 url = icalproperty_new_url(buf_cstring(&txn->buf));
8481 icalcomponent_add_property(fb, url);
8482
8483 /* Set filename of resource */
8484 buf_reset(&txn->buf);
8485 buf_printf(&txn->buf, "%s.%s",
8486 txn->req_tgt.userid,
8487 mime->file_ext);
8488 txn->resp_body.dispo.fname = buf_cstring(&txn->buf);
8489
8490 txn->resp_body.type = mime->content_type;
8491
8492 /* iCalendar data in response should not be transformed */
8493 txn->flags.cc |= CC_NOTRANSFORM;
8494
8495 /* Output the iCalendar object */
8496 struct buf *cal_str = mime->from_object(cal);
8497 icalcomponent_free(cal);
8498
8499 write_body(HTTP_OK, txn, buf_base(cal_str), buf_len(cal_str));
8500 buf_destroy(cal_str);
8501 }
8502 else ret = HTTP_NOT_FOUND;
8503
8504 return ret;
8505 }
8506
8507
meth_options_cal(struct transaction_t * txn,void * params)8508 static int meth_options_cal(struct transaction_t *txn, void *params)
8509 {
8510 struct meth_params *oparams = (struct meth_params *) params;
8511 int r;
8512
8513 /* Parse the path */
8514 r = dav_parse_req_target(txn, oparams);
8515 if (r) return r;
8516
8517 if (txn->req_tgt.allow & ALLOW_PATCH) {
8518 /* Add Accept-Patch formats to response */
8519 txn->resp_body.patch = caldav_patch_docs;
8520 }
8521 if (txn->req_tgt.collection && !txn->req_tgt.resource) {
8522 /* Add subscription upgrade links */
8523 struct buf link = BUF_INITIALIZER;
8524
8525 buf_printf(&link, "<%s>; rel=\"subscribe-caldav_auth\"",
8526 txn->req_tgt.path);
8527 strarray_appendm(&txn->resp_body.links, buf_release(&link));
8528 buf_printf(&link, "<%s>; rel=\"subscribe-webdav_sync\"",
8529 txn->req_tgt.path);
8530 strarray_appendm(&txn->resp_body.links, buf_release(&link));
8531 buf_printf(&link, "<%s>; rel=\"subscribe-enhanced-get\"",
8532 txn->req_tgt.path);
8533 strarray_appendm(&txn->resp_body.links, buf_release(&link));
8534 }
8535
8536 return meth_options(txn, oparams->parse_path);
8537 }
8538