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