diff --git a/imap/http_dav.c b/imap/http_dav.c index 1efd0054c..290db5b2e 100644 --- a/imap/http_dav.c +++ b/imap/http_dav.c @@ -1,8983 +1,8969 @@ /* http_dav.c -- Routines for dealing with DAV properties in httpd * * Copyright (c) 1994-2011 Carnegie Mellon University. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The name "Carnegie Mellon University" must not be used to * endorse or promote products derived from this software without * prior written permission. For permission or any legal * details, please contact * Carnegie Mellon University * Center for Technology Transfer and Enterprise Creation * 4615 Forbes Avenue * Suite 302 * Pittsburgh, PA 15213 * (412) 268-7393, fax: (412) 268-7395 * innovation@andrew.cmu.edu * * 4. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by Computing Services * at Carnegie Mellon University (http://www.cmu.edu/computing/)." * * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ /* * TODO: * * - CALDAV:supported-calendar-component-set should be a bitmask in * cyrus.index header Mailbox Options field * * - CALDAV:schedule-calendar-transp should be a flag in * cyrus.index header (Mailbox Options) * * - DAV:creationdate should be added to cyrus.header since it only * gets set at creation time * * - Should add a last_metadata_update field to cyrus.index header * for use in PROPFIND, PROPPATCH, and possibly REPORT. * This would get updated any time a mailbox annotation, mailbox * acl, or quota root limit is changed * * - Should we use cyrus.index header Format field to indicate * CalDAV mailbox? * */ #include #include "annotate.h" #include "acl.h" #include "append.h" #include "dlist.h" #include "global.h" #include "http_dav.h" #include "http_dav_sharing.h" #include "http_proxy.h" #include "index.h" #include "proxy.h" #include "times.h" #include "syslog.h" #include "strhash.h" #include "sync_support.h" #include "tok.h" #include "user.h" #include "util.h" #include "version.h" #include "xmalloc.h" #include "xml_support.h" #include "xstrlcat.h" #include "xstrlcpy.h" #include "xstrnchr.h" /* generated headers are not necessarily in current directory */ #include "imap/http_err.h" #include "imap/imap_err.h" #include #include static const struct dav_namespace_t { const char *href; const char *prefix; } known_namespaces[] = { { XML_NS_DAV, "D" }, { XML_NS_CALDAV, "C" }, { XML_NS_CARDDAV, "C" }, { XML_NS_ISCHED, NULL }, { XML_NS_CS, "CS" }, { XML_NS_MECOM, "MC" }, { XML_NS_MOBME, "MM" }, { XML_NS_CYRUS, "CY" }, { XML_NS_USERFLAG, "UF" }, { XML_NS_SYSFLAG, "SF" }, }; static const struct match_type_t dav_match_types[] = { { "contains", MATCH_TYPE_CONTAINS }, { "equals", MATCH_TYPE_EQUALS }, { "starts-with", MATCH_TYPE_PREFIX }, { "ends-with", MATCH_TYPE_SUFFIX }, { NULL, 0 } }; static const struct collation_t dav_collations[] = { { "i;unicode-casemap", COLLATION_UNICODE }, { "i;ascii-casemap", COLLATION_ASCII }, { "i;octet", COLLATION_OCTET }, { NULL, 0 } }; static xmlChar *server_info = NULL; static int server_info_size = 0; static time_t server_info_lastmod = 0; static struct buf server_info_token = BUF_INITIALIZER; static struct buf server_info_link = BUF_INITIALIZER; static void my_dav_init(struct buf *serverinfo); static void my_dav_shutdown(void); static int get_server_info(struct transaction_t *txn); static unsigned long principal_allow_cb(struct request_target_t *tgt); static int principal_parse_path(const char *path, struct request_target_t *tgt, const char **resultstr); static int propfind_principalname(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop, xmlNodePtr resp, struct propstat propstat[], void *rock); static int proppatch_principalname(xmlNodePtr prop, unsigned set, struct proppatch_ctx *pctx, struct propstat propstat[], void *rock); static int propfind_restype(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop, xmlNodePtr resp, struct propstat propstat[], void *rock); static int propfind_alturiset(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop, xmlNodePtr resp, struct propstat propstat[], void *rock); static int principal_search(const char *userid, void *rock); static int report_prin_prop_search(struct transaction_t *txn, struct meth_params *rparams, xmlNodePtr inroot, struct propfind_ctx *fctx); static int report_prin_search_prop_set(struct transaction_t *txn, struct meth_params *rparams, xmlNodePtr inroot, struct propfind_ctx *fctx); static int allprop_cb(const char *mailbox __attribute__((unused)), uint32_t uid __attribute__((unused)), const char *entry, const char *userid, const struct buf *attrib, const struct annotate_metadata *mdata __attribute__((unused)), void *rock); /* Array of supported REPORTs */ static const struct report_type_t principal_reports[] = { /* WebDAV Versioning (RFC 3253) REPORTs */ { "expand-property", NS_DAV, "multistatus", &report_expand_prop, DACL_READ, 0 }, /* WebDAV ACL (RFC 3744) REPORTs */ { "principal-property-search", NS_DAV, "multistatus", &report_prin_prop_search, 0, REPORT_ALLOW_PROPS | REPORT_DEPTH_ZERO }, { "principal-search-property-set", NS_DAV, "principal-search-property-set", &report_prin_search_prop_set, 0, REPORT_DEPTH_ZERO }, { NULL, 0, NULL, NULL, 0, 0 } }; /* Array of known "live" properties */ static const struct prop_entry principal_props[] = { /* WebDAV (RFC 4918) properties */ { "creationdate", NS_DAV, PROP_ALLPROP, NULL, NULL, NULL }, { "displayname", NS_DAV, PROP_ALLPROP | PROP_COLLECTION, propfind_principalname, proppatch_principalname, NULL }, { "getcontentlanguage", NS_DAV, PROP_ALLPROP, NULL, NULL, NULL }, { "getcontentlength", NS_DAV, PROP_ALLPROP | PROP_COLLECTION, propfind_getlength, NULL, NULL }, { "getcontenttype", NS_DAV, PROP_ALLPROP, NULL, NULL, NULL }, { "getetag", NS_DAV, PROP_ALLPROP, NULL, NULL, NULL }, { "getlastmodified", NS_DAV, PROP_ALLPROP, NULL, NULL, NULL }, { "lockdiscovery", NS_DAV, PROP_ALLPROP | PROP_COLLECTION, propfind_lockdisc, NULL, NULL }, { "resourcetype", NS_DAV, PROP_ALLPROP | PROP_COLLECTION, propfind_restype, NULL, NULL }, { "supportedlock", NS_DAV, PROP_ALLPROP | PROP_COLLECTION, propfind_suplock, NULL, NULL }, /* WebDAV Versioning (RFC 3253) properties */ { "supported-report-set", NS_DAV, PROP_COLLECTION, propfind_reportset, NULL, (void *) principal_reports }, { "supported-method-set", NS_DAV, PROP_COLLECTION, propfind_methodset, NULL, (void *) &principal_allow_cb }, /* WebDAV ACL (RFC 3744) properties */ { "alternate-URI-set", NS_DAV, PROP_COLLECTION, propfind_alturiset, NULL, NULL }, { "principal-URL", NS_DAV, PROP_COLLECTION, propfind_principalurl, NULL, NULL }, { "group-member-set", NS_DAV, PROP_COLLECTION, NULL, NULL, NULL }, { "group-membership", NS_DAV, PROP_COLLECTION, NULL, NULL, NULL }, { "principal-collection-set", NS_DAV, PROP_COLLECTION, propfind_princolset, NULL, NULL }, /* WebDAV Current Principal (RFC 5397) properties */ { "current-user-principal", NS_DAV, PROP_COLLECTION, propfind_curprin, NULL, NULL }, /* CalDAV (RFC 4791) properties */ { "calendar-home-set", NS_CALDAV, PROP_COLLECTION, propfind_calurl, NULL, NULL }, /* CalDAV Scheduling (RFC 6638) properties */ { "schedule-inbox-URL", NS_CALDAV, PROP_COLLECTION, propfind_calurl, NULL, SCHED_INBOX }, { "schedule-outbox-URL", NS_CALDAV, PROP_COLLECTION, propfind_calurl, NULL, SCHED_OUTBOX }, { "calendar-user-address-set", NS_CALDAV, PROP_COLLECTION, propfind_caluseraddr, proppatch_caluseraddr, NULL }, { "calendar-user-type", NS_CALDAV, PROP_COLLECTION, propfind_calusertype, NULL, NULL }, /* CardDAV (RFC 6352) properties */ { "addressbook-home-set", NS_CARDDAV, PROP_COLLECTION, propfind_abookhome, NULL, NULL }, /* WebDAV Notifications (draft-pot-webdav-notifications) properties */ { "notification-URL", NS_DAV, PROP_COLLECTION, propfind_notifyurl, NULL, NULL }, /* Backwards compatibility with Apple notifications clients */ { "notification-URL", NS_CS, PROP_COLLECTION, propfind_notifyurl, NULL, NULL }, { "email-address-set", NS_CS, PROP_COLLECTION, propfind_caluseremail, NULL, NULL }, { NULL, 0, 0, NULL, NULL, NULL } }; struct meth_params princ_params = { .parse_path = &principal_parse_path, .propfind = { 0, principal_props }, .reports = principal_reports }; /* Namespace for WebDAV principals */ struct namespace_t namespace_principal = { URL_NS_PRINCIPAL, 0, "principal", "/dav/principals", NULL, http_allow_noauth_get, /*authschemes*/0, /*mbtype */ 0, ALLOW_READ | ALLOW_DAV | ALLOW_PROPPATCH, &my_dav_init, NULL, NULL, &my_dav_shutdown, &dav_premethod, { { NULL, NULL }, /* ACL */ { NULL, NULL }, /* BIND */ { NULL, NULL }, /* CONNECT */ { NULL, NULL }, /* COPY */ { NULL, NULL }, /* DELETE */ { &meth_get_head, &princ_params }, /* GET */ { &meth_get_head, &princ_params }, /* HEAD */ { NULL, NULL }, /* LOCK */ { NULL, NULL }, /* MKCALENDAR */ { NULL, NULL }, /* MKCOL */ { NULL, NULL }, /* MOVE */ { &meth_options, NULL }, /* OPTIONS */ { NULL, NULL }, /* PATCH */ { NULL, NULL }, /* POST */ { &meth_propfind, &princ_params }, /* PROPFIND */ { &meth_proppatch, &princ_params }, /* PROPPATCH */ { NULL, NULL }, /* PUT */ { &meth_report, &princ_params }, /* REPORT */ { &meth_trace, NULL }, /* TRACE */ { NULL, NULL }, /* UNBIND */ { NULL, NULL } /* UNLOCK */ } }; /* Linked-list of properties for fetching */ struct propfind_entry_list { xmlChar *name; /* Property name (needs to be freed) */ xmlNsPtr ns; /* Property namespace */ unsigned char flags; /* Flags for how/where prop apply */ int (*get)(const xmlChar *name, /* Callback to fetch property */ xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop, xmlNodePtr resp, struct propstat propstat[], void *rock); xmlNodePtr prop; /* Property node from request */ void *rock; /* Add'l data to pass to callback */ struct propfind_entry_list *next; }; /* Bitmask of privilege flags */ enum { PRIV_IMPLICIT = (1<<0), PRIV_INBOX = (1<<1), PRIV_OUTBOX = (1<<2), PRIV_CONTAINED = (1<<3) }; /* Array of precondition/postcondition errors */ static const struct precond_t { const char *name; /* Property name */ unsigned ns; /* Index into known namespace array */ } preconds[] = { /* Placeholder for zero (no) precondition code */ { NULL, 0 }, /* WebDAV (RFC 4918) preconditions */ { "cannot-modify-protected-property", NS_DAV }, { "lock-token-matches-request-uri", NS_DAV }, { "lock-token-submitted", NS_DAV }, { "no-conflicting-lock", NS_DAV }, { "propfind-finite-depth", NS_DAV }, /* WebDAV Versioning (RFC 3253) preconditions */ { "supported-report", NS_DAV }, { "resource-must-be-null", NS_DAV }, /* WebDAV ACL (RFC 3744) preconditions */ { "need-privileges", NS_DAV }, { "no-invert", NS_DAV }, { "no-abstract", NS_DAV }, { "not-supported-privilege", NS_DAV }, { "recognized-principal", NS_DAV }, { "allowed-principal", NS_DAV }, { "grant-only", NS_DAV }, /* WebDAV Quota (RFC 4331) preconditions */ { "quota-not-exceeded", NS_DAV }, { "sufficient-disk-space", NS_DAV }, /* WebDAV Extended MKCOL (RFC 5689) preconditions */ { "valid-resourcetype", NS_DAV }, /* WebDAV Sync (RFC 6578) preconditions */ { "valid-sync-token", NS_DAV }, { "number-of-matches-within-limits", NS_DAV }, /* CalDAV (RFC 4791) preconditions */ { "supported-calendar-data", NS_CALDAV }, { "valid-calendar-data", NS_CALDAV }, { "valid-calendar-object-resource", NS_CALDAV }, { "supported-calendar-component", NS_CALDAV }, { "calendar-collection-location-ok", NS_CALDAV }, { "no-uid-conflict", NS_CALDAV }, { "supported-filter", NS_CALDAV }, { "valid-filter", NS_CALDAV }, { "supported-collation", NS_CALDAV }, { "max-resource-size", NS_CALDAV }, /* RSCALE (RFC 7529) preconditions */ { "supported-rscale", NS_CALDAV }, /* Time Zones by Reference (RFC 7809) preconditions */ { "valid-timezone", NS_CALDAV }, /* Managed Attachments (draft-ietf-calext-caldav-attachments) preconditions */ { "valid-managed-id", NS_CALDAV }, /* Bulk Change (draft-daboo-calendarserver-bulk-change) preconditions */ { "ctag-ok", NS_MECOM }, /* CalDAV Scheduling (RFC 6638) preconditions */ { "valid-scheduling-message", NS_CALDAV }, { "valid-organizer", NS_CALDAV }, { "unique-scheduling-object-resource", NS_CALDAV }, { "same-organizer-in-all-components", NS_CALDAV }, { "allowed-organizer-scheduling-object-change", NS_CALDAV }, { "allowed-attendee-scheduling-object-change", NS_CALDAV }, { "default-calendar-needed", NS_CALDAV }, { "valid-schedule-default-calendar-URL", NS_CALDAV }, /* iSchedule (draft-desruisseaux-ischedule) preconditions */ { "version-not-supported", NS_ISCHED }, { "invalid-calendar-data-type", NS_ISCHED }, { "invalid-calendar-data", NS_ISCHED }, { "invalid-scheduling-message", NS_ISCHED }, { "originator-missing", NS_ISCHED }, { "too-many-originators", NS_ISCHED }, { "originator-invalid", NS_ISCHED }, { "originator-denied", NS_ISCHED }, { "recipient-missing", NS_ISCHED }, { "recipient-mismatch", NS_ISCHED }, { "verification-failed", NS_ISCHED }, /* CardDAV (RFC 6352) preconditions */ { "supported-address-data", NS_CARDDAV }, { "valid-address-data", NS_CARDDAV }, { "no-uid-conflict", NS_CARDDAV }, { "addressbook-collection-location-ok", NS_CARDDAV }, { "supported-filter", NS_CARDDAV }, { "supported-collation", NS_CARDDAV }, { "max-resource-size", NS_CARDDAV }, }; void dav_precond_as_string(struct buf *buf, struct error_t *err) { const struct precond_t *precond = &preconds[err->precond]; switch (precond->ns) { case NS_DAV: buf_appendcstr(buf, "DAV:"); break; case NS_CALDAV: buf_appendcstr(buf, "CALDAV:"); break; case NS_CARDDAV: buf_appendcstr(buf, "CARDDAV:"); break; case NS_ISCHED: buf_appendcstr(buf, "ISCHED:"); break; case NS_MECOM: buf_appendcstr(buf, "MECOM:"); break; } buf_appendcstr(buf, precond->name); } /* Check ACL on userid's principal (Inbox): ACL_LOOKUP right gives access */ static int principal_acl_check(const char *userid, struct auth_state *authstate) { int r = 0; if (!httpd_userisadmin) { char *mboxname = caldav_mboxname(userid, NULL); mbentry_t *mbentry = NULL; r = proxy_mlookup(mboxname, &mbentry, NULL, NULL); if (r) { syslog(LOG_ERR, "mlookup(%s) failed: %s", mboxname, error_message(r)); r = HTTP_NOT_FOUND; } else if (!(httpd_myrights(authstate, mbentry) & ACL_LOOKUP)) { // allow READ (for owner) or USER6 (to grant access generally without anything else) r = HTTP_NOT_FOUND; } mboxlist_entry_free(&mbentry); free(mboxname); } return r; } /* Determine allowed methods in DAV principals namespace */ static unsigned long principal_allow_cb(struct request_target_t *tgt) { return tgt->namespace->allow; } static struct backend *proxy_findinboxserver(const char *userid) { mbentry_t *mbentry = NULL; struct backend *s = NULL; char *inbox = mboxname_user_mbox(userid, NULL); int r = mboxlist_lookup(inbox, &mbentry, NULL); free(inbox); if (r) return NULL; if (mbentry->mbtype & MBTYPE_REMOTE) { s = proxy_findserver(mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); } mboxlist_entry_free(&mbentry); return s; } /* Parse request-target path in DAV principals namespace */ static int principal_parse_path(const char *path, struct request_target_t *tgt, const char **resultstr) { char *p; size_t len; /* Make a working copy of target path */ strlcpy(tgt->path, path, sizeof(tgt->path)); tgt->tail = tgt->path + strlen(tgt->path); p = tgt->path; /* Sanity check namespace */ len = strlen(namespace_principal.prefix); if (strlen(p) < len || strncmp(namespace_principal.prefix, p, len) || (path[len] && path[len] != '/')) { *resultstr = "Namespace mismatch request target path"; return HTTP_FORBIDDEN; } /* Skip past namespace (and any extra '/') */ for (p += len; p[1] == '/'; p++); if (!*p || !*++p) { /* Make sure collection is terminated with '/' */ if (p[-1] != '/') *p++ = '/'; return 0; } /* Check if we're in user space */ len = strcspn(p, "/"); if (!strncmp(p, USER_COLLECTION_PREFIX, len)) { /* Skip past user prefix (and any extra '/') */ for (p += len; p[1] == '/'; p++); if (!*p || !*++p) { /* Make sure collection is terminated with '/' */ if (p[-1] != '/') *p++ = '/'; return 0; } /* Get user id */ len = strcspn(p, "/"); tgt->userid = xstrndup(p, len); if (httpd_extradomain) { char *at = strchr(tgt->userid, '@'); if (at && !strcmp(at+1, httpd_extradomain)) *at = 0; } /* Skip past userid (and any extra '/') */ for (p += len; p[1] == '/'; p++); if (!*p || !*++p) goto mailbox; } else if (!strncmp(p, SERVER_INFO, len)) { p += len; if (!*p || !*++p) { tgt->flags = TGT_SERVER_INFO; return 0; } } else return HTTP_NOT_FOUND; /* need to specify a userid */ if (*p) { // *resultstr = "Too many segments in request target path"; return HTTP_NOT_FOUND; } mailbox: if (tgt->userid) { /* Locate the home-set mailbox */ char *mboxname = NULL; if (tgt->allow & ALLOW_CAL) mboxname = mboxname_cal(tgt->userid, NULL); else if (tgt->allow & ALLOW_CARD) mboxname = mboxname_abook(tgt->userid, NULL); else mboxname = mboxname_drive(tgt->userid, NULL); int r = proxy_mlookup(mboxname, &tgt->mbentry, NULL, NULL); if (r) { *resultstr = error_message(r); syslog(LOG_ERR, "mlookup(%s) failed: %s", mboxname, *resultstr); } free(mboxname); switch (r) { case 0: break; case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; default: return HTTP_SERVER_ERROR; } } return 0; } /* Determine allowed methods in Cal/CardDAV namespace */ HIDDEN unsigned long calcarddav_allow_cb(struct request_target_t *tgt) { unsigned long allow = tgt->namespace->allow; if (!tgt->userid) { allow &= ALLOW_READ_MASK; } else if (!tgt->collection) { allow &= ~(ALLOW_DELETE | ALLOW_PATCH | ALLOW_POST | ALLOW_WRITE); } else if (!tgt->resource) { allow &= ~(ALLOW_MKCOL | ALLOW_PATCH); } else { allow &= ~(ALLOW_MKCOL | ALLOW_POST); } return allow; } /* Parse request-target path in *DAV namespace */ EXPORTED int dav_parse_req_target(struct transaction_t *txn, struct meth_params *params) { const char *resultstr = NULL; int r; r = params->parse_path(txn->req_uri->path, &txn->req_tgt, &resultstr); if (r) { if (r == HTTP_MOVED) txn->location = resultstr; else txn->error.desc = resultstr; } return r; } /* Parse a path in Cal/CardDAV namespace */ HIDDEN int calcarddav_parse_path(const char *path, struct request_target_t *tgt, const char *mboxprefix, const char **resultstr) { char *p, *owner = NULL, *collection = NULL, *freeme = NULL; size_t len; const char *mboxname; mbname_t *mbname = NULL; int ret = 0; static struct buf redirect_buf = BUF_INITIALIZER; if (*tgt->path) return 0; /* Already parsed */ /* Make a working copy of target path */ strlcpy(tgt->path, path, sizeof(tgt->path)); tgt->tail = tgt->path + strlen(tgt->path); p = tgt->path; /* Sanity check namespace */ len = strlen(tgt->namespace->prefix); if (strlen(p) < len || strncmp(tgt->namespace->prefix, p, len) || (path[len] && path[len] != '/')) { *resultstr = "Namespace mismatch request target path"; return HTTP_FORBIDDEN; } tgt->mboxprefix = mboxprefix; /* Default to bare-bones Allow bits */ tgt->allow &= ALLOW_READ_MASK; /* Skip past namespace (and any extra '/') */ for (p += len; p[1] == '/'; p++); if (!*p || !*++p) return 0; /* Check if we're in user space */ len = strcspn(p, "/"); /* zzzz is part of the FastMail sorting hack to make shared collections * always appear later */ if (!strncmp(p, USER_COLLECTION_PREFIX, len) || !strncmp(p, "zzzz", len)) { if (!strncmp(p, "zzzz", len)) tgt->flags |= TGT_USER_ZZZZ; /* Skip past user prefix (and any extra '/') */ for (p += len; p[1] == '/'; p++); if (!*p || !*++p) return 0; /* Get user id */ len = strcspn(p, "/"); tgt->userid = xstrndup(p, len); /* Skip past userid (and any extra '/') */ for (p += len; p[1] == '/'; p++); if (!*p || !*++p) { /* Make sure home-set is terminated with '/' */ if (p[-1] != '/') *p++ = '/'; goto mailbox; } len = strcspn(p, "/"); } /* Get collection */ tgt->collection = p; tgt->collen = len; /* Skip past collection (and any extra '/') */ for (p += len; p[1] == '/'; p++); if (!*p || !*++p) { /* Make sure collection is terminated with '/' */ if (p[-1] != '/') *p++ = '/'; goto mailbox; } /* Get resource */ len = strcspn(p, "/"); tgt->resource = p; tgt->reslen = len; p += len; if (*p) { // *resultstr = "Too many segments in request target path"; return HTTP_NOT_FOUND; } mailbox: /* Create mailbox name from the parsed path */ owner = tgt->userid; if (tgt->collen) { collection = freeme = xstrndup(tgt->collection, tgt->collen); p = strrchr(collection, SHARED_COLLECTION_DELIM); if (p) { if (tgt->mbentry) { /* MKCOL or COPY/MOVE destination */ *resultstr = "Invalid characters in collection name"; ret = HTTP_FORBIDDEN; goto done; } else { /* Shared collection encoded as: "." */ owner = collection; *p++ = '\0'; collection = p; tgt->flags = TGT_DAV_SHARED; tgt->allow |= ALLOW_DELETE; } } } mbname = mbname_from_userid(owner); mbname_push_boxes(mbname, mboxprefix); if (collection) { mbname_push_boxes(mbname, collection); } /* XXX - hack to allow @domain parts for non-domain-split users */ if (httpd_extradomain) { /* not allowed to be cross domain */ if (mbname_localpart(mbname) && strcmpsafe(mbname_domain(mbname), httpd_extradomain)) { ret = HTTP_NOT_FOUND; goto done; } mbname_set_domain(mbname, NULL); } mboxname = mbname_intname(mbname); /* Check for FastMail legacy sharing URLs and redirect */ if (httpd_userid && !config_getswitch(IMAPOPT_FASTMAILSHARING) && tgt->flags != TGT_DAV_SHARED && !mboxname_userownsmailbox(httpd_userid, mboxname)) { buf_reset(&redirect_buf); buf_printf(&redirect_buf, "%s/%s/%s/%s%c%s", tgt->namespace->prefix, USER_COLLECTION_PREFIX, httpd_userid, tgt->userid, SHARED_COLLECTION_DELIM, tgt->collection); *resultstr = buf_cstring(&redirect_buf); ret = HTTP_MOVED; goto done; } if (tgt->mbentry) { /* Just return the mboxname (MKCOL or COPY/MOVE destination) */ tgt->mbentry->name = xstrdup(mboxname); ret = mboxlist_createmailboxcheck(mboxname, 0, NULL, httpd_userisadmin, httpd_userid, httpd_authstate, NULL, NULL, 0 /* force */); if (ret) { if (ret == IMAP_MAILBOX_BADNAME) *resultstr = "Invalid name. Percent encoded HTTP URLs are in theory valid, but in practice not supported."; goto done; } tgt->allow |= ALLOW_MKCOL; } else if (*mboxname) { /* Locate the mailbox */ int r = proxy_mlookup(mboxname, &tgt->mbentry, NULL, NULL); if (r) { *resultstr = error_message(r); syslog(LOG_ERR, "mlookup(%s) failed: %s", mboxname, *resultstr); switch (r) { case IMAP_PERMISSION_DENIED: ret = HTTP_FORBIDDEN; break; case IMAP_MAILBOX_NONEXISTENT: ret = HTTP_NOT_FOUND; break; default: ret = HTTP_SERVER_ERROR; break; } goto done; } } /* Set generic Allow bits based on path components */ tgt->allow |= ALLOW_ACL | ALLOW_PROPPATCH; if (tgt->collection) { tgt->allow |= ALLOW_WRITE | ALLOW_DELETE; if (!tgt->resource) tgt->allow |= ALLOW_POST; } else if (tgt->userid) tgt->allow |= ALLOW_MKCOL; done: mbname_free(&mbname); free(freeme); return ret; } EXPORTED modseq_t dav_get_modseq(struct mailbox *mailbox __attribute__((unused)), void *data, const char *userid __attribute__((unused))) { return ((struct dav_data *) data)->modseq; } /* Evaluate If header. Note that we can't short-circuit any of the tests because we need to check for a lock-token anywhere in the header */ static int eval_list(char *list, struct mailbox *mailbox, const char *etag, const char *lock_token, unsigned *locked) { unsigned ret = 1; tok_t tok; char *cond; /* Process each condition, ANDing the results */ tok_initm(&tok, list+1, "]>", TOK_TRIMLEFT|TOK_TRIMRIGHT); while ((cond = tok_next(&tok))) { unsigned r = 0, not = 0; if (!strncmp(cond, "Not", 3)) { not = 1; cond += 3; while (*cond == ' ') cond++; } if (*cond++ == '[') { /* ETag */ r = !etagcmp(cond, etag); } else { /* State Token */ r = !strcmpnull(cond, lock_token); if (r) { /* Correct lock-token has been provided */ (*locked)--; } else if (mailbox) { struct buf buf = BUF_INITIALIZER; dav_get_synctoken(mailbox, &buf, SYNC_TOKEN_URL_SCHEME); r = !strcmp(cond, buf_cstring(&buf)); if (!r) { dav_get_synctoken(mailbox, &buf, XML_NS_MECOM "ctag/"); r = !strcmp(cond, buf_cstring(&buf)); } buf_free(&buf); } } ret &= (not ? !r : r); } tok_fini(&tok); return ret; } static int eval_if(const char *hdr, struct meth_params *params, const struct namespace_t *namespace, struct mailbox *tgt_mailbox, const char *tgt_resource, const char *tgt_etag, const char *tgt_lock_token, unsigned *locked) { unsigned ret = 0; tok_t tok; char *list; /* Process each list, ORing the results */ tok_init(&tok, hdr, ")", TOK_TRIMLEFT|TOK_TRIMRIGHT); while ((list = tok_next(&tok))) { struct mailbox *mailbox, *my_mailbox = NULL; const char *etag, *lock_token; struct buf buf = BUF_INITIALIZER; struct index_record record; struct dav_data *ddata; void *davdb = NULL; if (*list == '<') { /* Tagged-list */ const char *tag, *err; xmlURIPtr uri; tag = ++list; list = strchr(tag, '>'); *list++ = '\0'; mailbox = NULL; etag = lock_token = NULL; /* Parse the URL and assign mailbox, etag, and lock_token */ if (params && (uri = parse_uri(METH_UNKNOWN, tag, 1, &err))) { struct request_target_t tag_tgt; int r; memset(&tag_tgt, 0, sizeof(struct request_target_t)); tag_tgt.namespace = namespace; if (!params->parse_path(uri->path, &tag_tgt, &err)) { if (tag_tgt.mbentry && !tag_tgt.mbentry->server) { if (tgt_mailbox && !strcmp(mailbox_name(tgt_mailbox), tag_tgt.mbentry->name)) { /* Use target mailbox */ mailbox = tgt_mailbox; } else { /* Open new mailbox */ r = mailbox_open_irl(tag_tgt.mbentry->name, &my_mailbox); if (r) { syslog(LOG_NOTICE, "failed to open mailbox '%s'" " in tagged If header: %s", tag_tgt.mbentry->name, error_message(r)); } mailbox = my_mailbox; } if (mailbox) { if (!strcmpnull(tgt_resource, tag_tgt.resource)) { /* Tag IS target resource */ etag = tgt_etag; lock_token = tgt_lock_token; } else if (tag_tgt.resource) { /* Open DAV DB corresponding to the mailbox */ davdb = params->davdb.open_db(mailbox); /* Find message UID for the resource */ params->davdb.lookup_resource(davdb, tag_tgt.mbentry, tag_tgt.resource, (void **) &ddata, 0); if (ddata->rowid) { if (ddata->lock_expire > time(NULL)) { lock_token = ddata->lock_token; (*locked)++; } memset(&record, 0, sizeof(struct index_record)); if (ddata->imap_uid) { /* Mapped URL - Fetch index record */ r = mailbox_find_index_record(mailbox, ddata->imap_uid, &record); if (r) { syslog(LOG_NOTICE, "failed to fetch record for" " '%s':%u in tagged" " If header: %s", mailbox_name(mailbox), ddata->imap_uid, error_message(r)); } else { etag = message_guid_encode(&record.guid); } } else { /* Unmapped URL (empty resource) */ etag = NULL; } } } else { /* Collection */ buf_printf(&buf, "%u-%u-%u", mailbox->i.uidvalidity, mailbox->i.last_uid, mailbox->i.exists); etag = buf_cstring(&buf); } } } mboxlist_entry_free(&tag_tgt.mbentry); free(tag_tgt.userid); } xmlFreeURI(uri); } } else { /* No-tag-list */ mailbox = tgt_mailbox; etag = tgt_etag; lock_token = tgt_lock_token; } list = strchr(list, '('); ret |= eval_list(list, mailbox, etag, lock_token, locked); if (davdb) params->davdb.close_db(davdb); mailbox_close(&my_mailbox); buf_free(&buf); } tok_fini(&tok); return (ret || *locked); } /* Check headers for any preconditions */ EXPORTED int dav_check_precond(struct transaction_t *txn, struct meth_params *params, struct mailbox *mailbox, const void *data, const char *etag, time_t lastmod) { const struct dav_data *ddata = (const struct dav_data *) data; hdrcache_t hdrcache = txn->req_hdrs; const char **hdr; const char *lock_token = NULL; unsigned locked = 0; /* Check for a write-lock on the source */ if (ddata && ddata->lock_expire > time(NULL)) { lock_token = ddata->lock_token; switch (txn->meth) { case METH_DELETE: case METH_LOCK: case METH_MOVE: case METH_PATCH: case METH_POST: case METH_PUT: /* State-changing method: Only the lock owner can execute and MUST provide the correct lock-token in an If header */ if (strcmp(ddata->lock_ownerid, httpd_userid)) return HTTP_LOCKED; locked = 1; break; case METH_UNLOCK: /* State-changing method: Authorized in meth_unlock() */ break; case METH_ACL: case METH_MKCALENDAR: case METH_MKCOL: case METH_PROPPATCH: /* State-changing method: Locks on collections unsupported */ break; default: /* Non-state-changing method: Always allowed */ break; } } /* Per RFC 4918, If is similar to If-Match, but with lock-token submission. Per RFC 7232, LOCK errors supercede preconditions */ if ((hdr = spool_getheader(hdrcache, "If"))) { /* State tokens (sync-token, lock-token) and Etags */ if (!eval_if(hdr[0], params, txn->req_tgt.namespace, mailbox, txn->req_tgt.resource, etag, lock_token, &locked)) return HTTP_PRECOND_FAILED; } if (locked) { /* Correct lock-token was not provided in If header */ return HTTP_LOCKED; } /* Do normal HTTP checks */ return check_precond(txn, etag, lastmod); } EXPORTED int dav_premethod(struct transaction_t *txn) { if (buf_len(&server_info_link)) { /* Check for Server-Info-Token header */ const char **hdr = spool_getheader(txn->req_hdrs, "Server-Info-Token"); if ((hdr && strcmp(hdr[0], buf_cstring(&server_info_token))) || (!hdr && txn->meth == METH_OPTIONS)) { strarray_append(&txn->resp_body.links, buf_cstring(&server_info_link)); } } return 0; } EXPORTED unsigned get_preferences(struct transaction_t *txn) { unsigned mask = 0, prefs = 0; const char **hdr; /* Create a mask for preferences honored by method */ switch (txn->meth) { case METH_COPY: case METH_MOVE: case METH_PATCH: case METH_POST: case METH_PUT: mask = PREFER_REP; break; case METH_GET: case METH_MKCALENDAR: case METH_MKCOL: case METH_PROPPATCH: mask = PREFER_MIN; break; case METH_PROPFIND: case METH_REPORT: mask = (PREFER_MIN | PREFER_NOROOT); break; } if (!mask) return 0; else { txn->flags.vary |= VARY_PREFER; if (mask & PREFER_MIN) txn->flags.vary |= VARY_BRIEF; } /* Check for Prefer header(s) */ if ((hdr = spool_getheader(txn->req_hdrs, "Prefer"))) { int i; for (i = 0; hdr[i]; i++) { tok_t tok; char *token; tok_init(&tok, hdr[i], ",\r\n", TOK_TRIMLEFT|TOK_TRIMRIGHT); while ((token = tok_next(&tok))) { if ((mask & PREFER_MIN) && !strcmp(token, "return=minimal")) prefs |= PREFER_MIN; else if ((mask & PREFER_REP) && !strcmp(token, "return=representation")) prefs |= PREFER_REP; else if ((mask & PREFER_NOROOT) && !strcmp(token, "depth-noroot")) prefs |= PREFER_NOROOT; } tok_fini(&tok); } txn->resp_body.prefs = prefs; } /* Check for Brief header */ if ((mask & PREFER_MIN) && (hdr = spool_getheader(txn->req_hdrs, "Brief")) && !strcasecmp(hdr[0], "t")) { prefs |= PREFER_MIN; } /* Check for X-MobileMe-DAV-Options header */ if ((mask & PREFER_REP) && (hdr = spool_getheader(txn->req_hdrs, "X-MobileMe-DAV-Options")) && !strcasecmp(hdr[0], "return-changed-data")) { prefs |= PREFER_REP; } return prefs; } /* Check requested MIME type */ struct mime_type_t *get_accept_type(const char **hdr, struct mime_type_t *types) { struct mime_type_t *ret = NULL; struct accept *e, *enc = parse_accept(hdr); for (e = enc; e && e->token; e++) { if (!ret && e->qual > 0.0) { struct mime_type_t *m; for (m = types; !ret && m->content_type; m++) { if (is_mediatype(e->token, m->content_type) && (!e->version || !strcmpsafe(e->version, m->version))) { ret = m; } } } free_accept(e); } if (enc) free(enc); return ret; } static void add_privs(int rights, unsigned flags, xmlNodePtr parent, xmlNodePtr root, xmlNsPtr *ns); /* Ensure that we have a given namespace. If it doesn't exist in what we * parsed in the request, create it and attach to 'node'. */ int ensure_ns(xmlNsPtr *respNs, int ns, xmlNodePtr node, const char *url, const char *prefix) { if (!respNs[ns]) { xmlNsPtr nsDef; char myprefix[20]; /* Search for existing namespace using our prefix */ for (nsDef = node->nsDef; nsDef; nsDef = nsDef->next) { if ((!nsDef->prefix && !prefix) || (nsDef->prefix && prefix && !strcmp((const char *) nsDef->prefix, prefix))) break; } if (nsDef) { /* Prefix is already used - generate a new one */ snprintf(myprefix, sizeof(myprefix), "X%X", strhash(url) & 0xffff); prefix = myprefix; } respNs[ns] = xmlNewNs(node, BAD_CAST url, BAD_CAST prefix); } /* XXX check for errors */ return 0; } /* Add namespaces declared in the request to our root node and Ns array */ static int xml_add_ns(xmlNodePtr req, xmlNsPtr *respNs, xmlNodePtr root) { for (; req; req = req->next) { if (req->type == XML_ELEMENT_NODE) { xmlNsPtr nsDef; for (nsDef = req->nsDef; nsDef; nsDef = nsDef->next) { if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_DAV)) ensure_ns(respNs, NS_DAV, root, (const char *) nsDef->href, (const char *) nsDef->prefix); else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CALDAV)) ensure_ns(respNs, NS_CALDAV, root, (const char *) nsDef->href, (const char *) nsDef->prefix); else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CARDDAV)) ensure_ns(respNs, NS_CARDDAV, root, (const char *) nsDef->href, (const char *) nsDef->prefix); else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CS)) ensure_ns(respNs, NS_CS, root, (const char *) nsDef->href, (const char *) nsDef->prefix); else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_MECOM)) ensure_ns(respNs, NS_MECOM, root, (const char *) nsDef->href, (const char *) nsDef->prefix); else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_MOBME)) ensure_ns(respNs, NS_MOBME, root, (const char *) nsDef->href, (const char *) nsDef->prefix); else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CYRUS)) ensure_ns(respNs, NS_CYRUS, root, (const char *) nsDef->href, (const char *) nsDef->prefix); else xmlNewNs(root, nsDef->href, nsDef->prefix); } } xml_add_ns(req->children, respNs, root); } /* XXX check for errors */ return 0; } /* Initialize an XML tree for a property response */ xmlNodePtr init_xml_response(const char *resp, int ns, xmlNodePtr req, xmlNsPtr *respNs) { /* Start construction of our XML response tree */ xmlDocPtr doc = xmlNewDoc(BAD_CAST "1.0"); xmlNodePtr root = NULL; if (!doc) return NULL; if (!(root = xmlNewNode(NULL, BAD_CAST resp))) { xmlFreeDoc(doc); return NULL; } xmlDocSetRootElement(doc, root); /* Add namespaces from request to our response, * creating array of known namespaces that we can reference later. */ memset(respNs, 0, NUM_NAMESPACE * sizeof(xmlNsPtr)); xml_add_ns(req, respNs, root); /* Set namespace of root node */ if (ns == NS_REQ_ROOT) xmlSetNs(root, req->ns); else { ensure_ns(respNs, ns, root, known_namespaces[ns].href, known_namespaces[ns].prefix); xmlSetNs(root, respNs[ns]); } return root; } xmlNodePtr xml_add_href(xmlNodePtr parent, xmlNsPtr ns, const char *href) { xmlChar *uri = xmlURIEscapeStr(BAD_CAST href, BAD_CAST ":/?="); xmlNodePtr node = xmlNewChild(parent, ns, BAD_CAST "href", uri); free(uri); return node; } xmlNodePtr xml_add_error(xmlNodePtr root, struct error_t *err, xmlNsPtr *avail_ns) { xmlNsPtr ns[NUM_NAMESPACE]; xmlNodePtr error, node; const struct precond_t *precond = &preconds[err->precond]; unsigned err_ns = NS_DAV; const char *resp_desc = "responsedescription"; if (precond->ns == NS_ISCHED) { err_ns = NS_ISCHED; resp_desc = "response-description"; } if (!root) { error = root = init_xml_response("error", err_ns, NULL, ns); avail_ns = ns; } else error = xmlNewChild(root, NULL, BAD_CAST "error", NULL); ensure_ns(avail_ns, precond->ns, root, known_namespaces[precond->ns].href, known_namespaces[precond->ns].prefix); node = xmlNewChild(error, avail_ns[precond->ns], BAD_CAST precond->name, NULL); switch (err->precond) { case DAV_NEED_PRIVS: if (err->resource && err->rights) { unsigned flags = 0; size_t rlen = strlen(err->resource); const char *p = err->resource + rlen; node = xmlNewChild(node, NULL, BAD_CAST "resource", NULL); xml_add_href(node, NULL, err->resource); if (rlen > 6 && !strcmp(p-6, SCHED_INBOX)) flags = PRIV_INBOX; else if (rlen > 7 && !strcmp(p-7, SCHED_OUTBOX)) flags = PRIV_OUTBOX; add_privs(err->rights, flags, node, root, avail_ns); } break; default: if (err->node) xmlAddChild(node, err->node); if (err->resource) xml_add_href(node, avail_ns[NS_DAV], err->resource); break; } if (err->desc) { xmlNewTextChild(error, NULL, BAD_CAST resp_desc, BAD_CAST err->desc); } return error; } void xml_add_lockdisc(xmlNodePtr node, const char *root, struct dav_data *data) { time_t now = time(NULL); if (data->lock_expire > now) { xmlNodePtr active, node1; char tbuf[30]; /* "Second-" + long int + NUL */ active = xmlNewChild(node, NULL, BAD_CAST "activelock", NULL); node1 = xmlNewChild(active, NULL, BAD_CAST "lockscope", NULL); xmlNewChild(node1, NULL, BAD_CAST "exclusive", NULL); node1 = xmlNewChild(active, NULL, BAD_CAST "locktype", NULL); xmlNewChild(node1, NULL, BAD_CAST "write", NULL); xmlNewChild(active, NULL, BAD_CAST "depth", BAD_CAST "0"); if (data->lock_owner) { if (!strncmp(data->lock_owner, "", 10)) { node1 = xmlNewChild(active, NULL, BAD_CAST "owner", NULL); xml_add_href(node1, NULL, data->lock_owner + 10); } else { xmlNewTextChild(active, NULL, BAD_CAST "owner", BAD_CAST data->lock_owner); } } snprintf(tbuf, sizeof(tbuf), "Second-" TIME_T_FMT, data->lock_expire - now); xmlNewChild(active, NULL, BAD_CAST "timeout", BAD_CAST tbuf); node1 = xmlNewChild(active, NULL, BAD_CAST "locktoken", NULL); xml_add_href(node1, NULL, data->lock_token); node1 = xmlNewChild(active, NULL, BAD_CAST "lockroot", NULL); xml_add_href(node1, NULL, root); } } /* Add a property 'name', of namespace 'ns', with content 'content', * and status code/string 'status' to propstat element 'stat'. * 'stat' will be created as necessary. */ xmlNodePtr xml_add_prop(long status, xmlNsPtr davns, struct propstat *propstat, const xmlChar *name, xmlNsPtr ns, xmlChar *content, unsigned precond) { xmlNodePtr newprop = NULL; if (!propstat->root) { propstat->root = xmlNewNode(davns, BAD_CAST "propstat"); xmlNewChild(propstat->root, NULL, BAD_CAST "prop", NULL); } if (name) newprop = xmlNewTextChild(propstat->root->children, ns, name, content); propstat->status = status; propstat->precond = precond; return newprop; } struct allprop_rock { struct propfind_ctx *fctx; struct propstat *propstat; }; /* Add a response tree to 'root' for the specified href and either error code or property list */ int xml_add_response(struct propfind_ctx *fctx, long code, unsigned precond, const char *desc, const char *location) { xmlNodePtr resp; resp = xmlNewChild(fctx->root, fctx->ns[NS_DAV], BAD_CAST "response", NULL); if (!resp) { fctx->txn->error.desc = "Unable to add response XML element"; *fctx->ret = HTTP_SERVER_ERROR; return HTTP_SERVER_ERROR; } xml_add_href(resp, NULL, fctx->req_tgt->path); if (code) { xmlNewChild(resp, NULL, BAD_CAST "status", BAD_CAST http_statusline(VER_1_1, code)); if (precond) { xmlNodePtr error = xmlNewChild(resp, NULL, BAD_CAST "error", NULL); xmlNewChild(error, NULL, BAD_CAST preconds[precond].name, NULL); } if (desc) { xmlNewTextChild(resp, NULL, BAD_CAST "errordescription", BAD_CAST desc); } if (location) { xmlNodePtr node = xmlNewChild(resp, NULL, BAD_CAST "location", NULL); xml_add_href(node, NULL, location); } } else { struct propstat propstat[NUM_PROPSTAT], *stat; struct propfind_entry_list *e; int i; memset(propstat, 0, NUM_PROPSTAT * sizeof(struct propstat)); /* Process each property in the linked list */ for (e = fctx->elist; e; e = e->next) { int r = HTTP_NOT_FOUND; if (e->get) { r = 0; /* Pre-screen request based on prop flags */ if (fctx->req_tgt->resource) { if (!(e->flags & PROP_RESOURCE)) r = HTTP_NOT_FOUND; } else if (!(e->flags & PROP_COLLECTION)) r = HTTP_NOT_FOUND; if (!r) { if (fctx->mode == PROPFIND_NAME) { xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], e->name, e->ns, NULL, 0); } else { r = e->get(e->name, e->ns, fctx, e->prop, resp, propstat, e->rock); } } } switch (r) { case 0: case HTTP_OK: /* Nothing to do - property handled in callback */ break; case HTTP_UNAUTHORIZED: xml_add_prop(HTTP_UNAUTHORIZED, fctx->ns[NS_DAV], &propstat[PROPSTAT_UNAUTH], e->name, e->ns, NULL, 0); break; case HTTP_FORBIDDEN: xml_add_prop(HTTP_FORBIDDEN, fctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID], e->name, e->ns, NULL, 0); break; case HTTP_NOT_FOUND: if (!(fctx->prefer & PREFER_MIN)) { xml_add_prop(HTTP_NOT_FOUND, fctx->ns[NS_DAV], &propstat[PROPSTAT_NOTFOUND], e->name, e->ns, NULL, 0); } break; case HTTP_BAD_MEDIATYPE: /* CALDAV:calendar-data/timezone/availability and CARDDAV:address-data ONLY. 'e->rock' contains supported data precondition code. */ xml_add_prop(HTTP_FORBIDDEN, fctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID], e->name, e->ns, NULL, (uintptr_t) e->rock); break; default: xml_add_prop(r, fctx->ns[NS_DAV], &propstat[PROPSTAT_ERROR], e->name, e->ns, NULL, 0); break; } } /* Process dead properties for allprop/propname */ if (fctx->mailbox && !fctx->req_tgt->resource && (fctx->mode == PROPFIND_ALL || fctx->mode == PROPFIND_NAME)) { struct allprop_rock arock = { fctx, propstat }; annotatemore_findall_mailbox(fctx->mailbox, 0, "*", /*modseq*/0, allprop_cb, &arock, /*flags*/0); } /* Check if we have any propstat elements */ for (i = 0; i < NUM_PROPSTAT && !propstat[i].root; i++); if (i == NUM_PROPSTAT) { /* Add an empty propstat 200 */ xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], NULL, NULL, NULL, 0); } /* Add status and optional error to the propstat elements and then add them to response element */ for (i = 0; i < NUM_PROPSTAT; i++) { stat = &propstat[i]; if (stat->root) { xmlNewChild(stat->root, NULL, BAD_CAST "status", BAD_CAST http_statusline(VER_1_1, stat->status)); if (stat->precond) { struct error_t error = { NULL, stat->precond, NULL, NULL, 0 }; xml_add_error(stat->root, &error, fctx->ns); } xmlAddChild(resp, stat->root); } } } fctx->record = NULL; if (fctx->txn->flags.te & TE_CHUNKED) { /* Add element for this resource to output buffer. Only output the xmlBuffer every PROT_BUFSIZE bytes */ xml_partial_response((xmlBufferLength(fctx->xmlbuf) > PROT_BUFSIZE) ? fctx->txn : NULL, fctx->root->doc, resp, 1, &fctx->xmlbuf); /* Remove element from root (no need to keep in memory) */ xmlReplaceNode(resp, NULL); xmlFreeNode(resp); } return 0; } /* Helper function to prescreen/fetch resource data */ int propfind_getdata(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop, struct propstat propstat[], struct mime_type_t *mime_types, struct mime_type_t **out_type, const char *data, unsigned long datalen) { int ret = 0; if (!propstat) { /* Prescreen "property" request */ xmlChar *type, *ver = NULL; struct mime_type_t *mime; type = xmlGetProp(prop, BAD_CAST "content-type"); if (type) ver = xmlGetProp(prop, BAD_CAST "version"); /* Check/find requested MIME type */ for (mime = mime_types; type && mime->content_type; mime++) { if (is_mediatype((const char *) type, mime->content_type)) { if (ver && (!mime->version || xmlStrcmp(ver, BAD_CAST mime->version))) { continue; } break; } } if (type) xmlFree(type); if (ver) xmlFree(ver); *out_type = mime; } else { /* Add "property" */ struct mime_type_t *mime = *out_type; char *freeme = NULL; prop = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); if (mime != mime_types) { /* Not the storage format - convert into requested MIME type */ struct buf inbuf = BUF_INITIALIZER; if (!fctx->obj) { buf_init_ro(&inbuf, data, datalen); fctx->obj = mime_types->to_object(&inbuf); buf_free(&inbuf); } struct buf *outbuf = mime->from_object(fctx->obj); datalen = buf_len(outbuf); data = freeme = buf_release(outbuf); buf_destroy(outbuf); xmlSetProp(prop, BAD_CAST "content-type", BAD_CAST mime->content_type); if (mime->version) xmlSetProp(prop, BAD_CAST "version", BAD_CAST mime->version); } xmlAddChild(prop, xmlNewCDataBlock(fctx->root->doc, BAD_CAST data, datalen)); fctx->flags.fetcheddata = 1; if (freeme) free(freeme); } return ret; } /* Callback to fetch DAV:creationdate */ int propfind_creationdate(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { time_t t = 0; char datestr[RFC3339_DATETIME_MAX]; if (fctx->data) { struct dav_data *ddata = (struct dav_data *) fctx->data; t = ddata->creationdate; } else if (fctx->mailbox) { struct stat sbuf; fstat(fctx->mailbox->header_fd, &sbuf); t = sbuf.st_ctime; } if (!t) return HTTP_NOT_FOUND; time_to_rfc3339(t, datestr, RFC3339_DATETIME_MAX); xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, BAD_CAST datestr, 0); return 0; } /* Callback to write DAV:displayname for the principal */ int proppatch_principalname(xmlNodePtr prop, unsigned set, struct proppatch_ctx *pctx, struct propstat propstat[], void *rock __attribute__((unused))) { struct mailbox *mailbox = pctx->mailbox; struct mailbox *calhomeset = NULL; if (pctx->txn->req_tgt.namespace->id == URL_NS_PRINCIPAL) { /* We have been storing CUAS on cal-home-set, NOT INBOX */ char *mboxname = caldav_mboxname(pctx->txn->req_tgt.userid, NULL); int r = 0; if (!mailbox || strcmp(mboxname, mailbox_name(mailbox))) { r = mailbox_open_iwl(mboxname, &calhomeset); if (!r) pctx->mailbox = calhomeset; } free(mboxname); if (r) { xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV], &propstat[PROPSTAT_ERROR], prop->name, prop->ns, NULL, 0); *pctx->ret = HTTP_SERVER_ERROR; return 0; } } else { /* shouldn't happen! Internal server error 'r' us */ *pctx->ret = HTTP_SERVER_ERROR; return 0; } /* Make sure this is on a collection and the user has admin rights */ if (pctx->txn->req_tgt.resource || !(cyrus_acl_myrights(httpd_authstate, mailbox_acl(pctx->mailbox)) & DACL_ADMIN)) { xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, 0); *pctx->ret = HTTP_FORBIDDEN; } else { char *value = NULL; if (set) { value = (char *) xmlNodeGetContent(prop); } proppatch_todb(prop, set, pctx, propstat, (void *) value); free(value); } if (calhomeset) { mailbox_close(&calhomeset); pctx->mailbox = mailbox; } return 0; } /* Callback to fetch DAV:displayname for principals */ static int propfind_principalname(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { /* XXX Do LDAP/SQL lookup here */ buf_reset(&fctx->buf); if (fctx->req_tgt->userid) { const char *annotname = DAV_ANNOT_NS "<" XML_NS_DAV ">displayname"; char *mailboxname = caldav_mboxname(fctx->req_tgt->userid, NULL); int r = annotatemore_lookupmask(mailboxname, annotname, fctx->req_tgt->userid, &fctx->buf); free(mailboxname); if (r || !buf_len(&fctx->buf)) { buf_printf(&fctx->buf, "%s", fctx->req_tgt->userid); } } else { buf_printf(&fctx->buf, "no userid"); } xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); return 0; } /* Callback to fetch DAV:displayname for collections */ int propfind_collectionname(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop, xmlNodePtr resp, struct propstat propstat[], void *rock) { int r = propfind_fromdb(name, ns, fctx, prop, resp, propstat, rock); if (r && fctx->mbentry && !fctx->req_tgt->resource) { /* Special case empty displayname -- use last segment of path */ char *p = strrchr(fctx->mbentry->name, '.'); xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); buf_setcstr(&fctx->buf, (p ? p + 1 : fctx->mbentry->name)); xmlAddChild(node, xmlNewCDataBlock(fctx->root->doc, BAD_CAST buf_cstring(&fctx->buf), buf_len(&fctx->buf))); return 0; } return r; } /* Callback to fetch DAV:getcontentlength */ int propfind_getlength(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { buf_reset(&fctx->buf); if (fctx->record) { buf_printf(&fctx->buf, "%u", fctx->record->size - fctx->record->header_size); } xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); return 0; } /* Callback to fetch DAV:getetag */ int propfind_getetag(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { if (fctx->req_tgt->resource && !fctx->record) return HTTP_NOT_FOUND; if (!fctx->mailbox) return HTTP_NOT_FOUND; buf_reset(&fctx->buf); if (fctx->record) { const char *etag; fctx->get_validators(fctx->mailbox, fctx->data, fctx->userid, fctx->record, &etag, NULL); /* add DQUOTEs */ buf_printf(&fctx->buf, "\"%s\"", etag); } else { buf_printf(&fctx->buf, "\"%u-%u-%u\"", fctx->mailbox->i.uidvalidity, fctx->mailbox->i.last_uid, fctx->mailbox->i.exists); } xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); return 0; } /* Callback to fetch DAV:getlastmodified */ int propfind_getlastmod(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { time_t lastmod; if (!fctx->mailbox || (fctx->req_tgt->resource && !fctx->record)) return HTTP_NOT_FOUND; if (fctx->record) { fctx->get_validators(fctx->mailbox, fctx->data, fctx->userid, fctx->record, NULL, &lastmod); } else { lastmod = fctx->mailbox->index_mtime; } buf_ensure(&fctx->buf, 30); httpdate_gen(fctx->buf.s, fctx->buf.alloc, lastmod); xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, BAD_CAST fctx->buf.s, 0); return 0; } /* Callback to fetch DAV:lockdiscovery */ int propfind_lockdisc(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); if (fctx->data) { struct dav_data *ddata = (struct dav_data *) fctx->data; xml_add_lockdisc(node, fctx->req_tgt->path, ddata); } return 0; } /* Callback to fetch DAV:resourcetype */ static int propfind_restype(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); if (fctx->req_tgt->namespace->id == URL_NS_PRINCIPAL) { if (fctx->req_tgt->userid) xmlNewChild(node, NULL, BAD_CAST "principal", NULL); else xmlNewChild(node, NULL, BAD_CAST "collection", NULL); } else if (!fctx->record) { xmlNewChild(node, NULL, BAD_CAST "collection", NULL); if (fctx->req_tgt->userid) { xmlNewChild(node, NULL, BAD_CAST "notifications", NULL); ensure_ns(fctx->ns, NS_CS, resp->parent, XML_NS_CS, "CS"); xmlNewChild(node, fctx->ns[NS_CS], BAD_CAST "notification", NULL); } } return 0; } /* Callback to "write" resourcetype property */ int proppatch_restype(xmlNodePtr prop, unsigned set, struct proppatch_ctx *pctx, struct propstat propstat[], void *rock) { const char *coltype = (const char *) rock; unsigned precond = 0; if (set && (pctx->txn->meth != METH_PROPPATCH)) { /* "Writeable" for MKCOL/MKCALENDAR only */ xmlNodePtr cur; for (cur = prop->children; cur; cur = cur->next) { if (cur->type != XML_ELEMENT_NODE) continue; /* Make sure we have valid resourcetypes for the collection */ if (xmlStrcmp(cur->name, BAD_CAST "collection") && (!coltype || xmlStrcmp(cur->name, BAD_CAST coltype))) break; } if (!cur) { /* All resourcetypes are valid */ xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK], prop->name, prop->ns, NULL, 0); return 0; } /* Invalid resourcetype */ precond = DAV_VALID_RESTYPE; } else { /* Protected property */ precond = DAV_PROT_PROP; } xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, precond); *pctx->ret = HTTP_FORBIDDEN; return 0; } /* Callback to fetch DAV:supportedlock */ int propfind_suplock(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); if (fctx->mailbox && fctx->record) { xmlNodePtr entry = xmlNewChild(node, NULL, BAD_CAST "lockentry", NULL); xmlNodePtr scope = xmlNewChild(entry, NULL, BAD_CAST "lockscope", NULL); xmlNodePtr type = xmlNewChild(entry, NULL, BAD_CAST "locktype", NULL); xmlNewChild(scope, NULL, BAD_CAST "exclusive", NULL); xmlNewChild(type, NULL, BAD_CAST "write", NULL); } return 0; } /* Callback to fetch DAV:supported-report-set */ int propfind_reportset(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock) { xmlNodePtr top, node; const struct report_type_t *report; if (!propstat) { /* Prescreen "property" request */ for (report = (const struct report_type_t *) rock; report && report->name; report++) { /* Add namespaces for possible reports */ ensure_ns(fctx->ns, report->ns, fctx->root, known_namespaces[report->ns].href, known_namespaces[report->ns].prefix); } return 0; } top = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); for (report = (const struct report_type_t *) rock; report && report->name; report++) { node = xmlNewChild(top, NULL, BAD_CAST "supported-report", NULL); node = xmlNewChild(node, NULL, BAD_CAST "report", NULL); ensure_ns(fctx->ns, report->ns, resp->parent, known_namespaces[report->ns].href, known_namespaces[report->ns].prefix); xmlNewChild(node, fctx->ns[report->ns], BAD_CAST report->name, NULL); } return 0; } /* Callback to fetch DAV:supported-method-set */ int propfind_methodset(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock) { unsigned long (*allow_cb)(struct request_target_t *) = (unsigned long (*)(struct request_target_t *)) rock; unsigned long allow = allow_cb(fctx->req_tgt); xmlNodePtr top, node; top = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); if (allow & ALLOW_ACL) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "ACL"); } if (allow & ALLOW_READ) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "COPY"); } if (allow & ALLOW_DELETE) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "DELETE"); } if (allow & ALLOW_READ) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "GET"); node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "HEAD"); } if (allow & ALLOW_WRITE) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "LOCK"); } if (allow & ALLOW_MKCOL) { if (allow & ALLOW_CAL) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "MKCALENDAR"); } node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "MKCOL"); } if (allow & ALLOW_DELETE) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "MOVE"); } if (allow & ALLOW_READ) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "OPTIONS"); } if (allow & ALLOW_PATCH) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "PATCH"); } if (allow & ALLOW_POST) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "POST"); } if (allow & ALLOW_READ) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "PROPFIND"); } if (allow & ALLOW_PROPPATCH) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "PROPPATCH"); } if (allow & ALLOW_WRITE) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "PUT"); } if (allow & ALLOW_READ) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "REPORT"); } if (allow & ALLOW_TRACE) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "TRACE"); } if (allow & ALLOW_WRITE) { node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL); xmlSetProp(node, BAD_CAST "name", BAD_CAST "UNLOCK"); } return 0; } /* Callback to fetch *DAV:supported-collation-set */ int propfind_collationset(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { xmlNodePtr top; const struct collation_t *col; top = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); for (col = dav_collations; col->name; col++) { xmlNewChild(top, NULL, BAD_CAST "supported-collation", BAD_CAST col->name); } return 0; } /* Callback to fetch DAV:alternate-URI-set */ static int propfind_alturiset(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); return 0; } /* Callback to fetch DAV:principal-URL */ int propfind_principalurl(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop, xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock) { xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); const char *userid = rock ? (const char *) rock : fctx->req_tgt->userid; if (userid) { buf_reset(&fctx->buf); if (strchr(userid, '@') || !httpd_extradomain) { buf_printf(&fctx->buf, "%s/%s/%s/", namespace_principal.prefix, USER_COLLECTION_PREFIX, userid); } else { buf_printf(&fctx->buf, "%s/%s/%s@%s/", namespace_principal.prefix, USER_COLLECTION_PREFIX, userid, httpd_extradomain); } if ((fctx->mode == PROPFIND_EXPAND) && xmlFirstElementChild(prop)) { /* Return properties for this URL */ expand_property(prop, fctx, &namespace_principal, buf_cstring(&fctx->buf), &principal_parse_path, principal_props, node, 0); } else { /* Return just the URL */ xml_add_href(node, NULL, buf_cstring(&fctx->buf)); } } return 0; } /* Callback to fetch DAV:owner */ int propfind_owner(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop, xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { mbname_t *mbname; const char *owner; int r; if (!fctx->mbentry) return HTTP_NOT_FOUND; mbname = mbname_from_intname(fctx->mbentry->name); owner = mbname_userid(mbname); if (!owner) { static strarray_t *admins = NULL; if (!admins) admins = strarray_split(config_getstring(IMAPOPT_ADMINS), NULL, STRARRAY_TRIM); owner = strarray_nth(admins, 0); } r = propfind_principalurl(name, ns, fctx, prop, resp, propstat, (void *) owner); mbname_free(&mbname); return r; } /* Add possibly 'abstract' supported-privilege 'priv_name', of namespace 'ns', * with description 'desc_str' to node 'root'. For now, we assume all * descriptions are English. */ static xmlNodePtr add_suppriv(xmlNodePtr root, const char *priv_name, xmlNsPtr ns, int abstract, const char *desc_str) { xmlNodePtr supp, priv, desc; supp = xmlNewChild(root, NULL, BAD_CAST "supported-privilege", NULL); priv = xmlNewChild(supp, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns, BAD_CAST priv_name, NULL); if (abstract) xmlNewChild(supp, NULL, BAD_CAST "abstract", NULL); desc = xmlNewChild(supp, NULL, BAD_CAST "description", BAD_CAST desc_str); xmlNodeSetLang(desc, BAD_CAST "en"); return supp; } /* Callback to fetch DAV:supported-privilege-set */ int propfind_supprivset(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp, struct propstat propstat[], void *rock __attribute__((unused))) { xmlNodePtr set, all, agg, write; unsigned tgt_flags = 0; if (!propstat) { /* Prescreen "property" request */ if (fctx->req_tgt->collection || (fctx->req_tgt->userid && fctx->depth >= 1) || fctx->depth >= 2) { /* Add namespaces for possible privileges */ ensure_ns(fctx->ns, NS_CYRUS, fctx->root, XML_NS_CYRUS, "CY"); if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) { ensure_ns(fctx->ns, NS_CALDAV, fctx->root, XML_NS_CALDAV, "C"); } } return 0; } set = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); all = add_suppriv(set, "all", NULL, 0, "Any operation"); agg = add_suppriv(all, "read", NULL, 0, "Read any object"); add_suppriv(agg, "read-current-user-privilege-set", NULL, 1, "Read current user privilege set"); if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) { if (fctx->req_tgt->collection) { ensure_ns(fctx->ns, NS_CALDAV, resp->parent, XML_NS_CALDAV, "C"); if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX)) tgt_flags = TGT_SCHED_INBOX; else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX)) tgt_flags = TGT_SCHED_OUTBOX; else { add_suppriv(agg, "read-free-busy", fctx->ns[NS_CALDAV], 0, "Read free/busy time"); } } } write = add_suppriv(all, "write", NULL, 0, "Write any object"); add_suppriv(write, "write-content", NULL, 0, "Write resource content"); agg = add_suppriv(write, "write-properties", NULL, 0, "Write properties"); ensure_ns(fctx->ns, NS_CYRUS, resp->parent, XML_NS_CYRUS, "CY"); add_suppriv(agg, "write-properties-collection", fctx->ns[NS_CYRUS], 0, "Write properties on a collection"); add_suppriv(agg, "write-properties-resource", fctx->ns[NS_CYRUS], 0, "Write properties on a resource"); agg = add_suppriv(write, "bind", NULL, 0, "Add new member to collection"); add_suppriv(agg, "make-collection", fctx->ns[NS_CYRUS], 0, "Make new collection"); add_suppriv(agg, "add-resource", fctx->ns[NS_CYRUS], 0, "Add new resource"); agg = add_suppriv(write, "unbind", NULL, 0, "Remove member from collection"); add_suppriv(agg, "remove-collection", fctx->ns[NS_CYRUS], 0, "Remove collection"); add_suppriv(agg, "remove-resource", fctx->ns[NS_CYRUS], 0, "Remove resource"); agg = add_suppriv(all, "admin", fctx->ns[NS_CYRUS], 0, "Perform administrative operations"); add_suppriv(agg, "read-acl", NULL, 1, "Read ACL"); add_suppriv(agg, "write-acl", NULL, 1, "Write ACL"); add_suppriv(agg, "unlock", NULL, 1, "Unlock resource"); add_suppriv(agg, "share", NULL, 1, "Share resource"); if (tgt_flags == TGT_SCHED_INBOX) { agg = add_suppriv(all, "schedule-deliver", fctx->ns[NS_CALDAV], 0, "Deliver scheduling messages"); add_suppriv(agg, "schedule-deliver-invite", fctx->ns[NS_CALDAV], 0, "Deliver scheduling messages from Organizers"); add_suppriv(agg, "schedule-deliver-reply", fctx->ns[NS_CALDAV], 0, "Deliver scheduling messages from Attendees"); add_suppriv(agg, "schedule-query-freebusy", fctx->ns[NS_CALDAV], 0, "Accept free/busy requests"); } else if (tgt_flags == TGT_SCHED_OUTBOX) { agg = add_suppriv(all, "schedule-send", fctx->ns[NS_CALDAV], 0, "Send scheduling messages"); add_suppriv(agg, "schedule-send-invite", fctx->ns[NS_CALDAV], 0, "Send scheduling messages by Organizers"); add_suppriv(agg, "schedule-send-reply", fctx->ns[NS_CALDAV], 0, "Send scheduling messages by Attendees"); add_suppriv(agg, "schedule-send-freebusy", fctx->ns[NS_CALDAV], 0, "Submit free/busy requests"); } return 0; } static void add_privs(int rights, unsigned flags, xmlNodePtr parent, xmlNodePtr root, xmlNsPtr *ns) { xmlNodePtr priv; int do_contained; /* DAV:all */ if ((rights & DACL_ALL) == DACL_ALL && /* DAV:all on CALDAV:schedule-in/outbox MUST include CALDAV:schedule */ (!(flags & (PRIV_INBOX|PRIV_OUTBOX)) || (rights & DACL_SCHED) == DACL_SCHED)) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, NULL, BAD_CAST "all", NULL); if (!(flags & PRIV_CONTAINED)) return; } /* DAV:read */ if ((rights & DACL_READ) == DACL_READ) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, NULL, BAD_CAST "read", NULL); if (flags & PRIV_IMPLICIT) rights |= DACL_READFB; do_contained = (flags & PRIV_CONTAINED); } else do_contained = 1; if (do_contained) { if ((rights & DACL_READFB) && /* CALDAV:read-free-busy doesn't apply to CALDAV:sched-in/outbox */ !(flags & (PRIV_INBOX|PRIV_OUTBOX))) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); ensure_ns(ns, NS_CALDAV, root, XML_NS_CALDAV, "C"); xmlNewChild(priv, ns[NS_CALDAV], BAD_CAST "read-free-busy", NULL); } } /* DAV:write */ if ((rights & DACL_WRITE) == DACL_WRITE) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, NULL, BAD_CAST "write", NULL); do_contained = (flags & PRIV_CONTAINED); } else do_contained = 1; if (do_contained) { if (rights & DACL_WRITECONT) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, NULL, BAD_CAST "write-content", NULL); } if (rights & (DACL_WRITEPROPS|DACL_BIND|DACL_UNBIND)) { ensure_ns(ns, NS_CYRUS, root, XML_NS_CYRUS, "CY"); /* DAV:write-properties */ if ((rights & DACL_WRITEPROPS) == DACL_WRITEPROPS) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, NULL, BAD_CAST "write-properties", NULL); do_contained = (flags & PRIV_CONTAINED); } else do_contained = 1; if (do_contained) { if (rights & DACL_PROPCOL) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "write-properties-collection", NULL); } if (rights & DACL_PROPRSRC) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "write-properties-resource", NULL); } } /* DAV:bind */ if ((rights & DACL_BIND) == DACL_BIND) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, NULL, BAD_CAST "bind", NULL); do_contained = (flags & PRIV_CONTAINED); } else do_contained = 1; if (do_contained) { if (rights & DACL_MKCOL) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "make-collection", NULL); } if (rights & DACL_ADDRSRC) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "add-resource", NULL); } } /* DAV:unbind */ if ((rights & DACL_UNBIND) == DACL_UNBIND) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, NULL, BAD_CAST "unbind", NULL); do_contained = (flags & PRIV_CONTAINED); } else do_contained = 1; if (do_contained) { if (rights & DACL_RMCOL) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "remove-collection", NULL); } if ((rights & DACL_RMRSRC) == DACL_RMRSRC) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "remove-resource", NULL); } } } } /* CYRUS:admin */ if (rights & DACL_ADMIN) { ensure_ns(ns, NS_CYRUS, root, XML_NS_CYRUS, "CY"); priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "admin", NULL); } if ((rights & DACL_SCHED) && (flags & (PRIV_INBOX|PRIV_OUTBOX))) { struct buf buf = BUF_INITIALIZER; size_t len; if (flags & PRIV_INBOX) buf_setcstr(&buf, "schedule-deliver"); else buf_setcstr(&buf, "schedule-send"); len = buf_len(&buf); ensure_ns(ns, NS_CALDAV, root, XML_NS_CALDAV, "C"); /* CALDAV:schedule */ if ((rights & DACL_SCHED) == DACL_SCHED) { priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CALDAV], BAD_CAST buf_cstring(&buf), NULL); do_contained = (flags & PRIV_CONTAINED); } else do_contained = 1; if (do_contained) { if (rights & DACL_INVITE) { buf_truncate(&buf, len); buf_appendcstr(&buf, "-invite"); priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CALDAV], BAD_CAST buf_cstring(&buf), NULL); } if (rights & DACL_REPLY) { buf_truncate(&buf, len); buf_appendcstr(&buf, "-reply"); priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CALDAV], BAD_CAST buf_cstring(&buf), NULL); } if (rights & DACL_SCHEDFB) { if (flags & PRIV_INBOX) buf_setcstr(&buf, "schedule-query"); else buf_truncate(&buf, len); buf_appendcstr(&buf, "-freebusy"); priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); xmlNewChild(priv, ns[NS_CALDAV], BAD_CAST buf_cstring(&buf), NULL); } } buf_free(&buf); } } /* Callback to fetch DAV:current-user-privilege-set */ int propfind_curprivset(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp, struct propstat propstat[], void *rock __attribute__((unused))) { int rights; unsigned flags = 0; xmlNodePtr set; if (!propstat) { /* Prescreen "property" request */ /* Add namespaces for possible privileges */ ensure_ns(fctx->ns, NS_CYRUS, fctx->root, XML_NS_CYRUS, "CY"); if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) { ensure_ns(fctx->ns, NS_CALDAV, fctx->root, XML_NS_CALDAV, "C"); } return 0; } if (!fctx->mailbox) return HTTP_NOT_FOUND; rights = httpd_myrights(fctx->authstate, fctx->mbentry); if ((rights & DACL_READ) != DACL_READ) { return HTTP_UNAUTHORIZED; } /* Add in implicit rights */ if (fctx->userisadmin) { rights |= DACL_ADMIN; } else if (mboxname_userownsmailbox(httpd_userid, mailbox_name(fctx->mailbox))) { rights |= config_implicitrights; /* we always allow admin by the owner in DAV */ rights |= DACL_ADMIN; } /* Build the rest of the XML response */ set = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); if (!fctx->req_tgt->resource) { if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) { flags = PRIV_IMPLICIT; if (fctx->req_tgt->collection) { if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX)) flags = PRIV_INBOX; else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX)) flags = PRIV_OUTBOX; } } flags += PRIV_CONTAINED; add_privs(rights, flags, set, resp->parent, fctx->ns); } return 0; } /* Callback to fetch DAV:acl */ int propfind_acl(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp, struct propstat propstat[], void *rock __attribute__((unused))) { xmlNodePtr acl; char *aclstr, *userid; unsigned flags = 0; if (!propstat) { /* Prescreen "property" request */ if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR && (fctx->req_tgt->collection || (fctx->req_tgt->userid && fctx->depth >= 1) || fctx->depth >= 2)) { /* Add namespaces for possible privileges */ ensure_ns(fctx->ns, NS_CALDAV, fctx->root, XML_NS_CALDAV, "C"); } return 0; } if (!fctx->mailbox) return HTTP_NOT_FOUND; /* owner has implicit admin rights */ if (!mboxname_userownsmailbox(httpd_userid, mailbox_name(fctx->mailbox))) { int rights = httpd_myrights(fctx->authstate, fctx->mbentry); if (!(rights & DACL_ADMIN)) return HTTP_UNAUTHORIZED; } if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) { flags = PRIV_IMPLICIT; if (fctx->req_tgt->collection) { if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX)) flags = PRIV_INBOX; else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX)) flags = PRIV_OUTBOX; } } /* Start the acl XML response */ acl = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); /* Parse the ACL string (userid/rights pairs) */ userid = aclstr = xstrdupnull(mailbox_acl(fctx->mailbox)); while (userid) { int rights; char *rightstr, *nextid; xmlNodePtr ace, node; int deny = 0; rightstr = strchr(userid, '\t'); if (!rightstr) break; *rightstr++ = '\0'; nextid = strchr(rightstr, '\t'); if (!nextid) break; *nextid++ = '\0'; /* Check for negative rights */ if (*userid == '-') { deny = 1; userid++; } cyrus_acl_strtomask(rightstr, &rights); /* XXX and if strtomask fails? */ /* Add ace XML element for this userid/right pair */ ace = xmlNewChild(acl, NULL, BAD_CAST "ace", NULL); node = xmlNewChild(ace, NULL, BAD_CAST "principal", NULL); if (!strcmp(userid, fctx->userid)) xmlNewChild(node, NULL, BAD_CAST "self", NULL); else if (mboxname_userownsmailbox(userid, mailbox_name(fctx->mailbox))) { xmlNewChild(node, NULL, BAD_CAST "owner", NULL); /* we always allow admin by the owner in DAV */ rights |= DACL_ADMIN; } else if (!strcmp(userid, "anyone")) xmlNewChild(node, NULL, BAD_CAST "all", NULL); else if (!strcmp(userid, "anonymous")) xmlNewChild(node, NULL, BAD_CAST "unauthenticated", NULL); else if (!strncmp(userid, "group:", 6)) { buf_reset(&fctx->buf); buf_printf(&fctx->buf, "%s/%s/%s/", namespace_principal.prefix, GROUP_COLLECTION_PREFIX, userid+6); xml_add_href(node, NULL, buf_cstring(&fctx->buf)); } else { buf_reset(&fctx->buf); buf_printf(&fctx->buf, "%s/%s/%s/", namespace_principal.prefix, USER_COLLECTION_PREFIX, userid); xml_add_href(node, NULL, buf_cstring(&fctx->buf)); } node = xmlNewChild(ace, NULL, BAD_CAST (deny ? "deny" : "grant"), NULL); add_privs(rights, flags, node, resp->parent, fctx->ns); if (fctx->req_tgt->resource) { node = xmlNewChild(ace, NULL, BAD_CAST "inherited", NULL); buf_reset(&fctx->buf); buf_printf(&fctx->buf, "%.*s", (int)(fctx->req_tgt->resource - fctx->req_tgt->path), fctx->req_tgt->path); xml_add_href(node, NULL, buf_cstring(&fctx->buf)); } userid = nextid; } if (aclstr) free(aclstr); return 0; } /* Callback to fetch DAV:acl-restrictions */ int propfind_aclrestrict(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); xmlNewChild(node, NULL, BAD_CAST "no-invert", NULL); return 0; } /* Callback to fetch DAV:principal-collection-set */ EXPORTED int propfind_princolset(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); buf_reset(&fctx->buf); buf_printf(&fctx->buf, "%s/%s/", namespace_principal.prefix, USER_COLLECTION_PREFIX); xml_add_href(node, NULL, buf_cstring(&fctx->buf)); return 0; } /* Callback to fetch DAV:quota-available-bytes and DAV:quota-used-bytes */ EXPORTED int propfind_quota(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { static char prevroot[MAX_MAILBOX_BUFFER]; char foundroot[MAX_MAILBOX_BUFFER]; const char *qr = NULL; if (fctx->mailbox) { /* Use the quotaroot as specified in mailbox header */ qr = mailbox_quotaroot(fctx->mailbox); } else if (fctx->req_tgt->mbentry) { /* Find the quotaroot governing this hierarchy */ if (quota_findroot(foundroot, sizeof(foundroot), fctx->req_tgt->mbentry->name)) { qr = foundroot; } } if (!qr) return HTTP_NOT_FOUND; if (!fctx->quota.root || strcmp(fctx->quota.root, qr)) { /* Different quotaroot - read it */ syslog(LOG_DEBUG, "reading quota for '%s'", qr); fctx->quota.root = strcpy(prevroot, qr); quota_read_withconversations(&fctx->quota); } buf_reset(&fctx->buf); if (!xmlStrcmp(name, BAD_CAST "quota-available-bytes")) { /* Calculate limit in bytes and subtract usage */ quota_t limit = fctx->quota.limits[QUOTA_STORAGE] * quota_units[QUOTA_STORAGE]; buf_printf(&fctx->buf, QUOTA_T_FMT, limit - fctx->quota.useds[QUOTA_STORAGE]); } else if (fctx->record) { /* Bytes used by resource */ buf_printf(&fctx->buf, "%u", fctx->record->size); } else if (fctx->mailbox) { /* Bytes used by calendar collection */ buf_printf(&fctx->buf, QUOTA_T_FMT, fctx->mailbox->i.quota_mailbox_used); } else { /* Bytes used by entire hierarchy */ buf_printf(&fctx->buf, QUOTA_T_FMT, fctx->quota.useds[QUOTA_STORAGE]); } xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); return 0; } /* Callback to fetch DAV:current-user-principal */ EXPORTED int propfind_curprin(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop, xmlNodePtr resp, struct propstat propstat[], void *rock __attribute__((unused))) { if (httpd_userid) { propfind_principalurl(name, ns, fctx, prop, resp, propstat, httpd_userid); } else { xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); xmlNewChild(node, NULL, BAD_CAST "unauthenticated", NULL); } return 0; } /* Callback to fetch DAV:add-member */ int propfind_addmember(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { xmlNodePtr node; int len; if (!fctx->req_tgt->collection || !strcmp(fctx->req_tgt->collection, SCHED_INBOX) || !strcmp(fctx->req_tgt->collection, SCHED_OUTBOX) || (fctx->req_tgt->namespace->id == URL_NS_ADDRESSBOOK && !config_getswitch(IMAPOPT_CARDDAV_ALLOWADDMEMBER))) { /* Only allowed on non-scheduling collections */ return HTTP_NOT_FOUND; } node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); len = fctx->req_tgt->resource ? (size_t) (fctx->req_tgt->resource - fctx->req_tgt->path) : strlen(fctx->req_tgt->path); buf_reset(&fctx->buf); buf_printf(&fctx->buf, "%.*s?action=add-member", len, fctx->req_tgt->path); xml_add_href(node, NULL, buf_cstring(&fctx->buf)); return 0; } void dav_get_synctoken(struct mailbox *mailbox, struct buf *buf, const char *prefix) { buf_reset(buf); buf_printf(buf, "%s%u-" MODSEQ_FMT, prefix, mailbox->i.uidvalidity, mailbox->i.highestmodseq); } /* Callback to fetch DAV:sync-token and CS:getctag */ int propfind_sync_token(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock) { const char *prefix = (const char *) rock; if (!fctx->req_tgt->collection || /* until we support sync on cal-home */ !fctx->mailbox || fctx->record) return HTTP_NOT_FOUND; /* not defined on the top-level collection either (aka #calendars) */ if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND; dav_get_synctoken(fctx->mailbox, &fctx->buf, prefix); xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); return 0; } /* Callback to fetch MC:bulk-requests */ int propfind_bulkrequests(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); if (fctx->req_tgt->collection && !fctx->req_tgt->flags && !fctx->req_tgt->resource) { xmlNodePtr type = xmlNewChild(node, NULL, BAD_CAST "simple", NULL); xmlNewChild(type, NULL, BAD_CAST "max-resources", NULL); xmlNewChild(type, NULL, BAD_CAST "max-bytes", NULL); #if 0 type = xmlNewChild(node, NULL, BAD_CAST "crud", NULL); xmlNewChild(type, NULL, BAD_CAST "max-resources", NULL); xmlNewChild(type, NULL, BAD_CAST "max-bytes", NULL); #endif } return 0; } /* Callback to fetch properties from resource header */ int propfind_fromhdr(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock) { const char *hdrname = (const char *) rock; int r = HTTP_NOT_FOUND; if (fctx->record && (mailbox_cached_header(hdrname) != BIT32_MAX) && !mailbox_cacherecord(fctx->mailbox, fctx->record)) { unsigned size; struct protstream *stream; hdrcache_t hdrs = NULL; const char **hdr; size = cacheitem_size(fctx->record, CACHE_HEADERS); stream = prot_readmap(cacheitem_base(fctx->record, CACHE_HEADERS), size); hdrs = spool_new_hdrcache(); spool_fill_hdrcache(stream, NULL, hdrs, NULL); prot_free(stream); if ((hdr = spool_getheader(hdrs, (const char *) hdrname))) { xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, BAD_CAST hdr[0], 0); r = 0; } spool_free_hdrcache(hdrs); } return r; } static struct flaggedresources { const char *name; int flag; } fres[] = { { "answered", FLAG_ANSWERED }, { "flagged", FLAG_FLAGGED }, { "seen", FLAG_SEEN }, { NULL, 0 } /* last is always NULL */ }; /* Callback to write a property to annotation DB */ static int proppatch_toresource(xmlNodePtr prop, unsigned set, struct proppatch_ctx *pctx, struct propstat propstat[], void *rock __attribute__((unused))) { xmlChar *freeme = NULL; annotate_state_t *astate = NULL; struct buf value = BUF_INITIALIZER; int r = 1; /* default to error */ /* flags only store "exists" */ if (!strcmp((const char *)prop->ns->href, XML_NS_SYSFLAG)) { struct flaggedresources *frp; int isset; for (frp = fres; frp->name; frp++) { if (strcasecmp((const char *)prop->name, frp->name)) continue; r = 0; /* ok to do nothing */ isset = pctx->record->system_flags & frp->flag; if (set) { if (isset) goto done; pctx->record->system_flags |= frp->flag; } else { if (!isset) goto done; pctx->record->system_flags &= ~frp->flag; } r = mailbox_rewrite_index_record(pctx->mailbox, pctx->record); goto done; } goto done; } if (!strcmp((const char *)prop->ns->href, XML_NS_USERFLAG)) { int userflag = 0; int isset; r = mailbox_user_flag(pctx->mailbox, (const char *)prop->name, &userflag, 1); if (r) goto done; isset = pctx->record->user_flags[userflag/32] & (1<record->user_flags[userflag/32] |= (1<record->user_flags[userflag/32] &= ~(1<mailbox, pctx->record); goto done; } /* otherwise it's a database annotation */ buf_reset(&pctx->buf); buf_printf(&pctx->buf, DAV_ANNOT_NS "<%s>%s", (const char *) prop->ns->href, prop->name); if (set) { freeme = xmlNodeGetContent(prop); buf_init_ro_cstr(&value, (const char *)freeme); } r = mailbox_get_annotate_state(pctx->mailbox, pctx->record->uid, &astate); if (!r) r = annotate_state_writemask(astate, buf_cstring(&pctx->buf), httpd_userid, &value); /* we need to rewrite the record to update the modseq because the layering * of annotations and mailboxes is broken */ if (!r) r = mailbox_rewrite_index_record(pctx->mailbox, pctx->record); done: if (!r) { xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK], prop->name, prop->ns, NULL, 0); } else { xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV], &propstat[PROPSTAT_ERROR], prop->name, prop->ns, NULL, 0); } buf_free(&value); if (freeme) xmlFree(freeme); return 0; } /* Callback to read a property from annotation DB */ static int propfind_fromresource(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop __attribute__((unused)), xmlNodePtr resp __attribute__((unused)), struct propstat propstat[], void *rock __attribute__((unused))) { struct buf attrib = BUF_INITIALIZER; xmlNodePtr node; int r = 0; /* default no error */ if (!strcmp((const char *)ns->href, XML_NS_SYSFLAG)) { struct flaggedresources *frp; int isset; for (frp = fres; frp->name; frp++) { if (strcasecmp((const char *)name, frp->name)) continue; isset = fctx->record->system_flags & frp->flag; if (isset) buf_setcstr(&attrib, "1"); goto done; } goto done; } if (!strcmp((const char *)ns->href, XML_NS_USERFLAG)) { int userflag = 0; int isset; r = mailbox_user_flag(fctx->mailbox, (const char *)name, &userflag, 0); if (r) goto done; isset = fctx->record->user_flags[userflag/32] & (1<buf); buf_printf(&fctx->buf, DAV_ANNOT_NS "<%s>%s", (const char *) ns->href, name); r = annotatemore_msg_lookup(fctx->mailbox, fctx->record->uid, buf_cstring(&fctx->buf), NULL, &attrib); done: if (r) r = HTTP_SERVER_ERROR; else if (!buf_len(&attrib)) r = HTTP_NOT_FOUND; if (!r) { node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); xmlAddChild(node, xmlNewCDataBlock(fctx->root->doc, BAD_CAST buf_cstring(&attrib), buf_len(&attrib))); } buf_free(&attrib); return r; } /* Callback to read a property from annotation DB */ int propfind_fromdb(const xmlChar *name, xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop, xmlNodePtr resp, struct propstat propstat[], void *rock) { struct buf attrib = BUF_INITIALIZER; xmlNodePtr node; int r = 0; if (fctx->req_tgt->resource) { if (!fctx->record) return HTTP_NOT_FOUND; return propfind_fromresource(name, ns, fctx, prop, resp, propstat, rock); } buf_reset(&fctx->buf); buf_printf(&fctx->buf, DAV_ANNOT_NS "<%s>%s", (const char *) ns->href, name); if (fctx->mbentry && !fctx->record) { r = annotatemore_lookupmask_mbe(fctx->mbentry, buf_cstring(&fctx->buf), httpd_userid, &attrib); } if (r) return HTTP_SERVER_ERROR; if (!buf_len(&attrib)) return HTTP_NOT_FOUND; node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], name, ns, NULL, 0); xmlAddChild(node, xmlNewCDataBlock(fctx->root->doc, BAD_CAST buf_cstring(&attrib), buf_len(&attrib))); buf_free(&attrib); return 0; } /* Callback to write a property to annotation DB */ int proppatch_todb(xmlNodePtr prop, unsigned set, struct proppatch_ctx *pctx, struct propstat propstat[], void *rock) { xmlChar *freeme = NULL; annotate_state_t *astate = NULL; struct buf value = BUF_INITIALIZER; int r; if (pctx->txn->req_tgt.resource) return proppatch_toresource(prop, set, pctx, propstat, NULL); buf_reset(&pctx->buf); buf_printf(&pctx->buf, DAV_ANNOT_NS "<%s>%s", (const char *) prop->ns->href, prop->name); if (set) { if (rock) { buf_init_ro_cstr(&value, (const char *)rock); } else { freeme = xmlNodeGetContent(prop); buf_init_ro_cstr(&value, (const char *)freeme); } } r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate); if (!r) r = annotate_state_writemask(astate, buf_cstring(&pctx->buf), httpd_userid, &value); if (!r) { xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK], prop->name, prop->ns, NULL, 0); } else { xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV], &propstat[PROPSTAT_ERROR], prop->name, prop->ns, NULL, 0); } buf_free(&value); if (freeme) xmlFree(freeme); return 0; } /* annotatemore_findall callback for adding dead properties (allprop/propname) */ static int allprop_cb(const char *mailbox __attribute__((unused)), uint32_t uid __attribute__((unused)), const char *entry, const char *userid, const struct buf *attrib, const struct annotate_metadata *mdata __attribute__((unused)), void *rock) { struct allprop_rock *arock = (struct allprop_rock *) rock; const struct prop_entry *pentry; char *href, *name; xmlNsPtr ns; xmlNodePtr node; /* Make sure it is a shared entry or the user's private one */ if (userid && *userid && strcmp(userid, arock->fctx->userid)) return 0; /* Split entry into namespace href and name ( name ) */ buf_setcstr(&arock->fctx->buf, entry + strlen(DAV_ANNOT_NS) + 1); href = (char *) buf_cstring(&arock->fctx->buf); if ((name = strchr(href, '>'))) *name++ = '\0'; else if ((name = strchr(href, ':'))) *name++ = '\0'; /* Look for a match against live properties */ for (pentry = arock->fctx->lprops; pentry->name && (strcmp(name, pentry->name) || strcmp(href, known_namespaces[pentry->ns].href)); pentry++); if (pentry->name && (arock->fctx->mode == PROPFIND_ALL /* Skip all live properties */ || (pentry->flags & PROP_ALLPROP))) /* Skip those already included */ return 0; /* Look for an instance of this namespace in our response */ ns = hash_lookup(href, arock->fctx->ns_table); /* XXX - can return the same property multiple times with annotate masks! */ /* Add the dead property to the response */ node = xml_add_prop(HTTP_OK, arock->fctx->ns[NS_DAV], &arock->propstat[PROPSTAT_OK], BAD_CAST name, ns, NULL, 0); if (!ns) { /* Add the namespace directly to the property - its too late to add it to the root when chunking the responses */ char prefix[9]; snprintf(prefix, sizeof(prefix), "X%X", strhash(href)); xmlSetNs(node, xmlNewNs(node, BAD_CAST href, BAD_CAST prefix)); } if (arock->fctx->mode == PROPFIND_ALL) { xmlAddChild(node, xmlNewCDataBlock(arock->fctx->root->doc, BAD_CAST attrib->s, attrib->len)); } return 0; } static int prescreen_prop(const struct prop_entry *entry, xmlNodePtr prop, struct propfind_ctx *fctx) { unsigned allowed = 1; if (fctx->req_tgt->resource && !(entry->flags & PROP_RESOURCE)) allowed = 0; else if (entry->flags & PROP_PRESCREEN) { allowed = !entry->get(BAD_CAST entry->name, NULL, fctx, prop, NULL, NULL, entry->rock); } return allowed; } /* Parse the requested properties and create a linked list of fetch callbacks. * The list gets reused for each href if Depth > 0 */ HIDDEN int preload_proplist(xmlNodePtr proplist, struct propfind_ctx *fctx) { int ret = 0; xmlNodePtr prop; const struct prop_entry *entry; struct propfind_entry_list *tail = NULL; switch (fctx->mode) { case PROPFIND_ALL: case PROPFIND_NAME: /* Add live properties for allprop/propname */ for (entry = fctx->lprops; entry->name; entry++) { if (entry->flags & PROP_ALLPROP) { /* Pre-screen request based on prop flags */ int allowed = prescreen_prop(entry, NULL, fctx); if (allowed || fctx->mode == PROPFIND_ALL) { struct propfind_entry_list *nentry = xzmalloc(sizeof(struct propfind_entry_list)); ensure_ns(fctx->ns, entry->ns, fctx->root, known_namespaces[entry->ns].href, known_namespaces[entry->ns].prefix); nentry->name = xmlStrdup(BAD_CAST entry->name); nentry->ns = fctx->ns[entry->ns]; if (allowed) { nentry->flags = entry->flags; nentry->get = entry->get; nentry->prop = NULL; nentry->rock = entry->rock; } /* Append new entry to linked list */ if (tail) { tail->next = nentry; tail = nentry; } else tail = fctx->elist = nentry; } } } /* Fall through and build hash table of namespaces */ GCC_FALLTHROUGH case PROPFIND_EXPAND: /* Add all namespaces attached to the response to our hash table */ if (!fctx->ns_table->size) { xmlNsPtr nsDef; construct_hash_table(fctx->ns_table, 10, 1); for (nsDef = fctx->root->nsDef; nsDef; nsDef = nsDef->next) { hash_insert((const char *) nsDef->href, nsDef, fctx->ns_table); } } } /* Iterate through requested properties */ for (prop = proplist; !*fctx->ret && prop; prop = prop->next) { if (prop->type == XML_ELEMENT_NODE) { struct propfind_entry_list *nentry; xmlChar *name, *namespace = NULL; xmlNsPtr ns; const char *ns_href; unsigned i; if (!prop->ns) return HTTP_BAD_REQUEST; ns = prop->ns; ns_href = (const char *) ns->href; if (fctx->mode == PROPFIND_EXPAND) { /* Get name/namespace from */ name = xmlGetProp(prop, BAD_CAST "name"); namespace = xmlGetProp(prop, BAD_CAST "namespace"); if (namespace) { ns_href = (const char *) namespace; ns = NULL; } } else { /* node IS the property */ name = xmlStrdup(prop->name); } /* Look for this namespace in our known array */ for (i = 0; i < NUM_NAMESPACE; i++) { if (!strcmp(ns_href, known_namespaces[i].href)) { ensure_ns(fctx->ns, i, fctx->root, known_namespaces[i].href, known_namespaces[i].prefix); ns = fctx->ns[i]; break; } } if (namespace) { if (!ns) { /* Look for namespace in hash table */ ns = hash_lookup(ns_href, fctx->ns_table); if (!ns) { char prefix[6]; snprintf(prefix, sizeof(prefix), "X%X", strhash(ns_href) & 0xffff); ns = xmlNewNs(fctx->root, BAD_CAST ns_href, BAD_CAST prefix); hash_insert(ns_href, ns, fctx->ns_table); } } xmlFree(namespace); } /* Look for a match against our known properties */ for (entry = fctx->lprops; entry->name && (strcmp((const char *) name, entry->name) || strcmp((const char *) ns->href, known_namespaces[entry->ns].href)); entry++); /* Skip properties already included by allprop */ if (fctx->mode == PROPFIND_ALL && (entry->flags & PROP_ALLPROP)) { xmlFree(name); continue; } nentry = xzmalloc(sizeof(struct propfind_entry_list)); nentry->name = name; nentry->ns = ns; if (entry->name) { /* Found a match - Pre-screen request based on prop flags */ if (prescreen_prop(entry, prop, fctx)) { nentry->flags = entry->flags; nentry->get = entry->get; nentry->prop = prop; nentry->rock = entry->rock; } ret = *fctx->ret; } else { /* No match, treat as a dead property. Need to look at both collections and resources */ nentry->flags = PROP_COLLECTION | PROP_RESOURCE; nentry->get = propfind_fromdb; nentry->prop = NULL; nentry->rock = NULL; } /* Append new entry to linked list */ if (tail) { tail->next = nentry; tail = nentry; } else tail = fctx->elist = nentry; } } return ret; } /* Execute the given property patch instructions */ static int do_proppatch(struct proppatch_ctx *pctx, xmlNodePtr instr) { struct propstat propstat[NUM_PROPSTAT]; int i; memset(propstat, 0, NUM_PROPSTAT * sizeof(struct propstat)); /* Iterate through propertyupdate children */ for (; instr; instr = instr->next) { if (instr->type == XML_ELEMENT_NODE) { xmlNodePtr prop; unsigned set = 0; if (!xmlStrcmp(instr->name, BAD_CAST "set")) set = 1; else if ((pctx->txn->meth == METH_PROPPATCH) && !xmlStrcmp(instr->name, BAD_CAST "remove")) set = 0; else { syslog(LOG_INFO, "Unknown PROPPATCH instruction"); pctx->txn->error.desc = "Unknown PROPPATCH instruction"; return HTTP_BAD_REQUEST; } /* Find child element */ for (prop = instr->children; prop && prop->type != XML_ELEMENT_NODE; prop = prop->next); if (!prop || xmlStrcmp(prop->name, BAD_CAST "prop")) { pctx->txn->error.desc = "Missing prop element"; return HTTP_BAD_REQUEST; } /* Iterate through requested properties */ for (prop = prop->children; prop; prop = prop->next) { if (prop->type == XML_ELEMENT_NODE) { const struct prop_entry *entry; /* Look for a match against our known properties */ for (entry = pctx->lprops; entry->name && (strcmp((const char *) prop->name, entry->name) || !prop->ns || strcmp((const char *) prop->ns->href, known_namespaces[entry->ns].href)); entry++); if (entry->name) { int rights = httpd_myrights(httpd_authstate, pctx->txn->req_tgt.mbentry); if (!entry->put) { /* Protected property */ xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, DAV_PROT_PROP); *pctx->ret = HTTP_FORBIDDEN; } else if ((pctx->txn->meth == METH_PROPPATCH) && !(rights & ((entry->flags & PROP_PERUSER) ? DACL_READ : DACL_PROPCOL))) { /* DAV:need-privileges */ xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, DAV_NEED_PRIVS); *pctx->ret = HTTP_FORBIDDEN; } else { /* Write "live" property */ entry->put(prop, set, pctx, propstat, entry->rock); } } else if (!prop->ns) { /* Property with no namespace */ xmlNodePtr newprop = xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID], prop->name, NULL, NULL, 0); xmlSetNs(newprop, NULL); *pctx->ret = HTTP_FORBIDDEN; } else if (pctx->txn->req_tgt.namespace->id != URL_NS_PRINCIPAL) { /* Write "dead" property */ proppatch_todb(prop, set, pctx, propstat, NULL); } } } } } /* One or more of the properties failed */ if (*pctx->ret && propstat[PROPSTAT_OK].root) { /* 200 status must become 424 */ propstat[PROPSTAT_FAILEDDEP].root = propstat[PROPSTAT_OK].root; propstat[PROPSTAT_FAILEDDEP].status = HTTP_FAILED_DEP; propstat[PROPSTAT_OK].root = NULL; } /* Add status and optional error to the propstat elements and then add them to the response element */ for (i = 0; i < NUM_PROPSTAT; i++) { struct propstat *stat = &propstat[i]; if (stat->root) { xmlNewChild(stat->root, NULL, BAD_CAST "status", BAD_CAST http_statusline(VER_1_1, stat->status)); if (stat->precond) { struct error_t error = { NULL, stat->precond, NULL, NULL, 0 }; xml_add_error(stat->root, &error, pctx->ns); } xmlAddChild(pctx->root, stat->root); } } return 0; } /* Parse an XML body into a tree */ int parse_xml_body(struct transaction_t *txn, xmlNodePtr *root, const char *spec_type) { const char **hdr; xmlDocPtr doc = NULL; int r = 0; *root = NULL; /* Read body */ txn->req_body.flags |= BODY_DECODE; r = http_read_req_body(txn); if (r) { txn->flags.conn = CONN_CLOSE; return r; } if (!buf_len(&txn->req_body.payload)) return 0; /* Check Content-Type */ if (!(hdr = spool_getheader(txn->req_hdrs, "Content-Type")) || (!is_mediatype("text/xml", hdr[0]) && !is_mediatype("application/xml", hdr[0]) && !(spec_type && is_mediatype(spec_type, hdr[0])))) { txn->error.desc = "This method requires an XML body"; return HTTP_BAD_MEDIATYPE; } /* Parse the XML request */ doc = xmlCtxtReadMemory(txn->conn->xml, buf_cstring(&txn->req_body.payload), buf_len(&txn->req_body.payload), NULL, NULL, XML_PARSE_NOERROR | XML_PARSE_NOWARNING); if (!doc) { txn->error.desc = "Unable to parse XML body"; return HTTP_BAD_REQUEST; } else { xmlErrorPtr err = xmlCtxtGetLastError(txn->conn->xml); if (err) { xmlFreeDoc(doc); txn->error.desc = err->message; return HTTP_BAD_REQUEST; } } /* Get the root element of the XML request */ if (!(*root = xmlDocGetRootElement(doc))) { xmlFreeDoc(doc); txn->error.desc = "Missing root element in request"; return HTTP_BAD_REQUEST; } return 0; } /* Perform an ACL request * * preconditions: * DAV:no-ace-conflict * DAV:no-protected-ace-conflict * DAV:no-inherited-ace-conflict * DAV:limited-number-of-aces * DAV:deny-before-grant * DAV:grant-only * DAV:no-invert * DAV:no-abstract * DAV:not-supported-privilege * DAV:missing-required-principal * DAV:recognized-principal * DAV:allowed-principal * * The standard behavior of the ACL method is to completely replace the existing * ACL with the one in the request. We treat the element as providing * "negative" rights (-identifier) in IMAP-speak. Additionally, we treat * the special DAV identifiers as follows: * * == IMAP "anyone" * == IMAP "anonymous" * == IMAP "anyone -anonymous" */ int meth_acl(struct transaction_t *txn, void *params) { struct meth_params *aparams = (struct meth_params *) params; int ret = 0, r, rights; xmlDocPtr indoc = NULL; xmlNodePtr root, ace; struct mailbox *mailbox = NULL; struct buf acl = BUF_INITIALIZER; /* Response should not be cached */ txn->flags.cc |= CC_NOCACHE; /* Parse the path */ r = dav_parse_req_target(txn, aparams); if (r) return r; /* Make sure method is allowed (only allowed on collections) */ if (!(txn->req_tgt.allow & ALLOW_ACL)) { txn->error.desc = "ACLs can only be set on collections"; syslog(LOG_DEBUG, "Tried to set ACL on non-collection"); return HTTP_NOT_ALLOWED; } if (!mboxname_userownsmailbox(httpd_userid, txn->req_tgt.mbentry->name)) { /* Check ACL for current user */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if (!(rights & DACL_ADMIN)) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = DACL_ADMIN; return HTTP_NO_PRIVS; } } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local Mailbox */ /* Parse the ACL body */ ret = parse_xml_body(txn, &root, NULL); if (!ret && !root) { txn->error.desc = "Missing request body"; ret = HTTP_BAD_REQUEST; } if (ret) goto done; indoc = root->doc; /* Make sure it is a DAV:acl element */ if (!root->ns || xmlStrcmp(root->ns->href, BAD_CAST XML_NS_DAV) || xmlStrcmp(root->name, BAD_CAST "acl")) { txn->error.desc = "Missing DAV:acl element in ACL request"; ret = HTTP_BAD_REQUEST; goto done; } /* Open mailbox for writing */ r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } /* Parse the DAV:ace elements */ for (ace = root->children; ace; ace = ace->next) { if (ace->type == XML_ELEMENT_NODE) { xmlNodePtr child = NULL, prin = NULL, privs = NULL; const char *userid = NULL; int deny = 0, rights = 0; char *freeme = NULL; char rightstr[100]; struct request_target_t tgt; for (child = ace->children; child; child = child->next) { if (child->type == XML_ELEMENT_NODE) { if (!xmlStrcmp(child->name, BAD_CAST "principal")) { if (prin) { txn->error.desc = "Multiple principals in ACE"; ret = HTTP_BAD_REQUEST; goto done; } for (prin = child->children; prin && prin->type != XML_ELEMENT_NODE; prin = prin->next); if (!prin) { txn->error.desc = "Empty principal in ACE"; ret = HTTP_BAD_REQUEST; goto done; } } else if (!xmlStrcmp(child->name, BAD_CAST "grant")) { if (privs) { txn->error.desc = "Multiple grant|deny in ACE"; ret = HTTP_BAD_REQUEST; goto done; } for (privs = child->children; privs && privs->type != XML_ELEMENT_NODE; privs = privs->next); } else if (!xmlStrcmp(child->name, BAD_CAST "deny")) { if (privs) { txn->error.desc = "Multiple grant|deny in ACE"; ret = HTTP_BAD_REQUEST; goto done; } for (privs = child->children; privs && privs->type != XML_ELEMENT_NODE; privs = privs->next); deny = 1; } else if (!xmlStrcmp(child->name, BAD_CAST "invert")) { /* DAV:no-invert */ txn->error.precond = DAV_NO_INVERT; ret = HTTP_FORBIDDEN; goto done; } else { txn->error.desc = "Unknown element in ACE"; ret = HTTP_BAD_REQUEST; goto done; } } } if (!xmlStrcmp(prin->name, BAD_CAST "self")) { userid = httpd_userid; } else if (!xmlStrcmp(prin->name, BAD_CAST "owner")) { userid = freeme = mboxname_to_userid(txn->req_tgt.mbentry->name); } else if (!xmlStrcmp(prin->name, BAD_CAST "all")) { userid = "anyone"; } else if (!xmlStrcmp(prin->name, BAD_CAST "authenticated")) { if (deny) { /* DAV:grant-only */ txn->error.precond = DAV_GRANT_ONLY; ret = HTTP_FORBIDDEN; goto done; } userid = "\a"; /* flagged for use below */ } else if (!xmlStrcmp(prin->name, BAD_CAST "unauthenticated")) { userid = "anonymous"; } else if (!xmlStrcmp(prin->name, BAD_CAST "href")) { xmlChar *href = xmlNodeGetContent(prin); xmlURIPtr uri; const char *errstr = NULL; size_t plen = strlen(namespace_principal.prefix); uri = parse_uri(METH_UNKNOWN, (const char *) href, 1, &errstr); if (uri && !strncmp(namespace_principal.prefix, uri->path, plen) && uri->path[plen] == '/') { memset(&tgt, 0, sizeof(struct request_target_t)); tgt.namespace = &namespace_principal; /* XXX: there is no doubt that this leaks memory */ r = principal_parse_path(uri->path, &tgt, &errstr); if (!r && tgt.userid) userid = tgt.userid; } if (uri) xmlFreeURI(uri); xmlFree(href); } if (!userid) { /* DAV:recognized-principal */ txn->error.precond = DAV_RECOG_PRINC; ret = HTTP_FORBIDDEN; free(freeme); goto done; } for (; privs; privs = privs->next) { if (privs->type == XML_ELEMENT_NODE) { xmlNodePtr priv = privs->children; for (; priv->type != XML_ELEMENT_NODE; priv = priv->next); if (aparams->acl_ext && aparams->acl_ext(txn, priv, &rights)) { /* Extension (CalDAV) privileges */ if (txn->error.precond) { ret = HTTP_FORBIDDEN; free(freeme); goto done; } } else if (!xmlStrcmp(priv->ns->href, BAD_CAST XML_NS_DAV)) { /* WebDAV privileges */ if (!xmlStrcmp(priv->name, BAD_CAST "all")) { if (deny) rights |= ACL_FULL; /* wipe EVERYTHING */ else rights |= DACL_ALL; } else if (!xmlStrcmp(priv->name, BAD_CAST "read")) rights |= DACL_READ; else if (!xmlStrcmp(priv->name, BAD_CAST "write")) rights |= DACL_WRITE; else if (!xmlStrcmp(priv->name, BAD_CAST "write-content")) rights |= DACL_WRITECONT; else if (!xmlStrcmp(priv->name, BAD_CAST "write-properties")) rights |= DACL_WRITEPROPS; else if (!xmlStrcmp(priv->name, BAD_CAST "bind")) rights |= DACL_BIND; else if (!xmlStrcmp(priv->name, BAD_CAST "unbind")) rights |= DACL_UNBIND; else if (!xmlStrcmp(priv->name, BAD_CAST "read-current-user-privilege-set") || !xmlStrcmp(priv->name, BAD_CAST "read-acl") || !xmlStrcmp(priv->name, BAD_CAST "write-acl") || !xmlStrcmp(priv->name, BAD_CAST "unlock") || !xmlStrcmp(priv->name, BAD_CAST "share")) { /* DAV:no-abstract */ txn->error.precond = DAV_NO_ABSTRACT; ret = HTTP_FORBIDDEN; free(freeme); goto done; } else { /* DAV:not-supported-privilege */ txn->error.precond = DAV_SUPP_PRIV; ret = HTTP_FORBIDDEN; free(freeme); goto done; } } else if (!xmlStrcmp(priv->ns->href, BAD_CAST XML_NS_CALDAV)) { if (!xmlStrcmp(priv->name, BAD_CAST "read-free-busy")) rights |= DACL_READFB; else { /* DAV:not-supported-privilege */ txn->error.precond = DAV_SUPP_PRIV; ret = HTTP_FORBIDDEN; free(freeme); goto done; } } else if (!xmlStrcmp(priv->ns->href, BAD_CAST XML_NS_CYRUS)) { /* Cyrus-specific privileges */ if (!xmlStrcmp(priv->name, BAD_CAST "write-properties-collection")) rights |= DACL_PROPCOL; else if (!xmlStrcmp(priv->name, BAD_CAST "write-properties-resource")) rights |= DACL_PROPRSRC; else if (!xmlStrcmp(priv->name, BAD_CAST "make-collection")) rights |= DACL_MKCOL; else if (!xmlStrcmp(priv->name, BAD_CAST "remove-collection")) rights |= DACL_RMCOL; else if (!xmlStrcmp(priv->name, BAD_CAST "add-resource")) rights |= DACL_ADDRSRC; else if (!xmlStrcmp(priv->name, BAD_CAST "remove-resource")) rights |= DACL_RMRSRC; else if (!xmlStrcmp(priv->name, BAD_CAST "admin")) rights |= DACL_ADMIN; else { /* DAV:not-supported-privilege */ txn->error.precond = DAV_SUPP_PRIV; ret = HTTP_FORBIDDEN; free(freeme); goto done; } } else { /* DAV:not-supported-privilege */ txn->error.precond = DAV_SUPP_PRIV; ret = HTTP_FORBIDDEN; free(freeme); goto done; } } } /* gotta have something to do! */ if (rights) { cyrus_acl_masktostr(rights, rightstr); if (*userid == '\a') { /* authenticated = "anyone -anonymous" */ buf_printf(&acl, "anyone\t%s\t-anonymous\t%s\t", rightstr, rightstr); } else { buf_printf(&acl, "%s%s\t%s\t", deny ? "-" : "", userid, rightstr); } } free(freeme); } } r = mboxlist_sync_setacls(txn->req_tgt.mbentry->name, buf_cstring(&acl), mailbox_modseq_dirty(mailbox)); if (!r) { mailbox_set_acl(mailbox, buf_cstring(&acl)); char *userid = mboxname_to_userid(txn->req_tgt.mbentry->name); r = caldav_update_shareacls(userid); free(userid); } if (r) { syslog(LOG_ERR, "mboxlist_sync_setacls(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } response_header(HTTP_OK, txn); done: buf_free(&acl); if (indoc) xmlFreeDoc(indoc); mailbox_close(&mailbox); return ret; } struct move_rock { int omlen; int nmlen; struct buf newname; const char *urlprefix; xmlNodePtr root; xmlNsPtr ns[NUM_NAMESPACE]; }; /* Callback for use by dav_move_collection() to move single collection */ static int move_collection(const mbentry_t *mbentry, void *rock) { struct move_rock *mrock = (struct move_rock *) rock; int r = 0; buf_truncate(&mrock->newname, mrock->nmlen); buf_appendcstr(&mrock->newname, mbentry->name + mrock->omlen); if (buf_len(&mrock->newname) >= MAX_MAILBOX_NAME) { r = IMAP_MAILBOX_BADNAME; } else { /* Rename mailbox - Pretend we're an admin since we already renamed the parent */ r = mboxlist_renamemailbox(mbentry, buf_cstring(&mrock->newname), NULL /* partition */, 0 /* uidvalidity */, 1 /* admin */, httpd_userid, httpd_authstate, NULL, 0, 0, 1 /* ignorequota */, 0, 0, 0); } if (r) { struct error_t err = { error_message(r), 0, NULL, NULL, 0 }; struct buf href = BUF_INITIALIZER; xmlNodePtr resp; mbname_t *mbname; const strarray_t *boxes; int n, size, code; if (!mrock->root) { /* Create new */ mrock->root = init_xml_response("multistatus", NS_DAV, NULL, mrock->ns); if (!mrock->root) return HTTP_SERVER_ERROR; } /* Add new element */ resp = xmlNewChild(mrock->root, mrock->ns[NS_DAV], BAD_CAST "response", NULL); if (!resp) return HTTP_SERVER_ERROR; /* Generate href for destination collection */ mbname = mbname_from_intname(buf_cstring(&mrock->newname)); buf_setcstr(&href, mrock->urlprefix); if (mbname_localpart(mbname)) { const char *domain = mbname_domain(mbname) ? mbname_domain(mbname) : httpd_extradomain; buf_printf(&href, "/%s/%s", USER_COLLECTION_PREFIX, mbname_localpart(mbname)); if (domain) buf_printf(&href, "@%s", domain); } buf_putc(&href, '/'); boxes = mbname_boxes(mbname); size = strarray_size(boxes); for (n = 1; n < size; n++) { buf_appendcstr(&href, strarray_nth(boxes, n)); buf_putc(&href, '/'); } mbname_free(&mbname); /* Add element */ xml_add_href(resp, NULL, buf_cstring(&href)); /* Determine HTTP response code */ switch (r) { case IMAP_MAILBOX_BADNAME: code = HTTP_FORBIDDEN; break; case IMAP_MAILBOX_EXISTS: code = HTTP_PRECOND_FAILED; break; case IMAP_PERMISSION_DENIED: code = HTTP_FORBIDDEN; err.precond = DAV_NEED_PRIVS; err.rights = DACL_UNBIND; err.resource = buf_cstring(&href); break; default: code = HTTP_SERVER_ERROR; break; } /* Add element */ xmlNewChild(resp, NULL, BAD_CAST "status", BAD_CAST http_statusline(VER_1_1, code)); /* Add element */ xml_add_error(resp, &err, mrock->ns); buf_free(&href); /* XXX Per RFC 4918, we SHOULD continue to move non-children */ } return r; } /* Callback for use by dav_move_collection() ro remove a single collection */ static int remove_collection(const mbentry_t *mbentry, void *rock __attribute__((unused))) { int r; /* Delete mailbox - Pretend we're an admin since we already deleted the parent */ if (mboxlist_delayed_delete_isenabled()) { r = mboxlist_delayed_deletemailbox(mbentry->name, 1, /* admin */ httpd_userid, httpd_authstate, NULL, MBOXLIST_DELETE_CHECKACL); } else { r = mboxlist_deletemailbox(mbentry->name, 1, /* admin */ httpd_userid, httpd_authstate, NULL, MBOXLIST_DELETE_CHECKACL); } return r; } static int dav_move_collection(struct transaction_t *txn, struct request_target_t *dest_tgt, int overwrite) { int r = 0, recursive = 1, rights; int omlen, nmlen; char *oldmailboxname = txn->req_tgt.mbentry->name; char *newmailboxname = dest_tgt->mbentry->name; struct mboxevent *mboxevent = NULL; /* Make sure we're moving within the same user */ if (!mboxname_same_userid(newmailboxname, oldmailboxname)) { txn->error.desc = "Can only move within same user"; return HTTP_FORBIDDEN; } /* Check ACL for current user on source mailbox */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if ((rights & DACL_UNBIND) != DACL_UNBIND) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = DACL_UNBIND; return HTTP_NO_PRIVS; } if (txn->req_tgt.mbentry->server) { /* Remote source mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local source mailbox */ /* If we're renaming something inside of something else, don't recursively rename */ omlen = strlen(oldmailboxname); nmlen = strlen(newmailboxname); if (omlen < nmlen) { if (!strncmp(oldmailboxname, newmailboxname, omlen) && newmailboxname[omlen] == '.') { recursive = 0; } } else { if (!strncmp(oldmailboxname, newmailboxname, nmlen) && oldmailboxname[nmlen] == '.') { recursive = 0; } } struct mboxlock *namespacelock = mboxname_usernamespacelock(newmailboxname); r = mboxlist_createmailboxcheck(newmailboxname, 0, NULL, httpd_userisadmin, httpd_userid, httpd_authstate, NULL, NULL, 0 /* force */); if (r == IMAP_MAILBOX_EXISTS && overwrite) { /* Attempt to delete existing base mailbox */ overwrite = -1; mboxevent = mboxevent_new(EVENT_MAILBOX_DELETE); if (mboxlist_delayed_delete_isenabled()) { r = mboxlist_delayed_deletemailbox(newmailboxname, httpd_userisadmin, httpd_userid, httpd_authstate, mboxevent, MBOXLIST_DELETE_CHECKACL); } else { r = mboxlist_deletemailbox(newmailboxname, httpd_userisadmin, httpd_userid, httpd_authstate, mboxevent, MBOXLIST_DELETE_CHECKACL); } if (!r) mboxevent_notify(&mboxevent); mboxevent_free(&mboxevent); /* Attempt to delete all existing submailboxes */ if (!r && recursive) { char nmbn[MAX_MAILBOX_BUFFER]; strcpy(nmbn, newmailboxname); strcat(nmbn, "."); r = mboxlist_allmbox(nmbn, remove_collection, NULL, 0); } } if (r) goto done; /* Attempt to rename the base mailbox */ mboxevent = mboxevent_new(EVENT_MAILBOX_RENAME); r = mboxlist_renamemailbox(txn->req_tgt.mbentry, newmailboxname, NULL /* partition */, 0 /* uidvalidity */, httpd_userisadmin, httpd_userid, httpd_authstate, mboxevent, 0, 0, 1 /* ignorequota */, 0, 0, 0); if (!r) mboxevent_notify(&mboxevent); mboxevent_free(&mboxevent); /* Attempt to rename all submailboxes */ if (!r && recursive) { char ombn[MAX_MAILBOX_BUFFER]; struct move_rock mrock = { ++omlen, ++nmlen, BUF_INITIALIZER, dest_tgt->namespace->prefix, NULL, {0} }; strcpy(ombn, oldmailboxname); strcat(ombn, "."); /* Setup the rock */ buf_setcstr(&mrock.newname, newmailboxname); buf_putc(&mrock.newname, '.'); r = mboxlist_allmbox(ombn, move_collection, &mrock, 0); buf_free(&mrock.newname); if (mrock.root) { mboxname_release(&namespacelock); sync_checkpoint(txn->conn->pin); xml_response(HTTP_MULTI_STATUS, txn, mrock.root->doc); xmlFreeDoc(mrock.root->doc); return 0; } } done: mboxname_release(&namespacelock); switch (r) { case 0: sync_checkpoint(txn->conn->pin); return (overwrite < 0) ? HTTP_NO_CONTENT : HTTP_CREATED; case IMAP_MAILBOX_EXISTS: return HTTP_PRECOND_FAILED; case IMAP_PERMISSION_DENIED: /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = dest_tgt->path; txn->error.rights = (overwrite < 0) ? DACL_UNBIND : DACL_BIND; return HTTP_NO_PRIVS; default: txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } } /* Perform a COPY/MOVE request * * preconditions: * *DAV:need-privileges */ int meth_copy_move(struct transaction_t *txn, void *params) { struct meth_params *cparams = (struct meth_params *) params; int ret = HTTP_CREATED, overwrite = 1, r, precond, rights; const char **hdr; xmlURIPtr dest_uri; static struct request_target_t dest_tgt; /* Parsed destination URL - static for Location resp hdr */ struct backend *src_be = NULL, *dest_be = NULL; struct mailbox *src_mbox = NULL, *dest_mbox = NULL; struct dav_data *ddata; struct index_record src_rec; const char *etag = NULL; time_t lastmod = 0; unsigned meth_move = (txn->meth == METH_MOVE); void *src_davdb = NULL, *dest_davdb = NULL, *obj = NULL; struct buf msg_buf = BUF_INITIALIZER; struct buf body_buf = BUF_INITIALIZER; memset(&dest_tgt, 0, sizeof(struct request_target_t)); /* Response should not be cached */ txn->flags.cc |= CC_NOCACHE; /* Parse the source path */ r = dav_parse_req_target(txn, cparams); if (r) return r; /* Make sure method is allowed (not allowed on collections yet) */ if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED; /* Check for mandatory Destination header */ if (!(hdr = spool_getheader(txn->req_hdrs, "Destination"))) { txn->error.desc = "Missing Destination header"; return HTTP_BAD_REQUEST; } /* Parse destination URI */ if (!(dest_uri = parse_uri(METH_UNKNOWN, hdr[0], 1, &txn->error.desc))) { txn->error.desc = "Illegal Destination target URI"; return HTTP_BAD_REQUEST; } /* Make sure source and dest resources are NOT the same */ if (!strcmp(txn->req_uri->path, dest_uri->path)) { txn->error.desc = "Source and destination resources are the same"; ret = HTTP_FORBIDDEN; goto done; } /* Check for COPY/MOVE on collection */ if (!txn->req_tgt.resource) { if (meth_move) { /* Use our own entry for dest to suppress lookup in parse_path() */ dest_tgt.mbentry = mboxlist_entry_create(); } else { /* We don't yet handle COPY on collections */ ret = HTTP_NOT_ALLOWED; goto done; } } /* Parse the destination path */ dest_tgt.namespace = txn->req_tgt.namespace; r = cparams->parse_path(dest_uri->path, &dest_tgt, &txn->error.desc); if (r) { ret = (r == HTTP_MOVED) ? HTTP_FORBIDDEN : r; goto done; } /* Replace cached Destination header with just the absolute path */ spool_replace_header(xstrdup("Destination"), xstrdup(dest_tgt.path), txn->req_hdrs); /* Check for optional Overwrite header */ if ((hdr = spool_getheader(txn->req_hdrs, "Overwrite")) && !strcmp(hdr[0], "F")) { overwrite = 0; } /* Handle MOVE on collection */ if (!txn->req_tgt.resource) { ret = dav_move_collection(txn, &dest_tgt, overwrite); goto done; } /* Make sure we have a dest resource */ if (!dest_tgt.resource) { txn->error.desc = "No destination resource specified"; ret = HTTP_BAD_REQUEST; goto done; } /* Check ACL for current user on source mailbox */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if (((rights & DACL_READ) != DACL_READ) || (meth_move && !(rights & DACL_RMRSRC))) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = (rights & DACL_READ) != DACL_READ ? DACL_READ : DACL_RMRSRC; ret = HTTP_NO_PRIVS; goto done; } /* Check ACL for current user on destination */ rights = httpd_myrights(httpd_authstate, dest_tgt.mbentry); if (!(rights & DACL_ADDRSRC) || !(rights & DACL_WRITECONT)) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = dest_tgt.path; txn->error.rights = !(rights & DACL_ADDRSRC) ? DACL_ADDRSRC : DACL_WRITECONT; ret = HTTP_NO_PRIVS; goto done; } /* Check we're not copying within the same user */ if (!meth_move && cparams->copy.uid_conf_precond && mboxname_same_userid(dest_tgt.mbentry->name, txn->req_tgt.mbentry->name)) { txn->error.precond = cparams->copy.uid_conf_precond; txn->error.desc = "Can not copy resources within same user"; ret = HTTP_NOT_ALLOWED; goto done; } if (txn->req_tgt.mbentry->server) { /* Remote source mailbox */ if (!dest_tgt.mbentry->server) { /* Local destination mailbox */ /* XXX Currently only supports standard Murder */ txn->error.desc = "COPY/MOVE only supported in a standard Murder"; ret = HTTP_NOT_ALLOWED; } else if (!(src_be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in))) { txn->error.desc = "Unable to connect to source backend"; ret = HTTP_UNAVAILABLE; } else if (!(dest_be = proxy_findserver(dest_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in))) { txn->error.desc = "Unable to connect to destination backend"; ret = HTTP_UNAVAILABLE; } else if (src_be == dest_be) { /* Simply send the COPY to the backend */ ret = http_pipe_req_resp(src_be, txn); } else { /* This is the harder case: GET from source and PUT on dest */ ret = http_proxy_copy(src_be, dest_be, txn); } goto done; } else if (dest_tgt.mbentry->server) { /* Local source and remote destination mailbox */ /* XXX Currently only supports standard Murder */ txn->error.desc = "COPY/MOVE only supported in a standard Murder"; ret = HTTP_NOT_ALLOWED; goto done; } /* Local source and destination mailboxes */ if (!strcmp(txn->req_tgt.mbentry->name, dest_tgt.mbentry->name)) { /* Same source and destination - Open source mailbox for writing */ r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &src_mbox); dest_mbox = src_mbox; } else if (meth_move) { /* Open source mailbox for writing */ r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &src_mbox); } else { /* Open source mailbox for reading */ r = mailbox_open_irl(txn->req_tgt.mbentry->name, &src_mbox); } if (r) { syslog(LOG_ERR, "mailbox_open_i%cl(%s) failed: %s", (meth_move || !strcmp(txn->req_tgt.mbentry->name, dest_tgt.mbentry->name)) ? 'w' : 'r', txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } /* Open the DAV DB corresponding to the src mailbox */ src_davdb = cparams->davdb.open_db(src_mbox); /* Find message UID for the source resource */ cparams->davdb.lookup_resource(src_davdb, txn->req_tgt.mbentry, txn->req_tgt.resource, (void **) &ddata, 0); if (!ddata->rowid) { ret = HTTP_NOT_FOUND; goto done; } /* Fetch resource validators */ r = cparams->get_validators(src_mbox, (void *) ddata, httpd_userid, &src_rec, &etag, &lastmod); if (r) { txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } /* Check any preconditions on source */ precond = cparams->check_precond(txn, params, src_mbox, (void *) ddata, etag, lastmod); switch (precond) { case HTTP_OK: break; case HTTP_LOCKED: txn->error.precond = DAV_NEED_LOCK_TOKEN; txn->error.resource = txn->req_tgt.path; GCC_FALLTHROUGH default: /* We failed a precondition - don't perform the request */ ret = precond; goto done; } if (src_rec.uid) { /* Mapped URL - Load message containing the resource and parse data */ mailbox_map_record(src_mbox, &src_rec, &msg_buf); buf_init_ro(&body_buf, buf_base(&msg_buf) + src_rec.header_size, buf_len(&msg_buf) - src_rec.header_size); obj = cparams->mime_types[0].to_object(&body_buf); } else { /* Unmapped URL (empty resource) */ buf_init_ro_cstr(&body_buf, ""); obj = &body_buf; src_rec.recno = ddata->rowid; /* For deleting DAV record */ } if (dest_mbox != src_mbox) { if (!meth_move) { /* Done with source mailbox */ mailbox_unlock_index(src_mbox, NULL); } /* Open dest mailbox for writing */ r = mailbox_open_iwl(dest_tgt.mbentry->name, &dest_mbox); if (r) { syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s", dest_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } /* Open the DAV DB corresponding to the dest mailbox */ dest_davdb = cparams->davdb.open_db(dest_mbox); } else { dest_davdb = src_davdb; } /* Find message UID for the dest resource, if exists */ cparams->davdb.lookup_resource(dest_davdb, dest_tgt.mbentry, dest_tgt.resource, (void **) &ddata, 0); /* XXX Check errors */ /* Check any preconditions on destination */ if (ddata->rowid && !overwrite) { /* Don't overwrite the destination resource */ ret = HTTP_PRECOND_FAILED; goto done; } /* Store the resource at destination */ ret = cparams->copy.proc(txn, obj, dest_mbox, dest_tgt.resource, dest_davdb, 0); if (dest_mbox != src_mbox) { /* Done with destination mailbox */ mailbox_unlock_index(dest_mbox, NULL); } switch (ret) { case HTTP_CREATED: case HTTP_NO_CONTENT: if (meth_move) { /* Expunge the source message */ if (src_rec.uid) { /* Mapped URL */ src_rec.internal_flags |= FLAG_INTERNAL_EXPUNGED; if ((r = mailbox_rewrite_index_record(src_mbox, &src_rec))) { syslog(LOG_ERR, "expunging src record (%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } } else { /* Unmapped URL (empty resource) */ cparams->davdb.delete_resourceLOCKONLY(src_davdb, src_rec.recno); } } } done: if (ret == HTTP_CREATED) { /* Tell client where to find the new resource */ txn->location = dest_tgt.path; } else { /* Don't confuse client by providing ETag of Destination resource */ txn->resp_body.etag = NULL; } if (obj && cparams->mime_types[0].free) cparams->mime_types[0].free(obj); if (dest_mbox != src_mbox) { if (dest_davdb) cparams->davdb.close_db(dest_davdb); if (dest_mbox) mailbox_close(&dest_mbox); } if (src_davdb) cparams->davdb.close_db(src_davdb); if (src_mbox) mailbox_close(&src_mbox); buf_free(&msg_buf); buf_free(&body_buf); xmlFreeURI(dest_uri); free(dest_tgt.userid); mboxlist_entry_free(&dest_tgt.mbentry); return ret; } struct delete_rock { struct transaction_t *txn; struct mailbox *mailbox; delete_proc_t deletep; }; static int delete_cb(void *rock, void *data) { struct delete_rock *drock = (struct delete_rock *) rock; struct dav_data *ddata = (struct dav_data *) data; struct index_record record; int r; if (!ddata->imap_uid) { /* Unmapped URL (empty resource) */ return 0; } /* Fetch index record for the resource */ r = mailbox_find_index_record(drock->mailbox, ddata->imap_uid, &record); if (r) { drock->txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } r = drock->deletep(drock->txn, drock->mailbox, &record, data); return r; } /* DELETE collection */ static int meth_delete_collection(struct transaction_t *txn, struct meth_params *dparams) { int ret = HTTP_NO_CONTENT, r = 0, precond, rights, needrights; struct mailbox *mailbox = NULL; /* if FastMail sharing, we need to remove ACLs */ if (config_getswitch(IMAPOPT_FASTMAILSHARING) && !mboxname_userownsmailbox(httpd_userid, txn->req_tgt.mbentry->name)) { struct mboxlock *namespacelock = mboxname_usernamespacelock(txn->req_tgt.mbentry->name); r = mboxlist_setacl(&httpd_namespace, txn->req_tgt.mbentry->name, httpd_userid, /*rights*/NULL, /*isadmin*/1, httpd_userid, httpd_authstate); mboxname_release(&namespacelock); if (r) { syslog(LOG_ERR, "meth_delete(%s) failed to remove acl: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } sync_checkpoint(txn->conn->pin); return HTTP_OK; } /* Special case of deleting a shared collection */ if (txn->req_tgt.flags == TGT_DAV_SHARED) { char *inboxname = mboxname_user_mbox(txn->req_tgt.userid, NULL); mbentry_t *mbentry = NULL; r = proxy_mlookup(inboxname, &mbentry, NULL, NULL); if (r) { syslog(LOG_ERR, "mlookup(%s) failed: %s", inboxname, error_message(r)); ret = HTTP_NOT_FOUND; } else if (mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) ret = HTTP_UNAVAILABLE; else ret = http_pipe_req_resp(be, txn); } else { /* Local Mailbox */ struct mailbox *mailbox = NULL; /* Unsubscribe */ r = mboxlist_changesub(txn->req_tgt.mbentry->name, txn->req_tgt.userid, httpd_authstate, 0 /* remove */, 0, 0); if (r) { syslog(LOG_ERR, "mboxlist_changesub(%s, %s) failed: %s", txn->req_tgt.mbentry->name, txn->req_tgt.userid, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; } else ret = HTTP_NO_CONTENT; /* Set invite status to declined */ r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "IOERROR: failed to open mailbox %s for DELETE share", txn->req_tgt.mbentry->name); } else { annotate_state_t *astate = NULL; r = mailbox_get_annotate_state(mailbox, 0, &astate); if (!r) { const char *annot = DAV_ANNOT_NS "<" XML_NS_DAV ">invite-status"; struct buf value = BUF_INITIALIZER; buf_init_ro_cstr(&value, "invite-declined"); r = annotate_state_writemask(astate, annot, txn->req_tgt.userid, &value); } mailbox_close(&mailbox); } } mboxlist_entry_free(&mbentry); free(inboxname); sync_checkpoint(txn->conn->pin); return ret; } /* Check ACL for current user */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); needrights = DACL_RMCOL; if (!(rights & needrights)) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = needrights; return HTTP_NO_PRIVS; } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local Mailbox */ /* Open mailbox for reading */ r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } /* Check any preconditions */ precond = dparams->check_precond(txn, dparams, mailbox, NULL, NULL, 0); switch (precond) { case HTTP_OK: break; case HTTP_LOCKED: txn->error.precond = DAV_NEED_LOCK_TOKEN; txn->error.resource = txn->req_tgt.path; GCC_FALLTHROUGH default: /* We failed a precondition - don't perform the request */ ret = precond; goto done; } if (dparams->delete) { /* Do special processing on all resources */ struct delete_rock drock = { txn, NULL, dparams->delete }; /* Open the DAV DB corresponding to the mailbox */ void *davdb = dparams->davdb.open_db(mailbox); drock.mailbox = mailbox; r = dparams->davdb.foreach_resource(davdb, txn->req_tgt.mbentry, &delete_cb, &drock); dparams->davdb.close_db(davdb); if (r) { txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } } /* we need the mailbox closed before we delete it */ mailbox_close(&mailbox); mbname_t *mbname = mbname_from_intname(txn->req_tgt.mbentry->name); struct mboxlock *namespacelock = user_namespacelock(mbname_userid(mbname)); struct mboxevent *mboxevent = mboxevent_new(EVENT_MAILBOX_DELETE); if (mboxlist_delayed_delete_isenabled()) { r = mboxlist_delayed_deletemailbox(txn->req_tgt.mbentry->name, httpd_userisadmin || httpd_userisproxyadmin, httpd_userid, httpd_authstate, mboxevent, MBOXLIST_DELETE_CHECKACL); } else { r = mboxlist_deletemailbox(txn->req_tgt.mbentry->name, httpd_userisadmin || httpd_userisproxyadmin, httpd_userid, httpd_authstate, mboxevent, MBOXLIST_DELETE_CHECKACL); } if (!r) { r = caldav_update_shareacls(mbname_userid(mbname)); } if (r == IMAP_PERMISSION_DENIED) ret = HTTP_FORBIDDEN; else if (r == IMAP_MAILBOX_NONEXISTENT) ret = HTTP_NOT_FOUND; else if (r) ret = HTTP_SERVER_ERROR; else mboxevent_notify(&mboxevent); mboxevent_free(&mboxevent); mboxname_release(&namespacelock); mbname_free(&mbname); done: mailbox_close(&mailbox); sync_checkpoint(txn->conn->pin); return ret; } /* DELETE resource */ static int meth_delete_resource(struct transaction_t *txn, struct meth_params *dparams) { int ret = HTTP_NO_CONTENT, r = 0, precond, rights, needrights; struct mboxevent *mboxevent = NULL; struct mailbox *mailbox = NULL; struct dav_data *ddata; struct index_record record; const char *etag = NULL; time_t lastmod = 0; void *davdb = NULL; /* Check ACL for current user */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); needrights = DACL_RMRSRC; if (!(rights & needrights)) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = needrights; return HTTP_NO_PRIVS; } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local Mailbox */ /* Open mailbox for writing */ r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } /* Open the DAV DB corresponding to the mailbox */ davdb = dparams->davdb.open_db(mailbox); /* Find message UID for the resource, if exists */ dparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry, txn->req_tgt.resource, (void **) &ddata, 0); if (!ddata->rowid) { ret = HTTP_NOT_FOUND; goto done; } /* Fetch resource validators */ r = dparams->get_validators(mailbox, (void *) ddata, httpd_userid, &record, &etag, &lastmod); if (r) { txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } /* Check any preconditions */ precond = dparams->check_precond(txn, dparams, mailbox, (void *) ddata, etag, lastmod); switch (precond) { case HTTP_OK: break; case HTTP_LOCKED: txn->error.precond = DAV_NEED_LOCK_TOKEN; txn->error.resource = txn->req_tgt.path; GCC_FALLTHROUGH default: /* We failed a precondition - don't perform the request */ ret = precond; goto done; } if (record.uid) { /* Do any special processing */ if (dparams->delete) dparams->delete(txn, mailbox, &record, ddata); /* Expunge the resource */ record.internal_flags |= FLAG_INTERNAL_EXPUNGED; mboxevent = mboxevent_new(EVENT_MESSAGE_EXPUNGE); r = mailbox_rewrite_index_record(mailbox, &record); if (r) { syslog(LOG_ERR, "expunging record (%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; } else { mboxevent_extract_record(mboxevent, mailbox, &record); mboxevent_extract_mailbox(mboxevent, mailbox); mboxevent_set_numunseen(mboxevent, mailbox, -1); mboxevent_set_access(mboxevent, NULL, NULL, httpd_userid, txn->req_tgt.mbentry->name, 0); mboxevent_notify(&mboxevent); } mboxevent_free(&mboxevent); } done: if (davdb) dparams->davdb.close_db(davdb); mailbox_close(&mailbox); sync_checkpoint(txn->conn->pin); return ret; } /* Perform a DELETE request */ int meth_delete(struct transaction_t *txn, void *params) { struct meth_params *dparams = (struct meth_params *) params; /* Response should not be cached */ txn->flags.cc |= CC_NOCACHE; /* Parse the path */ int r = dav_parse_req_target(txn, dparams); if (r) return r; /* Make sure method is allowed */ if (!(txn->req_tgt.allow & ALLOW_DELETE)) return HTTP_NOT_ALLOWED; if (txn->req_tgt.resource) return meth_delete_resource(txn, dparams); return meth_delete_collection(txn, dparams); } /* Perform a GET/HEAD request on a DAV resource */ int meth_get_head(struct transaction_t *txn, void *params) { struct meth_params *gparams = (struct meth_params *) params; const char **hdr; struct mime_type_t *mime = NULL; int ret = 0, r = 0, precond, rights; const char *data = NULL; unsigned long datalen = 0, offset = 0; struct buf msg_buf = BUF_INITIALIZER; struct resp_body_t *resp_body = &txn->resp_body; struct mailbox *mailbox = NULL; struct dav_data *ddata; struct index_record record; const char *etag = NULL; time_t lastmod = 0; void *davdb = NULL, *obj = NULL; char *freeme = NULL; /* Parse the path */ r = dav_parse_req_target(txn, gparams); if (r) return r; /* Special "principal" */ if (txn->req_tgt.flags == TGT_SERVER_INFO) return get_server_info(txn); if (!txn->req_tgt.mbentry) { /* No content for non-mailbox URLs (yet) */ return HTTP_NO_CONTENT; } /* Check ACL for current user */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if ((rights & DACL_READ) != DACL_READ) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = DACL_READ; return HTTP_NO_PRIVS; } if (gparams->mime_types) { /* Check requested MIME type: 1st entry in gparams->mime_types array MUST be default MIME type */ if ((hdr = spool_getheader(txn->req_hdrs, "Accept"))) mime = get_accept_type(hdr, gparams->mime_types); else mime = gparams->mime_types; if (!mime) return HTTP_NOT_ACCEPTABLE; } if (!txn->req_tgt.resource) { /* Do any collection processing */ if (gparams->get) return gparams->get(txn, NULL, NULL, NULL, NULL, mime); /* We don't handle GET on a collection */ return HTTP_NO_CONTENT; } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local Mailbox */ /* Open mailbox for reading */ r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); goto done; } /* Open the DAV DB corresponding to the mailbox */ davdb = gparams->davdb.open_db(mailbox); /* Find message UID for the resource */ gparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry, txn->req_tgt.resource, (void **) &ddata, 0); if (!ddata->rowid) { ret = HTTP_NOT_FOUND; goto done; } /* Fetch resource validators */ r = gparams->get_validators(mailbox, (void *) ddata, httpd_userid, &record, &etag, &lastmod); if (r) { txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } txn->flags.ranges = (ddata->imap_uid != 0); /* Check any preconditions, including range request */ precond = gparams->check_precond(txn, params, mailbox, (void *) ddata, etag, lastmod); switch (precond) { case HTTP_OK: case HTTP_PARTIAL: case HTTP_NOT_MODIFIED: /* Fill in ETag, Last-Modified, Expires, and Cache-Control */ resp_body->etag = etag; resp_body->lastmod = lastmod; resp_body->maxage = 3600; /* 1 hr */ txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE; /* don't use stale data */ if (httpd_userid) txn->flags.cc |= CC_PRIVATE; if (precond != HTTP_NOT_MODIFIED && record.uid) break; GCC_FALLTHROUGH default: /* We failed a precondition - don't perform the request */ ret = precond; goto done; } /* Do any special processing */ if (gparams->get) { ret = gparams->get(txn, mailbox, &record, ddata, &obj, mime); if (ret != HTTP_CONTINUE) goto done; ret = 0; } if (mime && !resp_body->type) { txn->flags.vary |= VARY_ACCEPT; resp_body->type = mime->content_type; } if (!obj) { /* Raw resource - length doesn't include RFC 5322 header */ offset = record.header_size; datalen = record.size - offset; if (txn->meth == METH_GET) { /* Load message containing the resource */ r = mailbox_map_record(mailbox, &record, &msg_buf); if (r) goto done; data = buf_base(&msg_buf) + offset; if (mime != gparams->mime_types) { /* Not the storage format - create resource object */ struct buf inbuf = BUF_INITIALIZER; buf_init_ro(&inbuf, data, datalen); obj = gparams->mime_types[0].to_object(&inbuf); buf_free(&inbuf); } } } if (obj) { /* Convert object into requested MIME type */ struct buf *outbuf = mime->from_object(obj); datalen = buf_len(outbuf); if (txn->meth == METH_GET) data = freeme = buf_release(outbuf); buf_destroy(outbuf); if (gparams->mime_types[0].free) gparams->mime_types[0].free(obj); } write_body(precond, txn, data, datalen); buf_free(&msg_buf); free(freeme); done: if (davdb) gparams->davdb.close_db(davdb); if (r) { txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; } mailbox_close(&mailbox); return ret; } /* Perform a LOCK request * * preconditions: * DAV:need-privileges * DAV:no-conflicting-lock * DAV:lock-token-submitted */ int meth_lock(struct transaction_t *txn, void *params) { struct meth_params *lparams = (struct meth_params *) params; int ret = HTTP_OK, r, precond, rights; struct mailbox *mailbox = NULL; struct dav_data *ddata; struct index_record oldrecord; const char *etag; time_t lastmod; xmlDocPtr indoc = NULL, outdoc = NULL; xmlNodePtr root = NULL; xmlNsPtr ns[NUM_NAMESPACE]; xmlBufferPtr owner = NULL; time_t now = time(NULL); void *davdb = NULL; /* XXX We ignore Depth and Timeout header fields */ /* Response should not be cached */ txn->flags.cc |= CC_NOCACHE; /* Parse the path */ r = dav_parse_req_target(txn, lparams); if (r) return r; /* Make sure method is allowed (only allowed on resources) */ if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED; /* Check ACL for current user */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if (!(rights & DACL_WRITECONT) || !(rights & DACL_ADDRSRC)) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = !(rights & DACL_WRITECONT) ? DACL_WRITECONT : DACL_ADDRSRC; return HTTP_NO_PRIVS; } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local Mailbox */ /* Open mailbox for reading */ r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } /* Open the DAV DB corresponding to the mailbox */ davdb = lparams->davdb.open_db(mailbox); lparams->davdb.begin_transaction(davdb); /* Find message UID for the resource, if exists */ lparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry, txn->req_tgt.resource, (void *) &ddata, 1); /* Fetch resource validators */ r = lparams->get_validators(mailbox, (void *) ddata, httpd_userid, &oldrecord, &etag, &lastmod); if (r) { txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } if (!ddata->alive) { /* New resource */ ddata->creationdate = now; ddata->mailbox = ddata->mailbox_byname ? mailbox_name(mailbox) : mailbox_uniqueid(mailbox); ddata->resource = txn->req_tgt.resource; ddata->imap_uid = 0; ddata->lock_expire = 0; ddata->alive = 1; } /* Check any preconditions */ precond = lparams->check_precond(txn, params, mailbox, (void *) ddata, etag, lastmod); switch (precond) { case HTTP_OK: break; case HTTP_LOCKED: if (strcmp(ddata->lock_ownerid, httpd_userid)) txn->error.precond = DAV_LOCKED; else txn->error.precond = DAV_NEED_LOCK_TOKEN; txn->error.resource = txn->req_tgt.path; GCC_FALLTHROUGH default: /* We failed a precondition - don't perform the request */ ret = precond; goto done; } if (ddata->lock_expire <= now) { /* Create new lock */ xmlNodePtr node, sub; /* Parse the required body */ ret = parse_xml_body(txn, &root, NULL); if (!ret && !root) { txn->error.desc = "Missing request body"; ret = HTTP_BAD_REQUEST; } if (ret) goto done; /* Make sure it is a DAV:lockinfo element */ indoc = root->doc; if (!root->ns || xmlStrcmp(root->ns->href, BAD_CAST XML_NS_DAV) || xmlStrcmp(root->name, BAD_CAST "lockinfo")) { txn->error.desc = "Missing DAV:lockinfo element in LOCK request"; ret = HTTP_BAD_MEDIATYPE; goto done; } /* Parse elements of lockinfo */ for (node = root->children; node; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (!xmlStrcmp(node->name, BAD_CAST "lockscope")) { /* Find child element of lockscope */ for (sub = node->children; sub && sub->type != XML_ELEMENT_NODE; sub = sub->next); /* Make sure it is an exclusive element */ if (!sub || xmlStrcmp(sub->name, BAD_CAST "exclusive")) { txn->error.desc = "Only exclusive locks are supported"; ret = HTTP_BAD_REQUEST; goto done; } } else if (!xmlStrcmp(node->name, BAD_CAST "locktype")) { /* Find child element of locktype */ for (sub = node->children; sub && sub->type != XML_ELEMENT_NODE; sub = sub->next); /* Make sure it is a write element */ if (!sub || xmlStrcmp(sub->name, BAD_CAST "write")) { txn->error.desc = "Only write locks are supported"; ret = HTTP_BAD_REQUEST; goto done; } } else if (!xmlStrcmp(node->name, BAD_CAST "owner")) { /* Find child element of owner */ owner = xmlBufferCreate(); for (sub = node->children; sub && sub->type != XML_ELEMENT_NODE; sub = sub->next); if (!sub) { xmlNodeBufGetContent(owner, node); } /* Make sure it is a href element */ else if (xmlStrcmp(sub->name, BAD_CAST "href")) { ret = HTTP_BAD_REQUEST; goto done; } else { xmlNodeBufGetContent(owner, sub); xmlBufferAddHead(owner, BAD_CAST "", 10); } } } ddata->lock_ownerid = httpd_userid; if (owner) ddata->lock_owner = (const char *) xmlBufferContent(owner); /* Construct lock-token */ assert(!buf_len(&txn->buf)); buf_printf(&txn->buf, LOCK_TOKEN_URL_SCHEME "%s", makeuuid()); ddata->lock_token = buf_cstring(&txn->buf); } /* Update lock expiration */ ddata->lock_expire = now + 300; /* 5 min */ /* Start construction of our prop response */ if (!(root = init_xml_response("prop", NS_DAV, root, ns))) { ret = HTTP_SERVER_ERROR; txn->error.desc = "Unable to create XML response"; goto done; } outdoc = root->doc; root = xmlNewChild(root, NULL, BAD_CAST "lockdiscovery", NULL); xml_add_lockdisc(root, txn->req_tgt.path, (struct dav_data *) ddata); r = lparams->davdb.write_resourceLOCKONLY(davdb, ddata); if (r) { syslog(LOG_ERR, "Unable to write lock record to DAV DB: %s", error_message(r)); ret = HTTP_SERVER_ERROR; txn->error.desc = "Unable to create locked resource"; goto done; } txn->resp_body.lock = ddata->lock_token; if (!ddata->rowid) { ret = HTTP_CREATED; /* Tell client where to find the new resource */ txn->location = txn->req_tgt.path; } else ret = HTTP_OK; xml_response(ret, txn, outdoc); ret = 0; done: if (davdb) { /* XXX - error handling/abort */ lparams->davdb.commit_transaction(davdb); lparams->davdb.close_db(davdb); } mailbox_close(&mailbox); if (outdoc) xmlFreeDoc(outdoc); if (indoc) xmlFreeDoc(indoc); if (owner) xmlBufferFree(owner); return ret; } /* Perform a MKCOL/MKCALENDAR request */ /* * preconditions: * DAV:resource-must-be-null * DAV:need-privileges * DAV:valid-resourcetype * CALDAV:calendar-collection-location-ok * CALDAV:valid-calendar-data (CALDAV:calendar-timezone) */ int meth_mkcol(struct transaction_t *txn, void *params) { struct meth_params *mparams = (struct meth_params *) params; int ret = 0, r = 0; xmlDocPtr indoc = NULL, outdoc = NULL; xmlNodePtr root = NULL, instr = NULL; xmlNsPtr ns[NUM_NAMESPACE]; char *partition = NULL; struct proppatch_ctx pctx; struct mailbox *mailbox = NULL; struct mboxlock *namespacelock = NULL; memset(&pctx, 0, sizeof(struct proppatch_ctx)); /* Response should not be cached */ txn->flags.cc |= CC_NOCACHE; /* Parse the path (use our own entry to suppress lookup) */ txn->req_tgt.mbentry = mboxlist_entry_create(); r = mparams->parse_path(txn->req_uri->path, &txn->req_tgt, &txn->error.desc); /* Make sure method is allowed (only allowed on child of home-set) */ if (!txn->req_tgt.collection || txn->req_tgt.resource) { txn->error.precond = mparams->mkcol.location_precond; return HTTP_FORBIDDEN; } else if (r) { switch (r) { case IMAP_MAILBOX_EXISTS: txn->error.precond = DAV_RES_EXISTS; break; case IMAP_PERMISSION_DENIED: txn->error.precond = DAV_NEED_PRIVS; txn->error.rights = DACL_BIND; buf_reset(&txn->buf); buf_printf(&txn->buf, "%s/%s/%s", txn->req_tgt.namespace->prefix, USER_COLLECTION_PREFIX, txn->req_tgt.userid); txn->error.resource = buf_cstring(&txn->buf); break; default: txn->error.precond = mparams->mkcol.location_precond; break; } return HTTP_FORBIDDEN; } if (config_mupdate_server && !config_getstring(IMAPOPT_PROXYSERVERS)) { /* Remote mailbox - find the parent */ mbentry_t *parent = NULL; struct backend *be; r = mboxlist_findparent(txn->req_tgt.mbentry->name, &parent); if (r) { txn->error.precond = mparams->mkcol.location_precond; ret = HTTP_FORBIDDEN; goto done; } be = proxy_findserver(parent->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) ret = HTTP_UNAVAILABLE; else ret = http_pipe_req_resp(be, txn); goto done; } /* Local Mailbox */ /* Parse the MKCOL/MKCALENDAR body, if exists */ ret = parse_xml_body(txn, &root, NULL); if (ret) goto done; if (root) { /* Check for correct root element (lowercase method name ) */ const char *ns_href; indoc = root->doc; buf_setcstr(&txn->buf, http_methods[txn->meth].name); ns_href = buf_len(&txn->buf) > 5 ? XML_NS_CALDAV : XML_NS_DAV; if (!root->ns || xmlStrcmp(root->ns->href, BAD_CAST ns_href) || xmlStrcmp(root->name, BAD_CAST buf_lcase(&txn->buf))) { txn->error.desc = "Incorrect root element in MKCOL/MKCALENDAR request"; ret = HTTP_BAD_REQUEST; goto done; } instr = root->children; } namespacelock = mboxname_usernamespacelock(txn->req_tgt.mbentry->name); /* Create the mailbox */ mbentry_t mbentry = MBENTRY_INITIALIZER; mbentry.name = txn->req_tgt.mbentry->name; mbentry.mbtype = mparams->mkcol.mbtype; mbentry.partition = partition; r = mboxlist_createmailbox(&mbentry, 0/*options*/, 0/*highestmodseq*/, httpd_userisadmin || httpd_userisproxyadmin, httpd_userid, httpd_authstate, 0/*flags*/, &mailbox); if (instr && !r) { /* Start construction of our mkcol/mkcalendar response */ buf_appendcstr(&txn->buf, "-response"); root = init_xml_response(buf_cstring(&txn->buf), NS_REQ_ROOT, root, ns); buf_reset(&txn->buf); if (!root) { ret = HTTP_SERVER_ERROR; txn->error.desc = "Unable to create XML response"; mboxname_release(&namespacelock); goto done; } outdoc = root->doc; /* Populate our proppatch context */ pctx.txn = txn; pctx.mailbox = mailbox; pctx.lprops = mparams->propfind.lprops; pctx.root = root; pctx.ns = ns; pctx.tid = NULL; pctx.ret = &r; /* Execute the property patch instructions */ ret = do_proppatch(&pctx, instr); if (ret || r) { /* Setting properties failed - delete mailbox */ mailbox_abort(mailbox); mailbox_close(&mailbox); mboxlist_deletemailbox(txn->req_tgt.mbentry->name, /*isadmin*/1, NULL, NULL, NULL, MBOXLIST_DELETE_FORCE); if (!ret) { /* Output the XML response */ if (txn->meth == METH_MKCALENDAR) { /* MKCALENDAR failure response MUST be 207 (Multi-Status) */ xmlNodeSetName(root, BAD_CAST "multistatus"); xmlSetNs(root, ns[NS_DAV]); r = HTTP_MULTI_STATUS; } xml_response(r, txn, outdoc); } mboxname_release(&namespacelock); goto done; } } if (!r) { if (mparams->mkcol.proc) r = mparams->mkcol.proc(mailbox); assert(!buf_len(&txn->buf)); dav_get_synctoken(mailbox, &txn->buf, ""); txn->resp_body.ctag = buf_cstring(&txn->buf); ret = HTTP_CREATED; } else if (r == IMAP_PERMISSION_DENIED) ret = HTTP_NO_PRIVS; else if (r == IMAP_MAILBOX_EXISTS) { txn->error.precond = DAV_RES_EXISTS; ret = HTTP_FORBIDDEN; } else { txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; } done: buf_free(&pctx.buf); mailbox_close(&mailbox); mboxname_release(&namespacelock); sync_checkpoint(txn->conn->pin); if (partition) free(partition); if (outdoc) xmlFreeDoc(outdoc); if (indoc) xmlFreeDoc(indoc); return ret; } /* dav_foreach() callback to find props on a resource */ int propfind_by_resource(void *rock, void *data) { struct propfind_ctx *fctx = (struct propfind_ctx *) rock; struct dav_data *ddata = (struct dav_data *) data; struct index_record record; char *p; size_t len; int r = 0, ret = 0; keepalive_response(fctx->txn); /* Append resource name to URL path */ if (!fctx->req_tgt->resource) { len = strlen(fctx->req_tgt->path); p = fctx->req_tgt->path + len; } else { p = fctx->req_tgt->resource; len = p - fctx->req_tgt->path; } if (p[-1] != '/') { *p++ = '/'; len++; } strlcpy(p, ddata->resource, MAX_MAILBOX_PATH - len); fctx->req_tgt->resource = p; fctx->req_tgt->reslen = strlen(p); fctx->data = data; if (ddata->imap_uid && !fctx->record) { /* Fetch index record for the resource */ r = mailbox_find_index_record(fctx->mailbox, ddata->imap_uid, &record); fctx->record = r ? NULL : &record; } if (r || (!ddata->imap_uid && ddata->lock_expire <= time(NULL))) { /* Add response for missing target */ ret = xml_add_response(fctx, HTTP_NOT_FOUND, 0, NULL, NULL); } else if (!fctx->filter || fctx->filter(fctx, data)) { /* Add response for target */ ret = xml_add_response(fctx, 0, 0, NULL, NULL); } buf_free(&fctx->msg_buf); if (fctx->obj) { fctx->free_obj(fctx->obj); fctx->obj = NULL; } fctx->record = NULL; fctx->data = NULL; return ret; } static int propfind_by_resources(struct propfind_ctx *fctx) { int r = 0; sqlite3 *newdb; if (!fctx->mailbox) return 0; /* Open the DAV DB corresponding to the mailbox. * * Note we open the new one first before closing the old one, so we * get refcounted retaining of the open database within a single user */ newdb = fctx->open_db(fctx->mailbox); if (fctx->davdb) fctx->close_db(fctx->davdb); fctx->davdb = newdb; if (fctx->req_tgt->resource) { /* Add response for target resource */ struct dav_data *ddata; /* Find message UID for the resource */ fctx->lookup_resource(fctx->davdb, fctx->mbentry, fctx->req_tgt->resource, (void **) &ddata, 0); if (!ddata->rowid) { /* Add response for missing target */ xml_add_response(fctx, HTTP_NOT_FOUND, 0, NULL, NULL); return HTTP_NOT_FOUND; } r = fctx->proc_by_resource(fctx, ddata); } else { /* Add responses for all contained resources */ fctx->foreach_resource(fctx->davdb, fctx->mbentry, fctx->proc_by_resource, fctx); /* Started with NULL resource, end with NULL resource */ fctx->req_tgt->resource = NULL; fctx->req_tgt->reslen = 0; } return r; } HIDDEN size_t make_collection_url(struct buf *buf, const char *urlprefix, int haszzzz, const mbname_t *mbname, const char *userid) { const strarray_t *boxes; int n, size; size_t len; buf_reset(buf); buf_printf(buf, "%s/", urlprefix); if (userid) { const char *owner = mbname_userid(mbname); if (!owner) owner = ""; if (config_getswitch(IMAPOPT_FASTMAILSHARING)) { buf_printf(buf, "%s/%s/", haszzzz ? "zzzz" : USER_COLLECTION_PREFIX, owner); } else { buf_printf(buf, "%s/", USER_COLLECTION_PREFIX); if (*userid) { buf_printf(buf, "%s/", userid); if (strcmp(owner, userid)) { /* Encode shared collection as: "." */ buf_printf(buf, "%s%c", owner, SHARED_COLLECTION_DELIM); } } else buf_printf(buf, "%s/", owner); } } len = buf_len(buf); /* add collection(s) to path */ boxes = mbname_boxes(mbname); size = strarray_size(boxes); for (n = 1; n < size; n++) { buf_appendcstr(buf, strarray_nth(boxes, n)); buf_putc(buf, '/'); } return len; } /* mboxlist_findall() callback to find props on a collection */ int propfind_by_collection(const mbentry_t *mbentry, void *rock) { struct propfind_ctx *fctx = (struct propfind_ctx *) rock; const char *mboxname = mbentry->name; struct buf writebuf = BUF_INITIALIZER; struct mailbox *mailbox = NULL; char *p; size_t len; int r = 0, rights = 0; /* skip deleted items */ if (mboxname_isdeletedmailbox(mbentry->name, 0) || (mbentry->mbtype & MBTYPE_DELETED)) { goto done; } /* Check ACL on mailbox for current user */ rights = httpd_myrights(httpd_authstate, mbentry); if ((rights & fctx->reqd_privs) != fctx->reqd_privs) goto done; /* We only match known types */ if (mbtype_isa(mbentry->mbtype) != fctx->req_tgt->namespace->mboxtype) goto done; p = strrchr(mboxname, '.'); if (!p) goto done; p++; /* skip dot */ switch (fctx->req_tgt->namespace->id) { case URL_NS_DRIVE: if (fctx->req_tgt->flags == TGT_DRIVE_USER) { /* Special case of listing users with DAV #drives */ p = strchr(mboxname+5, '.') + 1; /* skip "user.XXX." */ if (strcmp(p, fctx->req_tgt->mboxprefix)) goto done; } else if (p - mboxname > 1 + (int) strlen(fctx->req_tgt->mbentry->name)) { /* Reject folders that are more than one level deep */ goto done; } break; default: /* Magic folder filter */ if (httpd_extrafolder && strcasecmp(p, httpd_extrafolder)) goto done; break; } /* skip toplevels */ if (config_getswitch(IMAPOPT_FASTMAILSHARING) && *p == '#') goto done; /* Open mailbox for reading */ if ((r = mailbox_open_irl(mboxname, &mailbox))) { syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s", mboxname, error_message(r)); } fctx->mbentry = mbentry; fctx->mailbox = mailbox; fctx->record = NULL; if (!fctx->req_tgt->resource) { /* we always have zzzz if it's already in the URL */ int haszzzz = fctx->req_tgt->flags & TGT_USER_ZZZZ; mbname_t *mbname = mbname_from_intname(mboxname); if (!mbname_domain(mbname)) mbname_set_domain(mbname, httpd_extradomain); /* we also need to deal with the discovery case, * where mboxname doesn't match request path */ if (fctx->req_tgt->userid && strcmpsafe(mbname_userid(mbname), fctx->req_tgt->userid)) { haszzzz = 1; } len = make_collection_url(&writebuf, fctx->req_tgt->namespace->prefix, - haszzzz, mbname, mbname_userid(mbname_from_intname(mboxname))); + haszzzz, mbname, fctx->req_tgt->userid); mbname_free(&mbname); /* copy it all back into place... in theory we should check against * 'last' and make sure it doesn't change from the original request. * yay for micro-optimised memory usage... */ strlcpy(fctx->req_tgt->path, buf_cstring(&writebuf), MAX_MAILBOX_PATH); p = fctx->req_tgt->path + len; fctx->req_tgt->collection = p; fctx->req_tgt->collen = strlen(p); /* If not filtering by calendar resource, and not excluding root, add response for collection */ if (!r && !fctx->filter_crit && !(fctx->prefer & PREFER_NOROOT) && (r = xml_add_response(fctx, 0, 0, NULL, NULL))) goto done; } if (r) { xml_add_response(fctx, HTTP_SERVER_ERROR, 0, error_message(r), NULL); goto done; } if (fctx->depth > 1 && fctx->open_db) { // can't do davdb searches if no dav db /* Resource(s) */ r = propfind_by_resources(fctx); } done: buf_free(&writebuf); if (mailbox) mailbox_close(&mailbox); return 0; } /* Free an entry list */ HIDDEN void free_entry_list(struct propfind_entry_list *elist) { while (elist) { struct propfind_entry_list *freeme = elist; elist = elist->next; if (freeme->flags & PROP_CLEANUP) { freeme->get(freeme->name, NULL, NULL, NULL, NULL, NULL, freeme->rock); } xmlFree(freeme->name); free(freeme); } } -static int wrapper_propfind_by_collection(struct findall_data *data, void* rock) { - if (!data || !data->mbentry) return 0; - struct propfind_ctx *fctx = (struct propfind_ctx *) rock; - if ((httpd_myrights(httpd_authstate, data->mbentry) & DACL_READ) != DACL_READ) return 0; - return propfind_by_collection(data->mbentry, fctx); -} - /* Perform a PROPFIND request */ EXPORTED int meth_propfind(struct transaction_t *txn, void *params) { struct meth_params *fparams = (struct meth_params *) params; int ret = 0, r; const char **hdr; unsigned depth; xmlDocPtr indoc = NULL, outdoc = NULL; xmlNodePtr root, cur = NULL, props = NULL; xmlNsPtr ns[NUM_NAMESPACE]; struct hash_table ns_table = HASH_TABLE_INITIALIZER; struct propfind_ctx fctx; memset(&fctx, 0, sizeof(struct propfind_ctx)); /* Parse the path */ if (fparams->parse_path) { r = dav_parse_req_target(txn, fparams); if (r) { struct backend *be; be = proxy_findinboxserver(txn->req_tgt.userid); if (!be) { syslog(LOG_ERR, "Could not find a backend"); return HTTP_UNAVAILABLE; } return http_pipe_req_resp(be, txn); return r; } } /* Make sure method is allowed */ if (!(txn->req_tgt.allow & ALLOW_DAV)) return HTTP_NOT_ALLOWED; /* Check Depth */ hdr = spool_getheader(txn->req_hdrs, "Depth"); if (!hdr || !strcmp(hdr[0], "infinity")) { depth = 3; if ((txn->error.precond = fparams->propfind.finite_depth_precond)) { ret = HTTP_FORBIDDEN; goto done; } } else if (!strcmp(hdr[0], "1")) { depth = 1; } else if (!strcmp(hdr[0], "0")) { depth = 0; } else { txn->error.desc = "Illegal Depth value"; return HTTP_BAD_REQUEST; } if (txn->req_tgt.mbentry) { int rights; /* Check ACL for current user */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if ((rights & DACL_READ) != DACL_READ) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = DACL_READ; ret = HTTP_NO_PRIVS; goto done; } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local Mailbox */ } /* Principal or Local Mailbox */ /* Parse the PROPFIND body, if exists */ ret = parse_xml_body(txn, &root, NULL); if (ret) goto done; if (!root) { /* Empty request */ fctx.mode = PROPFIND_ALL; } else { indoc = root->doc; /* Make sure it is a DAV:propfind element */ if (!root->ns || xmlStrcmp(root->ns->href, BAD_CAST XML_NS_DAV) || xmlStrcmp(root->name, BAD_CAST "propfind")) { txn->error.desc = "Missing DAV:propfind element in PROPFIND request"; ret = HTTP_BAD_REQUEST; goto done; } /* Find child element of propfind */ for (cur = root->children; cur && cur->type != XML_ELEMENT_NODE; cur = cur->next); if (!cur) { txn->error.desc = "Missing child node element in PROPFIND request"; ret = HTTP_BAD_REQUEST; goto done; } /* Add propfind type to our header cache */ spool_cache_header(xstrdup(":type"), xstrdup((const char *) cur->name), txn->req_hdrs); /* Make sure it is a known element */ if (!xmlStrcmp(cur->name, BAD_CAST "allprop")) { fctx.mode = PROPFIND_ALL; } else if (!xmlStrcmp(cur->name, BAD_CAST "propname")) { fctx.mode = PROPFIND_NAME; fctx.prefer = PREFER_MIN; /* Don't want 404 (Not Found) */ } else if (!xmlStrcmp(cur->name, BAD_CAST "prop")) { fctx.mode = PROPFIND_PROP; props = cur->children; } else { ret = HTTP_BAD_REQUEST; goto done; } /* Check for extra elements */ for (cur = cur->next; cur; cur = cur->next) { if (cur->type == XML_ELEMENT_NODE) { if ((fctx.mode == PROPFIND_ALL) && !props && /* Check for 'include' element */ !xmlStrcmp(cur->name, BAD_CAST "include")) { props = cur->children; } else { ret = HTTP_BAD_REQUEST; goto done; } } } } /* Start construction of our multistatus response */ root = init_xml_response("multistatus", NS_DAV, root, ns); if (!root) { ret = HTTP_SERVER_ERROR; txn->error.desc = "Unable to create XML response"; goto done; } outdoc = root->doc; /* Populate our propfind context */ fctx.txn = txn; fctx.req_tgt = &txn->req_tgt; fctx.depth = depth; fctx.prefer |= get_preferences(txn); fctx.userid = httpd_userid; fctx.userisadmin = httpd_userisadmin; fctx.authstate = httpd_authstate; fctx.mbentry = NULL; fctx.mailbox = NULL; fctx.record = NULL; fctx.get_validators = fparams->get_validators; fctx.reqd_privs = DACL_READ; fctx.filter = NULL; fctx.filter_crit = NULL; if (fparams->mime_types) fctx.free_obj = fparams->mime_types[0].free; fctx.open_db = fparams->davdb.open_db; fctx.close_db = fparams->davdb.close_db; fctx.lookup_resource = fparams->davdb.lookup_resource; fctx.foreach_resource = fparams->davdb.foreach_resource; fctx.proc_by_resource = &propfind_by_resource; fctx.elist = NULL; fctx.lprops = fparams->propfind.lprops; fctx.root = root; fctx.ns = ns; fctx.ns_table = &ns_table; fctx.ret = &ret; /* Parse the list of properties and build a list of callbacks */ ret = preload_proplist(props, &fctx); if (ret) goto done; /* iCalendar/vCard data in response should not be transformed */ if (fctx.flags.fetcheddata) txn->flags.cc |= CC_NOTRANSFORM; /* Setup for chunked response */ txn->flags.te |= TE_CHUNKED; /* Begin XML response */ xml_response(HTTP_MULTI_STATUS, txn, fctx.root->doc); /* Generate responses */ if (txn->req_tgt.namespace->id == URL_NS_PRINCIPAL) { if (!depth || !(fctx.prefer & PREFER_NOROOT)) { /* Add response for target URL */ xml_add_response(&fctx, 0, 0, NULL, NULL); } if (depth > 0 && !txn->req_tgt.userid) { size_t len = strlen(namespace_principal.prefix); char *p = txn->req_tgt.path + len; if (!strcmp(p, "/" USER_COLLECTION_PREFIX "/")) { /* Normalize depth so that: * 0 = prin-set, 1+ = collection, 2+ = principal, 3+ = infinity! */ depth++; } else { /* Add a response for 'user' collection */ snprintf(p, MAX_MAILBOX_PATH - len, "/%s/", USER_COLLECTION_PREFIX); xml_add_response(&fctx, 0, 0, NULL, NULL); } if (depth >= 2) { /* Add responses for all user principals */ ret = mboxlist_alluser(principal_search, &fctx); } } } else { /* Normalize depth so that: * 0 = home-set, 1+ = collection, 2+ = resource, 3+ = infinity! */ if (txn->req_tgt.collection) depth++; if (txn->req_tgt.resource) depth++; fctx.depth = depth; if (!txn->req_tgt.collection && (!depth || !(fctx.prefer & PREFER_NOROOT))) { /* Add response for home-set collection */ if (txn->req_tgt.mbentry) { /* Open mailbox for reading */ if ((r = mailbox_open_irl(txn->req_tgt.mbentry->name, &fctx.mailbox)) && r != IMAP_MAILBOX_NONEXISTENT) { syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } fctx.mbentry = txn->req_tgt.mbentry; } if (!fctx.req_tgt->resource) xml_add_response(&fctx, 0, 0, NULL, NULL); /* Resource(s) */ r = propfind_by_resources(&fctx); if (r) ret = r; mailbox_close(&fctx.mailbox); } if (depth > 0) { /* Collection(s) */ if (txn->req_tgt.collection) { /* Add response for target collection */ propfind_by_collection(txn->req_tgt.mbentry, &fctx); } else if (config_getswitch(IMAPOPT_FASTMAILSHARING)) { /* Add responses for all visible collections */ mboxlist_usermboxtree(httpd_userid, httpd_authstate, propfind_by_collection, &fctx, MBOXTREE_PLUS_RACL); } else if (txn->req_tgt.mbentry) { /* Add responses for all contained collections */ fctx.prefer &= ~PREFER_NOROOT; + mboxlist_mboxtree(txn->req_tgt.mbentry->name, + propfind_by_collection, &fctx, + MBOXTREE_SKIP_ROOT); switch (txn->req_tgt.namespace->id) { case URL_NS_DRIVE: - mboxlist_mboxtree(txn->req_tgt.mbentry->name, - propfind_by_collection, &fctx, - MBOXTREE_SKIP_ROOT); if (txn->req_tgt.flags == TGT_DRIVE_ROOT) { /* Add a response for 'user' hierarchy */ buf_setcstr(&fctx.buf, txn->req_tgt.namespace->prefix); buf_printf(&fctx.buf, "/%s/", USER_COLLECTION_PREFIX); strlcpy(fctx.req_tgt->path, buf_cstring(&fctx.buf), MAX_MAILBOX_PATH); fctx.mbentry = NULL; fctx.mailbox = NULL; r = xml_add_response(&fctx, 0, 0, NULL, NULL); } break; case URL_NS_CALENDAR: - mboxlist_mboxtree(txn->req_tgt.mbentry->name, - propfind_by_collection, &fctx, - MBOXTREE_SKIP_ROOT); if (fctx.flags.cs_sharing) { /* Add response for notification collection */ r = propfind_csnotify_collection(&fctx, props); } /* Fall through */ case URL_NS_ADDRESSBOOK: - { - const char *conf_str = txn->req_tgt.namespace->id == URL_NS_CALENDAR ? - config_getstring(IMAPOPT_CALENDARPREFIX) : config_getstring(IMAPOPT_ADDRESSBOOKPREFIX); - char *pat = malloc(5 + strlen(conf_str)); - sprintf(pat, "*%s%c*", conf_str, httpd_namespace.hier_sep); - mboxlist_findall(&httpd_namespace, pat, httpd_userisadmin, txn->req_tgt.userid, httpd_authstate, wrapper_propfind_by_collection, &fctx); - free(pat); - } + /* Add responses for shared collections */ + mboxlist_usersubs(txn->req_tgt.userid, + propfind_by_collection, &fctx, + MBOXTREE_SKIP_PERSONAL); break; } } ret = *fctx.ret; } } if (fctx.davdb) fctx.close_db(fctx.davdb); /* End XML response */ xml_partial_response(txn, fctx.root->doc, NULL /* end */, 0, &fctx.xmlbuf); xmlBufferFree(fctx.xmlbuf); // might have made a change! sync_checkpoint(txn->conn->pin); /* End of output */ write_body(0, txn, NULL, 0); ret = 0; done: /* Free the entry list */ free_entry_list(fctx.elist); buf_free(&fctx.buf); free_hash_table(&ns_table, NULL); if (outdoc) xmlFreeDoc(outdoc); if (indoc) xmlFreeDoc(indoc); return ret; } /* Perform a PROPPATCH request * * preconditions: * DAV:cannot-modify-protected-property * CALDAV:valid-calendar-data (CALDAV:calendar-timezone) */ int meth_proppatch(struct transaction_t *txn, void *params) { struct meth_params *pparams = (struct meth_params *) params; int ret = 0, r = 0; xmlDocPtr indoc = NULL, outdoc = NULL; xmlNodePtr root, instr, resp; xmlNsPtr ns[NUM_NAMESPACE]; struct mailbox *mailbox = NULL; struct proppatch_ctx pctx; struct index_record record; void *davdb = NULL; memset(&pctx, 0, sizeof(struct proppatch_ctx)); /* Response should not be cached */ txn->flags.cc |= CC_NOCACHE; /* Parse the path */ r = dav_parse_req_target(txn, pparams); if (r) return r; if (!txn->req_tgt.collection && !txn->req_tgt.userid) { txn->error.desc = "PROPPATCH requires a collection"; return HTTP_NOT_ALLOWED; } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local Mailbox */ r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "IOERROR: failed to open mailbox %s for proppatch", txn->req_tgt.mbentry->name); return HTTP_SERVER_ERROR; } /* Parse the PROPPATCH body */ ret = parse_xml_body(txn, &root, NULL); if (!ret && !root) { txn->error.desc = "Missing request body"; ret = HTTP_BAD_REQUEST; } if (ret) goto done; indoc = root->doc; /* Make sure it is a DAV:propertyupdate element */ if (!root->ns || xmlStrcmp(root->ns->href, BAD_CAST XML_NS_DAV) || xmlStrcmp(root->name, BAD_CAST "propertyupdate")) { txn->error.desc = "Missing DAV:propertyupdate element in PROPPATCH request"; ret = HTTP_BAD_REQUEST; goto done; } instr = root->children; /* Start construction of our multistatus response */ if (!(root = init_xml_response("multistatus", NS_DAV, root, ns))) { txn->error.desc = "Unable to create XML response"; ret = HTTP_SERVER_ERROR; goto done; } outdoc = root->doc; /* Add a response tree to 'root' for the specified href */ resp = xmlNewChild(root, NULL, BAD_CAST "response", NULL); if (!resp) syslog(LOG_ERR, "new child response failed"); xmlNewChild(resp, NULL, BAD_CAST "href", BAD_CAST txn->req_tgt.path); /* Populate our proppatch context */ pctx.txn = txn; pctx.mailbox = mailbox; pctx.record = NULL; pctx.lprops = pparams->propfind.lprops; pctx.root = resp; pctx.ns = ns; pctx.tid = NULL; pctx.ret = &r; if (txn->req_tgt.resource) { struct dav_data *ddata; /* gotta find the resource */ /* Open the DAV DB corresponding to the mailbox */ davdb = pparams->davdb.open_db(mailbox); /* Find message UID for the resource */ pparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry, txn->req_tgt.resource, (void **) &ddata, 0); if (!ddata->imap_uid) { ret = HTTP_NOT_FOUND; goto done; } memset(&record, 0, sizeof(struct index_record)); /* Mapped URL - Fetch index record for the resource */ r = mailbox_find_index_record(mailbox, ddata->imap_uid, &record); if (r) { ret = HTTP_NOT_FOUND; goto done; } pctx.record = &record; } /* Execute the property patch instructions */ ret = do_proppatch(&pctx, instr); done: if (r) mailbox_abort(mailbox); mailbox_close(&mailbox); if (davdb) pparams->davdb.close_db(davdb); sync_checkpoint(txn->conn->pin); if (!ret) { /* Output the XML response if wanted */ if (get_preferences(txn) & PREFER_MIN) ret = HTTP_OK; else xml_response(HTTP_MULTI_STATUS, txn, outdoc); } buf_free(&pctx.buf); if (outdoc) xmlFreeDoc(outdoc); if (indoc) xmlFreeDoc(indoc); return ret; } static int dav_post_import(struct transaction_t *txn, struct meth_params *pparams) { int ret = 0, r, precond = HTTP_OK, rights; const char **hdr; struct mime_type_t *mime = NULL; struct mailbox *mailbox = NULL; quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_DONTCARE_INITIALIZER; void *davdb = NULL, *obj = NULL; xmlDocPtr outdoc = NULL; xmlNodePtr root; xmlNsPtr ns[NUM_NAMESPACE]; unsigned data_ns = pparams->post.bulk.data_ns; /* Check Content-Type */ mime = pparams->mime_types; if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) { for (; mime->content_type; mime++) { if (is_mediatype(mime->content_type, hdr[0])) break; } if (!mime->content_type) { txn->error.precond = pparams->put.supp_data_precond; return HTTP_FORBIDDEN; } } /* Check ACL for current user */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if (!(rights & DACL_WRITECONT) || !(rights & DACL_ADDRSRC)) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = !(rights & DACL_WRITECONT) ? DACL_WRITECONT : DACL_ADDRSRC; return HTTP_NO_PRIVS; } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local mailbox */ /* Read body */ txn->req_body.flags |= BODY_DECODE; r = http_read_req_body(txn); if (r) { txn->flags.conn = CONN_CLOSE; return r; } /* Check if we can append a new message to mailbox */ qdiffs[QUOTA_STORAGE] = buf_len(&txn->req_body.payload); qdiffs[QUOTA_MESSAGE] = 1; if ((r = append_check(txn->req_tgt.mbentry->name, httpd_authstate, ACL_INSERT, ignorequota ? NULL : qdiffs))) { syslog(LOG_ERR, "append_check(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } /* Open mailbox for writing */ r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } /* Open the DAV DB corresponding to the mailbox */ davdb = pparams->davdb.open_db(mailbox); /* Check any preconditions */ assert(!buf_len(&txn->buf)); buf_printf(&txn->buf, "%u-%u-%u", mailbox->i.uidvalidity, mailbox->i.last_uid, mailbox->i.exists); ret = precond = pparams->check_precond(txn, pparams, mailbox, NULL, buf_cstring(&txn->buf), mailbox->index_mtime); buf_reset(&txn->buf); switch (precond) { case HTTP_OK: break; case HTTP_LOCKED: txn->error.precond = DAV_NEED_LOCK_TOKEN; txn->error.resource = txn->req_tgt.path; GCC_FALLTHROUGH case HTTP_PRECOND_FAILED: default: /* We failed a precondition */ ret = precond; goto done; } /* Parse and validate the resources */ obj = mime->to_object(&txn->req_body.payload); ret = pparams->post.bulk.import(txn, obj, NULL, NULL, NULL, NULL, 0); if (ret) goto done; /* Start construction of our multistatus response */ root = init_xml_response("multistatus", NS_DAV, NULL, ns); if (!root) { ret = HTTP_SERVER_ERROR; txn->error.desc = "Unable to create XML response"; goto done; } ensure_ns(ns, NS_CS, root, XML_NS_CS, "CS"); ensure_ns(ns, data_ns, root, known_namespaces[data_ns].href, known_namespaces[data_ns].prefix); outdoc = root->doc; /* Setup for chunked response */ txn->flags.te |= TE_CHUNKED; /* Include CTag in trailer */ txn->flags.trailer |= TRAILER_CTAG; /* Begin XML response */ xml_response(HTTP_MULTI_STATUS, txn, outdoc); /* Store the resources */ ret = pparams->post.bulk.import(txn, obj, mailbox, davdb, root, ns, get_preferences(txn)); /* Validators */ dav_get_synctoken(mailbox, &txn->buf, ""); txn->resp_body.ctag = buf_cstring(&txn->buf); sync_checkpoint(txn->conn->pin); /* End of output */ write_body(0, txn, NULL, 0); ret = 0; done: if (outdoc) xmlFreeDoc(outdoc); if (obj) { if (pparams->mime_types[0].free) pparams->mime_types[0].free(obj); } if (davdb) pparams->davdb.close_db(davdb); mailbox_close(&mailbox); return ret; } /* Perform a POST request */ int meth_post(struct transaction_t *txn, void *params) { struct meth_params *pparams = (struct meth_params *) params; struct strlist *action; int r, ret; size_t len; /* Response should not be cached */ txn->flags.cc |= CC_NOCACHE; if (httpd_userid) txn->flags.cc |= CC_PRIVATE; /* Parse the path */ r = dav_parse_req_target(txn, pparams); if (r) return r; /* Make sure method is allowed (only allowed on certain collections) */ if (!(txn->req_tgt.allow & ALLOW_POST)) return HTTP_NOT_ALLOWED; /* Do any special processing */ if (pparams->post.proc) { ret = pparams->post.proc(txn); if (ret != HTTP_CONTINUE) return ret; } /* Check for query params */ action = hash_lookup("action", &txn->req_qparams); if (!action) { /* Check Content-Type */ const char **hdr = spool_getheader(txn->req_hdrs, "Content-Type"); if ((pparams->post.allowed & POST_SHARE) && hdr && (is_mediatype(hdr[0], DAVSHARING_CONTENT_TYPE) || is_mediatype(hdr[0], "text/xml"))) { /* Sharing request */ return dav_post_share(txn, pparams); } else if (pparams->post.bulk.data_prop && hdr && is_mediatype(hdr[0], "application/xml")) { /* Bulk CRUD */ return HTTP_FORBIDDEN; } else if (pparams->post.bulk.import && hdr) { /* Bulk import */ return dav_post_import(txn, pparams); } else return HTTP_BAD_REQUEST; } if (!(pparams->post.allowed & POST_ADDMEMBER) || !action || action->next || strcmp(action->s, "add-member")) { return HTTP_BAD_REQUEST; } /* POST add-member to regular collection */ /* Append a unique resource name to URL path and perform a PUT */ len = strlen(txn->req_tgt.path); txn->req_tgt.resource = txn->req_tgt.path + len; txn->req_tgt.reslen = snprintf(txn->req_tgt.resource, MAX_MAILBOX_PATH - len, "%s.%s", makeuuid(), pparams->mime_types[0].file_ext ? pparams->mime_types[0].file_ext : ""); /* Tell client where to find the new resource */ txn->location = txn->req_tgt.path; ret = meth_put(txn, params); if (ret != HTTP_CREATED) txn->location = NULL; return ret; } /* Perform a PATCH request * * preconditions: */ int meth_patch(struct transaction_t *txn, void *params) { struct meth_params *pparams = (struct meth_params *) params; int ret, r, precond, rights; const char **hdr, *etag; struct patch_doc_t *patch_doc = NULL; struct mailbox *mailbox = NULL; struct dav_data *ddata; struct index_record oldrecord; time_t lastmod; unsigned flags = 0; void *davdb = NULL, *obj = NULL; struct buf msg_buf = BUF_INITIALIZER; /* Response should not be cached */ txn->flags.cc |= CC_NOCACHE; /* Parse the path */ r = dav_parse_req_target(txn, pparams); if (r) return r; /* Make sure method is allowed (only allowed on resources) */ if (!(txn->req_tgt.allow & ALLOW_PATCH)) return HTTP_NOT_ALLOWED; /* Check Content-Type */ if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) { for (patch_doc = pparams->patch_docs; patch_doc->format; patch_doc++) { if (is_mediatype(patch_doc->format, hdr[0])) break; } } if (!patch_doc || !patch_doc->format) { txn->resp_body.patch = pparams->patch_docs; return HTTP_BAD_MEDIATYPE; } /* Check ACL for current user */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if (!(rights & DACL_WRITECONT)) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = DACL_WRITECONT; return HTTP_NO_PRIVS; } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local Mailbox */ /* Read body */ txn->req_body.flags |= BODY_DECODE; ret = http_read_req_body(txn); if (ret) { txn->flags.conn = CONN_CLOSE; return ret; } /* Check if we can append a new message to mailbox */ /* XXX Can we guess-timate the size difference? */ if ((r = append_check(txn->req_tgt.mbentry->name, httpd_authstate, ACL_INSERT, NULL))) { syslog(LOG_ERR, "append_check(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } /* Open mailbox for writing */ r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } /* Open the DAV DB corresponding to the mailbox */ davdb = pparams->davdb.open_db(mailbox); /* Find message UID for the resource */ pparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry, txn->req_tgt.resource, (void *) &ddata, 0); if (!ddata->imap_uid) { ret = HTTP_NOT_FOUND; goto done; } /* Fetch resource validators */ r = pparams->get_validators(mailbox, (void *) ddata, httpd_userid, &oldrecord, &etag, &lastmod); if (r) { txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } /* Check any preferences */ flags = get_preferences(txn); /* Check any preconditions */ ret = precond = pparams->check_precond(txn, params, mailbox, (void *) ddata, etag, lastmod); switch (precond) { case HTTP_PRECOND_FAILED: if (!(flags & PREFER_REP)) goto done; /* Fill in ETag and Last-Modified */ txn->resp_body.etag = etag; txn->resp_body.lastmod = lastmod; /* Fall through and load message */ GCC_FALLTHROUGH case HTTP_OK: { unsigned offset; struct buf buf = BUF_INITIALIZER; /* Load message containing the resource */ mailbox_map_record(mailbox, &oldrecord, &msg_buf); /* Resource length doesn't include RFC 5322 header */ offset = oldrecord.header_size; /* Parse existing resource */ buf_init_ro(&buf, buf_base(&msg_buf) + offset, buf_len(&msg_buf) - offset); obj = pparams->mime_types[0].to_object(&buf); buf_free(&buf); if (precond == HTTP_OK) { /* Parse, validate, and apply the patch document to the resource */ ret = patch_doc->proc(txn, obj); if (!ret) { ret = pparams->put.proc(txn, obj, mailbox, txn->req_tgt.resource, davdb, flags); if (ret == HTTP_FORBIDDEN) ret = HTTP_UNPROCESSABLE; } } break; } case HTTP_LOCKED: txn->error.precond = DAV_NEED_LOCK_TOKEN; txn->error.resource = txn->req_tgt.path; default: /* We failed a precondition */ goto done; } if (flags & PREFER_REP) { struct resp_body_t *resp_body = &txn->resp_body; struct mime_type_t *mime = pparams->mime_types; struct buf *data; if ((hdr = spool_getheader(txn->req_hdrs, "Accept"))) { mime = get_accept_type(hdr, pparams->mime_types); if (!mime) goto done; } switch (ret) { case HTTP_NO_CONTENT: ret = HTTP_OK; GCC_FALLTHROUGH case HTTP_CREATED: case HTTP_PRECOND_FAILED: /* Convert into requested MIME type */ data = mime->from_object(obj); /* Fill in Content-Type, Content-Length */ resp_body->type = mime->content_type; resp_body->len = buf_len(data); /* Fill in Content-Location */ resp_body->loc = txn->req_tgt.path; /* Fill in Expires and Cache-Control */ resp_body->maxage = 3600; /* 1 hr */ txn->flags.cc = CC_MAXAGE | CC_REVALIDATE /* don't use stale data */ | CC_NOTRANSFORM; /* don't alter iCal data */ /* Output current representation */ write_body(ret, txn, buf_base(data), buf_len(data)); buf_destroy(data); ret = 0; break; default: /* failure - do nothing */ break; } } done: if (obj) { if (pparams->mime_types[0].free) pparams->mime_types[0].free(obj); buf_free(&msg_buf); } if (davdb) pparams->davdb.close_db(davdb); mailbox_close(&mailbox); return ret; } /* Perform a PUT request * * preconditions: * *DAV:supported-address-data */ int meth_put(struct transaction_t *txn, void *params) { struct meth_params *pparams = (struct meth_params *) params; int ret, r, precond, rights, reqd_rights; const char **hdr, *etag; struct mime_type_t *mime = NULL; struct mailbox *mailbox = NULL; struct dav_data *ddata; struct index_record oldrecord; quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_DONTCARE_INITIALIZER; time_t lastmod; unsigned flags = 0; void *davdb = NULL, *obj = NULL; struct buf msg_buf = BUF_INITIALIZER; if (txn->meth == METH_POST) { reqd_rights = DACL_ADDRSRC; } else { /* Response should not be cached */ txn->flags.cc |= CC_NOCACHE; /* Parse the path */ r = dav_parse_req_target(txn, pparams); if (r) { switch (r){ case HTTP_MOVED: case HTTP_SERVER_ERROR: return r; default: return HTTP_FORBIDDEN; } } /* Make sure method is allowed (only allowed on resources) */ if (!((txn->req_tgt.allow & ALLOW_WRITE) && txn->req_tgt.resource)) return HTTP_NOT_ALLOWED; reqd_rights = DACL_WRITECONT; if (txn->req_tgt.allow & ALLOW_USERDATA) reqd_rights |= DACL_PROPRSRC; } /* Make sure Content-Range isn't specified */ if (spool_getheader(txn->req_hdrs, "Content-Range")) return HTTP_BAD_REQUEST; /* Check Content-Type */ mime = pparams->mime_types; if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) { for (; mime->content_type; mime++) { if (is_mediatype(mime->content_type, hdr[0])) break; } if (!mime->content_type) { txn->error.precond = pparams->put.supp_data_precond; return HTTP_FORBIDDEN; } } /* Check ACL for current user */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if (!(rights & reqd_rights)) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = reqd_rights; return HTTP_NO_PRIVS; } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local Mailbox */ /* Make sure mailbox type is correct */ if (mbtype_isa(txn->req_tgt.mbentry->mbtype) != txn->req_tgt.namespace->mboxtype) { txn->error.precond = CALDAV_LOCATION_OK; return HTTP_FORBIDDEN; } /* Read body */ txn->req_body.flags |= BODY_DECODE; ret = http_read_req_body(txn); if (ret) { txn->flags.conn = CONN_CLOSE; return ret; } if (rights & DACL_WRITECONT) { /* Check if we can append a new message to mailbox */ qdiffs[QUOTA_STORAGE] = buf_len(&txn->req_body.payload); qdiffs[QUOTA_MESSAGE] = 1; if ((r = append_check(txn->req_tgt.mbentry->name, httpd_authstate, ACL_INSERT, ignorequota ? NULL : qdiffs))) { syslog(LOG_ERR, "append_check(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } } /* Open mailbox for writing */ r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); return HTTP_SERVER_ERROR; } /* Open the DAV DB corresponding to the mailbox */ davdb = pparams->davdb.open_db(mailbox); /* Find message UID for the resource, if exists */ pparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry, txn->req_tgt.resource, (void *) &ddata, 0); /* XXX Check errors */ /* Fetch resource validators */ r = pparams->get_validators(mailbox, (void *) ddata, httpd_userid, &oldrecord, &etag, &lastmod); if (r) { txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } /* Check any preferences */ flags = get_preferences(txn); /* Check any preconditions */ if (txn->meth == METH_POST) { assert(!buf_len(&txn->buf)); buf_printf(&txn->buf, "%u-%u-%u", mailbox->i.uidvalidity, mailbox->i.last_uid, mailbox->i.exists); ret = precond = pparams->check_precond(txn, params, mailbox, NULL, buf_cstring(&txn->buf), mailbox->index_mtime); buf_reset(&txn->buf); } else { ret = precond = pparams->check_precond(txn, params, mailbox, (void *) ddata, etag, lastmod); } switch (precond) { case HTTP_OK: /* Parse, validate, and store the resource */ obj = mime->to_object(&txn->req_body.payload); ret = pparams->put.proc(txn, obj, mailbox, txn->req_tgt.resource, davdb, flags); break; case HTTP_PRECOND_FAILED: if ((flags & PREFER_REP) && ((rights & DACL_READ) == DACL_READ)) { /* Fill in ETag and Last-Modified */ txn->resp_body.etag = etag; txn->resp_body.lastmod = lastmod; if (pparams->get) { r = pparams->get(txn, mailbox, &oldrecord, (void *) ddata, &obj, mime); if (r != HTTP_CONTINUE) flags &= ~PREFER_REP; } else { unsigned offset; struct buf buf = BUF_INITIALIZER; /* Load message containing the resource */ mailbox_map_record(mailbox, &oldrecord, &msg_buf); /* Resource length doesn't include RFC 5322 header */ offset = oldrecord.header_size; /* Parse existing resource */ buf_init_ro(&buf, buf_base(&msg_buf) + offset, buf_len(&msg_buf) - offset); obj = pparams->mime_types[0].to_object(&buf); buf_free(&buf); } } break; case HTTP_LOCKED: txn->error.precond = DAV_NEED_LOCK_TOKEN; txn->error.resource = txn->req_tgt.path; default: /* We failed a precondition */ goto done; } if (txn->req_tgt.allow & ALLOW_PATCH) { /* Add Accept-Patch formats to response */ txn->resp_body.patch = pparams->patch_docs; } if (flags & PREFER_REP) { struct resp_body_t *resp_body = &txn->resp_body; const char **hdr; struct buf *data; if ((hdr = spool_getheader(txn->req_hdrs, "Accept"))) { mime = get_accept_type(hdr, pparams->mime_types); if (!mime) goto done; } switch (ret) { case HTTP_NO_CONTENT: ret = HTTP_OK; GCC_FALLTHROUGH case HTTP_CREATED: case HTTP_PRECOND_FAILED: /* Convert into requested MIME type */ data = mime->from_object(obj); /* Fill in Content-Type, Content-Length */ resp_body->type = mime->content_type; resp_body->len = buf_len(data); /* Fill in Content-Location */ resp_body->loc = txn->req_tgt.path; /* Fill in Expires and Cache-Control */ resp_body->maxage = 3600; /* 1 hr */ txn->flags.cc = CC_MAXAGE | CC_REVALIDATE /* don't use stale data */ | CC_NOTRANSFORM; /* don't alter iCal data */ if (httpd_userid) txn->flags.cc |= CC_PRIVATE; /* Output current representation */ write_body(ret, txn, buf_base(data), buf_len(data)); buf_destroy(data); ret = 0; break; default: /* failure - do nothing */ break; } } done: if (obj && pparams->mime_types[0].free) pparams->mime_types[0].free(obj); buf_free(&msg_buf); if (davdb) pparams->davdb.close_db(davdb); mailbox_close(&mailbox); // XXX - this is AFTER the response has been sent, we need to // refactor this for total safety sync_checkpoint(txn->conn->pin); return ret; } /* CALDAV:calendar-multiget/CARDDAV:addressbook-multiget REPORT */ int report_multiget(struct transaction_t *txn, struct meth_params *rparams, xmlNodePtr inroot, struct propfind_ctx *fctx) { int r, ret = 0; struct mailbox *mailbox = NULL; xmlNodePtr node; /* Setup for chunked response */ txn->flags.te |= TE_CHUNKED; /* Begin XML response */ xml_response(HTTP_MULTI_STATUS, txn, fctx->root->doc); /* Get props for each href */ for (node = inroot->children; node; node = node->next) { if ((node->type == XML_ELEMENT_NODE) && !xmlStrcmp(node->name, BAD_CAST "href")) { xmlChar *href = xmlNodeListGetString(inroot->doc, node->children, 1); xmlURIPtr uri; struct request_target_t tgt; struct dav_data *ddata; const char *resultstr = NULL; memset(&tgt, 0, sizeof(struct request_target_t)); /* Parse the URI */ uri = parse_uri(METH_REPORT, (const char *) href, 1 /* path required */, &resultstr); xmlFree(href); if (!uri) { r = HTTP_FORBIDDEN; } else { /* Parse the path */ tgt.namespace = txn->req_tgt.namespace; r = rparams->parse_path(uri->path, &tgt, &resultstr); xmlFreeURI(uri); } if (r) { if (r == HTTP_MOVED) xml_add_response(fctx, HTTP_MOVED, 0, NULL, resultstr); else xml_add_response(fctx, r, 0, resultstr, NULL); goto next; } fctx->req_tgt = &tgt; fctx->mbentry = tgt.mbentry; /* Check if we already have this mailbox open */ if (!mailbox || strcmp(mailbox_name(mailbox), tgt.mbentry->name)) { if (mailbox) mailbox_close(&mailbox); /* Open mailbox for reading */ r = mailbox_open_irl(tgt.mbentry->name, &mailbox); if (r && r != IMAP_MAILBOX_NONEXISTENT) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", tgt.mbentry->name, error_message(r)); xml_add_response(fctx, HTTP_SERVER_ERROR, 0, error_message(r), NULL); goto next; } fctx->mailbox = mailbox; } if (!fctx->mailbox || !tgt.resource) { /* Add response for missing target */ xml_add_response(fctx, HTTP_NOT_FOUND, 0, NULL, NULL); goto next; } /* Open the DAV DB corresponding to the mailbox */ fctx->davdb = rparams->davdb.open_db(fctx->mailbox); /* Find message UID for the resource */ rparams->davdb.lookup_resource(fctx->davdb, fctx->mbentry, tgt.resource, (void **) &ddata, 0); ddata->resource = tgt.resource; /* XXX Check errors */ fctx->proc_by_resource(fctx, ddata); rparams->davdb.close_db(fctx->davdb); next: /* XXX - split this into a req_tgt cleanup */ free(tgt.userid); mboxlist_entry_free(&tgt.mbentry); } } /* End XML response */ xml_partial_response(txn, fctx->root->doc, NULL /* end */, 0, &fctx->xmlbuf); xmlBufferFree(fctx->xmlbuf); /* End of output */ write_body(0, txn, NULL, 0); mailbox_close(&mailbox); return ret; } struct updates_rock { struct propfind_ctx *fctx; get_modseq_t get_modseq; uint32_t limit; modseq_t syncmodseq; modseq_t basemodseq; modseq_t *respmodseq; uint32_t *nresp; }; static int updates_cb(void *rock, void *data) { struct dav_data *ddata = (struct dav_data *) data; struct updates_rock *urock = (struct updates_rock *) rock; struct propfind_ctx *fctx = urock->fctx; modseq_t modseq = urock->get_modseq(fctx->mailbox, data, fctx->userid); if (!ddata->alive) { if (modseq <= urock->basemodseq) { /* Initial sync - ignore unmapped resources */ return 0; } /* Report resource as NOT FOUND IMAP UID of 0 will cause index record to be ignored propfind_by_resource() will append our resource name */ ddata->imap_uid = 0; } else if (modseq <= urock->syncmodseq) { /* Per-user modseq hasn't changed */ return 0; } if (*urock->nresp >= urock->limit) { /* Number of responses has reached client-specified limit */ return HTTP_NO_STORAGE; } else { /* Bump response count */ *urock->nresp += 1; } /* respmodseq will be highest modseq of the resources we return */ *(urock->respmodseq) = MAX(modseq, *(urock->respmodseq)); /* Add element for this resource to root */ fctx->proc_by_resource(fctx, ddata); fctx->record = NULL; return 0; } /* DAV:sync-collection REPORT */ int report_sync_col(struct transaction_t *txn, struct meth_params *rparams, xmlNodePtr inroot, struct propfind_ctx *fctx) { int ret = 0, r; struct mailbox *mailbox = NULL; uint32_t uidvalidity = 0; modseq_t syncmodseq = 0; modseq_t basemodseq = 0; modseq_t highestmodseq = 0; modseq_t respmodseq = 0; uint32_t limit = UINT32_MAX - 1; uint32_t nresp = 0; xmlNodePtr node; char tokenuri[MAX_MAILBOX_PATH+1]; /* XXX Handle Depth (cal-home-set at toplevel) */ /* Open mailbox for reading */ r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } fctx->mbentry = txn->req_tgt.mbentry; fctx->mailbox = mailbox; highestmodseq = mailbox->i.highestmodseq; /* Parse children element of report */ for (node = inroot->children; node; node = node->next) { xmlNodePtr node2; xmlChar *str = NULL; if (node->type == XML_ELEMENT_NODE) { if (!xmlStrcmp(node->name, BAD_CAST "sync-token") && (str = xmlNodeListGetString(inroot->doc, node->children, 1))) { /* Add sync-token to our header cache */ spool_cache_header(xstrdup(":token"), xstrdup((const char *) str), txn->req_hdrs); /* Parse sync-token */ r = sscanf((char *) str, SYNC_TOKEN_URL_SCHEME "%u-" MODSEQ_FMT "-" MODSEQ_FMT "%1s", &uidvalidity, &syncmodseq, &basemodseq, tokenuri /* test for trailing junk */); syslog(LOG_DEBUG, "scanned token %s to %d %u %llu %llu", str, r, uidvalidity, syncmodseq, basemodseq); /* Sanity check the token components */ if (r < 2 || r > 3 || (uidvalidity != mailbox->i.uidvalidity) || (syncmodseq > highestmodseq)) { fctx->txn->error.desc = "Invalid sync-token"; } else if (r == 3) { /* Previous partial read token */ if (basemodseq > highestmodseq) { fctx->txn->error.desc = "Invalid sync-token"; } else if (basemodseq < mailbox->i.deletedmodseq) { fctx->txn->error.desc = "Stale sync-token"; } } else { /* Regular token */ if (syncmodseq < mailbox->i.deletedmodseq) { fctx->txn->error.desc = "Stale sync-token"; } } if (fctx->txn->error.desc) { /* DAV:valid-sync-token */ txn->error.precond = DAV_SYNC_TOKEN; ret = HTTP_FORBIDDEN; } } else if (!xmlStrcmp(node->name, BAD_CAST "sync-level") && (str = xmlNodeListGetString(inroot->doc, node->children, 1))) { if (!strcmp((char *) str, "infinity")) { fctx->txn->error.desc = "This server DOES NOT support infinite depth requests"; ret = HTTP_SERVER_ERROR; } else if ((sscanf((char *) str, "%u", &fctx->depth) != 1) || (fctx->depth != 1)) { fctx->txn->error.desc = "Illegal sync-level"; ret = HTTP_BAD_REQUEST; } } else if (!xmlStrcmp(node->name, BAD_CAST "limit")) { errno = 0; for (node2 = node->children; node2; node2 = node2->next) { if ((node2->type == XML_ELEMENT_NODE) && !xmlStrcmp(node2->name, BAD_CAST "nresults") && (!(str = xmlNodeListGetString(inroot->doc, node2->children, 1)) || (sscanf((char *) str, "%u", &limit) != 1) || (errno != 0) || (limit >= UINT32_MAX))) { txn->error.precond = DAV_OVER_LIMIT; ret = HTTP_FORBIDDEN; } } } if (str) xmlFree(str); if (ret) goto done; } } /* Check Depth */ if (!fctx->depth) { fctx->txn->error.desc = "Illegal sync-level"; ret = HTTP_BAD_REQUEST; goto done; } if (!syncmodseq) { /* Initial sync - set basemodseq in case client limits results */ basemodseq = highestmodseq; } /* Open the DAV DB corresponding to the mailbox */ fctx->davdb = rparams->davdb.open_db(fctx->mailbox); /* Setup for chunked response */ txn->flags.te |= TE_CHUNKED; /* Begin XML response */ xml_response(HTTP_MULTI_STATUS, txn, fctx->root->doc); /* Report the resources within the client requested limit (if any) */ struct updates_rock rock = { fctx, rparams->get_modseq, limit, syncmodseq, basemodseq, &respmodseq, &nresp }; r = rparams->davdb.foreach_update(fctx->davdb, syncmodseq, fctx->mbentry, -1 /* ALL kinds of resources */, (syncmodseq && basemodseq) ? 0 : limit + 1, &updates_cb, &rock); if (r) { /* Tell client we truncated the responses */ if (fctx->req_tgt->resource) *(fctx->req_tgt->resource) = '\0'; xml_add_response(fctx, HTTP_NO_STORAGE, DAV_OVER_LIMIT, NULL, NULL); } else { /* Full response - respmodseq will be highestmodseq of mailbox */ respmodseq = highestmodseq; } if (fctx->davdb) rparams->davdb.close_db(fctx->davdb); /* Add sync-token element to root */ if (respmodseq < basemodseq) { /* Client limited results of initial sync - include basemodseq */ snprintf(tokenuri, MAX_MAILBOX_PATH, SYNC_TOKEN_URL_SCHEME "%u-" MODSEQ_FMT "-" MODSEQ_FMT, mailbox->i.uidvalidity, respmodseq, basemodseq); } else { snprintf(tokenuri, MAX_MAILBOX_PATH, SYNC_TOKEN_URL_SCHEME "%u-" MODSEQ_FMT, mailbox->i.uidvalidity, respmodseq); } node = xmlNewChild(fctx->root, NULL, BAD_CAST "sync-token", BAD_CAST tokenuri); /* Add sync-token element to output buffer */ xml_partial_response(NULL /* !output */, fctx->root->doc, node, 1, &fctx->xmlbuf); /* End XML response */ xml_partial_response(txn, fctx->root->doc, NULL /* end */, 0, &fctx->xmlbuf); xmlBufferFree(fctx->xmlbuf); /* End of output */ write_body(0, txn, NULL, 0); done: mailbox_close(&mailbox); return ret; } int expand_property(xmlNodePtr inroot, struct propfind_ctx *fctx, struct namespace_t *namespace, const char *href, parse_path_t parse_path, const struct prop_entry *lprops, xmlNodePtr root, int depth) { int ret = 0, r; struct propfind_ctx prev_ctx; struct request_target_t req_tgt; memcpy(&prev_ctx, fctx, sizeof(struct propfind_ctx)); memset(&req_tgt, 0, sizeof(struct request_target_t)); fctx->mode = PROPFIND_EXPAND; fctx->prefer &= ~PREFER_NOROOT; if (href) { /* Parse the URL */ req_tgt.namespace = namespace; parse_path(href, &req_tgt, &fctx->txn->error.desc); fctx->req_tgt = &req_tgt; } fctx->lprops = lprops; fctx->elist = NULL; fctx->root = root; fctx->depth = depth; fctx->mbentry = NULL; fctx->mailbox = NULL; ret = preload_proplist(inroot->children, fctx); if (ret) goto done; if (!fctx->req_tgt->collection && !fctx->depth) { /* Add response for principal or home-set collection */ struct mailbox *mailbox = NULL; if (fctx->req_tgt->mbentry) { /* Open mailbox for reading */ if ((r = mailbox_open_irl(fctx->req_tgt->mbentry->name, &mailbox))) { syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s", fctx->req_tgt->mbentry->name, error_message(r)); fctx->txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } fctx->mbentry = fctx->req_tgt->mbentry; fctx->mailbox = mailbox; } xml_add_response(fctx, 0, 0, NULL, NULL); mailbox_close(&mailbox); } if (fctx->depth > 0) { /* Collection(s) */ if (fctx->req_tgt->collection) { /* Add response for target collection */ propfind_by_collection(fctx->req_tgt->mbentry, fctx); } else { /* Add responses for all contained collections */ mboxlist_mboxtree(fctx->req_tgt->mbentry->name, propfind_by_collection, fctx, MBOXTREE_SKIP_ROOT); switch (fctx->req_tgt->namespace->id) { case URL_NS_CALENDAR: case URL_NS_ADDRESSBOOK: /* Add responses for shared collections */ mboxlist_usersubs(fctx->req_tgt->userid, propfind_by_collection, fctx, MBOXTREE_SKIP_PERSONAL); break; } } if (fctx->davdb) fctx->close_db(fctx->davdb); ret = *fctx->ret; } done: /* Free the entry list */ free_entry_list(fctx->elist); free(req_tgt.userid); fctx->mbentry = prev_ctx.mbentry; fctx->mailbox = prev_ctx.mailbox; fctx->depth = prev_ctx.depth; fctx->root = prev_ctx.root; fctx->elist = prev_ctx.elist; fctx->lprops = prev_ctx.lprops; fctx->req_tgt = prev_ctx.req_tgt; fctx->prefer = prev_ctx.prefer; if (root != fctx->root) { /* Move any defined namespaces up to the previous parent */ xmlNsPtr nsDef; if (fctx->root->nsDef) { /* Find last nsDef in list */ for (nsDef = fctx->root->nsDef; nsDef->next; nsDef = nsDef->next); nsDef->next = root->nsDef; } else fctx->root->nsDef = root->nsDef; root->nsDef = NULL; } return ret; } /* DAV:expand-property REPORT */ int report_expand_prop(struct transaction_t *txn __attribute__((unused)), struct meth_params *rparams __attribute__((unused)), xmlNodePtr inroot, struct propfind_ctx *fctx) { int ret = expand_property(inroot, fctx, NULL, NULL, NULL, fctx->lprops, fctx->root, fctx->depth); return (ret ? ret : HTTP_MULTI_STATUS); } /* DAV:acl-principal-prop-set REPORT */ int report_acl_prin_prop(struct transaction_t *txn __attribute__((unused)), struct meth_params *rparams __attribute__((unused)), xmlNodePtr inroot __attribute__((unused)), struct propfind_ctx *fctx) { int ret = 0; struct request_target_t req_tgt; mbentry_t *mbentry = fctx->req_tgt->mbentry; char *userid, *nextid; /* Generate URL for user principal collection */ buf_reset(&fctx->buf); buf_printf(&fctx->buf, "%s/%s/", namespace_principal.prefix, USER_COLLECTION_PREFIX); /* Allowed properties are for principals, NOT the request URL */ memset(&req_tgt, 0, sizeof(struct request_target_t)); principal_parse_path(buf_cstring(&fctx->buf), &req_tgt, &fctx->txn->error.desc); fctx->req_tgt = &req_tgt; fctx->lprops = principal_props; fctx->proc_by_resource = &propfind_by_resource; /* Parse the ACL string (userid/rights pairs) */ for (userid = mbentry->acl; userid; userid = nextid) { char *rightstr; rightstr = strchr(userid, '\t'); if (!rightstr) break; *rightstr++ = '\0'; nextid = strchr(rightstr, '\t'); if (!nextid) break; *nextid++ = '\0'; if (strcmp(userid, "anyone") && strcmp(userid, "anonymous")) { /* Add userid to principal URL */ strcpy(req_tgt.tail, userid); req_tgt.userid = xstrdup(userid); /* Add response for URL */ xml_add_response(fctx, 0, 0, NULL, NULL); free(req_tgt.userid); } } return (ret ? ret : HTTP_MULTI_STATUS); } struct search_crit { struct strlist *props; xmlChar *match; struct search_crit *next; }; /* mboxlist_alluser() callback to find user principals (has Inbox) */ static int principal_search(const char *userid, void *rock) { struct propfind_ctx *fctx = (struct propfind_ctx *) rock; struct search_crit *search_crit; size_t len; char *p; /* XXX - this function needs extradomain and virtdomains support */ /* Check ACL for current user */ if (principal_acl_check(userid, httpd_authstate)) return 0; /* Check against search criteria */ for (search_crit = (struct search_crit *) fctx->filter_crit; search_crit; search_crit = search_crit->next) { struct strlist *prop; for (prop = search_crit->props; prop; prop = prop->next) { if (!strcmp(prop->s, "displayname")) { if (!xmlStrcasestr(BAD_CAST userid, search_crit->match)) return 0; } else if (!strcmp(prop->s, "calendar-user-address-set")) { char email[MAX_MAILBOX_NAME+1]; snprintf(email, MAX_MAILBOX_NAME, "%s@%s", userid, config_servername); if (!xmlStrcasestr(BAD_CAST email, search_crit->match)) return 0; } else if (!strcmp(prop->s, "calendar-user-type")) { if (!xmlStrcasestr(BAD_CAST "INDIVIDUAL", search_crit->match)) return 0; } } } /* Append principal name to URL path */ len = strlen(namespace_principal.prefix); p = fctx->req_tgt->path + len; snprintf(p, MAX_MAILBOX_PATH - len, "/%s/%s/", USER_COLLECTION_PREFIX, userid); free(fctx->req_tgt->userid); fctx->req_tgt->userid = xstrdup(userid); return xml_add_response(fctx, 0, 0, NULL, NULL); } static const struct prop_entry prin_search_props[] = { /* WebDAV (RFC 4918) properties */ { "displayname", NS_DAV, 0, NULL, NULL, NULL }, /* CalDAV Scheduling (RFC 6638) properties */ { "calendar-user-address-set", NS_CALDAV, 0, NULL, NULL, NULL }, { "calendar-user-type", NS_CALDAV, 0, NULL, NULL, NULL }, { NULL, 0, 0, NULL, NULL, NULL } }; /* DAV:principal-property-search REPORT */ static int report_prin_prop_search(struct transaction_t *txn, struct meth_params *rparams __attribute__((unused)), xmlNodePtr inroot, struct propfind_ctx *fctx) { int ret = 0; xmlNodePtr node; struct search_crit *search_crit, *next; unsigned apply_prin_set = 0; /* Parse children element of report */ fctx->filter_crit = NULL; for (node = inroot->children; node; node = node->next) { if (node->type == XML_ELEMENT_NODE) { if (!xmlStrcmp(node->name, BAD_CAST "property-search")) { xmlNodePtr search; search_crit = xzmalloc(sizeof(struct search_crit)); search_crit->next = fctx->filter_crit; fctx->filter_crit = search_crit; for (search = node->children; search; search = search->next) { if (search->type == XML_ELEMENT_NODE) { if (!xmlStrcmp(search->name, BAD_CAST "prop")) { xmlNodePtr prop; for (prop = search->children; prop; prop = prop->next) { if (prop->type == XML_ELEMENT_NODE) { const struct prop_entry *entry; for (entry = prin_search_props; entry->name && (xmlStrcmp(fctx->ns[entry->ns]->href, prop->ns->href) || xmlStrcmp(prop->name, BAD_CAST entry->name)); entry++); if (!entry->name) { txn->error.desc = "Unsupported XML search prop"; ret = HTTP_BAD_REQUEST; goto done; } else { appendstrlist(&search_crit->props, (char *) entry->name); } } } } else if (!xmlStrcmp(search->name, BAD_CAST "match")) { if (search_crit->match) { txn->error.desc = "Too many DAV:match XML elements"; ret = HTTP_BAD_REQUEST; goto done; } search_crit->match = xmlNodeListGetString(inroot->doc, search->children, 1); } else { txn->error.desc = "Unknown XML element"; ret = HTTP_BAD_REQUEST; goto done; } } } if (!search_crit->props) { txn->error.desc = "Missing DAV:prop XML element"; ret = HTTP_BAD_REQUEST; goto done; } if (!search_crit->match) { txn->error.desc = "Missing DAV:match XML element"; ret = HTTP_BAD_REQUEST; goto done; } } else if (!xmlStrcmp(node->name, BAD_CAST "prop")) { /* Already parsed in meth_report() */ } else if (!xmlStrcmp(node->name, BAD_CAST "apply-to-principal-collection-set")) { apply_prin_set = 1; } else { txn->error.desc = "Unknown XML element"; ret = HTTP_BAD_REQUEST; goto done; } } } if (!fctx->filter_crit) { txn->error.desc = "Missing DAV:property-search XML element"; ret = HTTP_BAD_REQUEST; goto done; } /* Only search DAV:principal-collection-set */ if (apply_prin_set || !fctx->req_tgt->userid) { /* XXX Do LDAP/SQL lookup of CN/email-address(es) here */ ret = mboxlist_alluser(principal_search, fctx); } done: for (search_crit = fctx->filter_crit; search_crit; search_crit = next) { next = search_crit->next; if (search_crit->match) xmlFree(search_crit->match); freestrlist(search_crit->props); free(search_crit); } return (ret ? ret : HTTP_MULTI_STATUS); } /* DAV:principal-search-property-set REPORT */ static int report_prin_search_prop_set(struct transaction_t *txn, struct meth_params *rparams __attribute__((unused)), xmlNodePtr inroot, struct propfind_ctx *fctx) { xmlNodePtr node; const struct prop_entry *entry; /* Look for child elements in request */ for (node = inroot->children; node; node = node->next) { if (node->type == XML_ELEMENT_NODE) { txn->error.desc = "DAV:principal-search-property-set XML element MUST be empty"; return HTTP_BAD_REQUEST; } } for (entry = prin_search_props; entry->name; entry++) { node = xmlNewChild(fctx->root, NULL, BAD_CAST "principal-search-property", NULL); node = xmlNewChild(node, NULL, BAD_CAST "prop", NULL); ensure_ns(fctx->ns, entry->ns, fctx->root, known_namespaces[entry->ns].href, known_namespaces[entry->ns].prefix); xmlNewChild(node, fctx->ns[entry->ns], BAD_CAST entry->name, NULL); } return HTTP_OK; } /* Perform a REPORT request */ int meth_report(struct transaction_t *txn, void *params) { struct meth_params *rparams = (struct meth_params *) params; int ret = 0, r; const char **hdr; unsigned depth = 0; xmlNodePtr inroot = NULL, outroot = NULL, cur, prop = NULL, props = NULL; const struct report_type_t *report = NULL; xmlNsPtr ns[NUM_NAMESPACE]; struct hash_table ns_table = HASH_TABLE_INITIALIZER; struct propfind_ctx fctx; memset(&fctx, 0, sizeof(struct propfind_ctx)); /* Parse the path */ r = dav_parse_req_target(txn, rparams); if (r) return r; /* Make sure method is allowed */ if (!(txn->req_tgt.allow & ALLOW_DAV)) return HTTP_NOT_ALLOWED; /* Check Depth */ if ((hdr = spool_getheader(txn->req_hdrs, "Depth"))) { if (!strcmp(hdr[0], "infinity")) { depth = 2; } else if ((sscanf(hdr[0], "%u", &depth) != 1) || (depth > 1)) { txn->error.desc = "Illegal Depth value"; return HTTP_BAD_REQUEST; } } /* Parse the REPORT body */ ret = parse_xml_body(txn, &inroot, NULL); if (!ret && !inroot) { txn->error.desc = "Missing request body"; return HTTP_BAD_REQUEST; } if (ret) goto done; /* Add report type to our header cache */ spool_cache_header(xstrdup(":type"), xstrdup((const char *) inroot->name), txn->req_hdrs); /* Check the report type against our supported list */ for (report = rparams->reports; report && report->name; report++) { if (inroot->ns && !xmlStrcmp(inroot->ns->href, BAD_CAST known_namespaces[report->ns].href) && !xmlStrcmp(inroot->name, BAD_CAST report->name)) break; } if (!report || !report->name) { syslog(LOG_WARNING, "REPORT %s", inroot->name); /* DAV:supported-report */ txn->error.precond = DAV_SUPP_REPORT; ret = HTTP_FORBIDDEN; goto done; } /* Check any depth limit */ if (depth && (report->flags & REPORT_DEPTH_ZERO)) { txn->error.desc = "Depth header field MUST have value zero (0)"; ret = HTTP_BAD_REQUEST; goto done; } /* Normalize depth so that: * 0 = home-set collection, 1+ = calendar collection, 2+ = calendar resource */ if (txn->req_tgt.collection) depth++; if (txn->req_tgt.resource) depth++; /* Check ACL and location of mailbox */ if (report->flags & REPORT_NEED_MBOX) { int rights; /* Check ACL for current user */ rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if ((rights & report->reqd_privs) != report->reqd_privs) { if (report->reqd_privs == DACL_READFB) ret = HTTP_NOT_FOUND; else { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = report->reqd_privs; ret = HTTP_NO_PRIVS; } goto done; } if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) ret = HTTP_UNAVAILABLE; else ret = http_pipe_req_resp(be, txn); goto done; } /* Local Mailbox */ } /* Principal or Local Mailbox */ if (report->flags & (REPORT_NEED_PROPS | REPORT_ALLOW_PROPS)) { /* Parse children element of report */ for (cur = inroot->children; cur; cur = cur->next) { unsigned mode = PROPFIND_NONE; if (cur->type == XML_ELEMENT_NODE) { if (!xmlStrcmp(cur->name, BAD_CAST "allprop")) { mode = PROPFIND_ALL; prop = cur; } else if (!xmlStrcmp(cur->name, BAD_CAST "propname")) { mode = PROPFIND_NAME; fctx.prefer = PREFER_MIN; /* Don't want 404 (Not Found) */ prop = cur; } else if (!xmlStrcmp(cur->name, BAD_CAST "prop")) { mode = PROPFIND_PROP; prop = cur; props = cur->children; } } if (mode != PROPFIND_NONE) { if (fctx.mode != PROPFIND_NONE) { txn->error.desc = "Multiple <*prop*> elements in REPORT"; ret = HTTP_BAD_REQUEST; goto done; } fctx.mode = mode; } } if (!prop && (report->flags & REPORT_NEED_PROPS)) { txn->error.desc = "Missing element in REPORT"; ret = HTTP_BAD_REQUEST; goto done; } } /* Start construction of our multistatus response */ if (report->resp_root && !(outroot = init_xml_response(report->resp_root, NS_DAV, inroot, ns))) { txn->error.desc = "Unable to create XML response"; ret = HTTP_SERVER_ERROR; goto done; } /* Populate our propfind context */ fctx.txn = txn; fctx.req_tgt = &txn->req_tgt; fctx.depth = depth; fctx.prefer |= get_preferences(txn); fctx.userid = httpd_userid; fctx.userisadmin = httpd_userisadmin; fctx.authstate = httpd_authstate; fctx.mbentry = NULL; fctx.mailbox = NULL; fctx.record = NULL; fctx.get_validators = rparams->get_validators; fctx.reqd_privs = report->reqd_privs; if (rparams->mime_types) fctx.free_obj = rparams->mime_types[0].free; fctx.proc_by_resource = &propfind_by_resource; fctx.elist = NULL; fctx.lprops = rparams->propfind.lprops; fctx.root = outroot; fctx.ns = ns; fctx.ns_table = &ns_table; fctx.ret = &ret; /* Parse the list of properties and build a list of callbacks */ if (fctx.mode) { ret = preload_proplist(props, &fctx); /* iCalendar/vCard data in response should not be transformed */ if (fctx.flags.fetcheddata) txn->flags.cc |= CC_NOTRANSFORM; } /* Process the requested report */ if (!ret) ret = (*report->proc)(txn, rparams, inroot, &fctx); // might have made a change! sync_checkpoint(txn->conn->pin); /* Output the XML response */ if (outroot) { switch (ret) { case HTTP_OK: case HTTP_MULTI_STATUS: /* iCalendar/vCard data in response should not be transformed */ if (fctx.flags.fetcheddata) txn->flags.cc |= CC_NOTRANSFORM; xml_response(ret, txn, outroot->doc); ret = 0; break; default: break; } } done: /* Free the entry list */ free_entry_list(fctx.elist); buf_free(&fctx.buf); free_hash_table(&ns_table, NULL); if (inroot) xmlFreeDoc(inroot->doc); if (outroot) xmlFreeDoc(outroot->doc); return ret; } /* Perform a UNLOCK request * * preconditions: * DAV:need-privileges * DAV:lock-token-matches-request-uri */ int meth_unlock(struct transaction_t *txn, void *params) { struct meth_params *lparams = (struct meth_params *) params; int ret = HTTP_NO_CONTENT, r, precond; const char **hdr, *token; struct mailbox *mailbox = NULL; struct dav_data *ddata; struct index_record record; const char *etag; time_t lastmod; size_t len; void *davdb = NULL; /* Response should not be cached */ txn->flags.cc |= CC_NOCACHE; /* Parse the path */ r = dav_parse_req_target(txn, lparams); if (r) return r; /* Make sure method is allowed (only allowed on resources) */ if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED; /* Check for mandatory Lock-Token header */ if (!(hdr = spool_getheader(txn->req_hdrs, "Lock-Token"))) { txn->error.desc = "Missing Lock-Token header"; return HTTP_BAD_REQUEST; } token = hdr[0]; if (txn->req_tgt.mbentry->server) { /* Remote mailbox */ struct backend *be; be = proxy_findserver(txn->req_tgt.mbentry->server, &http_protocol, httpd_userid, &backend_cached, NULL, NULL, httpd_in); if (!be) return HTTP_UNAVAILABLE; return http_pipe_req_resp(be, txn); } /* Local Mailbox */ /* Open mailbox for reading */ r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", txn->req_tgt.mbentry->name, error_message(r)); txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } /* Open the DAV DB corresponding to the mailbox */ davdb = lparams->davdb.open_db(mailbox); lparams->davdb.begin_transaction(davdb); /* Find message UID for the resource, if exists */ lparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry, txn->req_tgt.resource, (void **) &ddata, 0); if (!ddata->rowid) { ret = HTTP_NOT_FOUND; goto done; } /* Check if resource is locked */ if (ddata->lock_expire <= time(NULL)) { /* DAV:lock-token-matches-request-uri */ txn->error.precond = DAV_BAD_LOCK_TOKEN; ret = HTTP_CONFLICT; goto done; } /* Check if current user owns the lock */ if (strcmp(ddata->lock_ownerid, httpd_userid)) { /* Check ACL for current user */ int rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry); if (!(rights & DACL_ADMIN)) { /* DAV:need-privileges */ txn->error.precond = DAV_NEED_PRIVS; txn->error.resource = txn->req_tgt.path; txn->error.rights = DACL_ADMIN; ret = HTTP_NO_PRIVS; goto done; } } /* Check if lock token matches */ len = strlen(ddata->lock_token); if (token[0] != '<' || strlen(token) != len+2 || token[len+1] != '>' || strncmp(token+1, ddata->lock_token, len)) { /* DAV:lock-token-matches-request-uri */ txn->error.precond = DAV_BAD_LOCK_TOKEN; ret = HTTP_CONFLICT; goto done; } /* Fetch resource validators */ r = lparams->get_validators(mailbox, (void *) ddata, httpd_userid, &record, &etag, &lastmod); if (r) { txn->error.desc = error_message(r); ret = HTTP_SERVER_ERROR; goto done; } /* Check any preconditions */ precond = lparams->check_precond(txn, params, mailbox, (void *) ddata, etag, lastmod); if (precond != HTTP_OK) { /* We failed a precondition - don't perform the request */ ret = precond; goto done; } if (ddata->imap_uid) { /* Mapped URL - Remove the lock */ ddata->lock_token = NULL; ddata->lock_owner = NULL; ddata->lock_ownerid = NULL; ddata->lock_expire = 0; lparams->davdb.write_resourceLOCKONLY(davdb, ddata); } else { /* Unmapped URL - Treat as lock-null and delete mapping entry */ lparams->davdb.delete_resourceLOCKONLY(davdb, ddata->rowid); } done: if (davdb) { /* XXX error handling abort */ lparams->davdb.commit_transaction(davdb); lparams->davdb.close_db(davdb); } mailbox_close(&mailbox); return ret; } static void my_dav_init(struct buf *serverinfo) { time_t compile_time = calc_compile_time(__TIME__, __DATE__); struct stat sbuf; struct message_guid guid; xmlNodePtr root, node, apps, app; xmlNsPtr ns[NUM_NAMESPACE]; buf_printf(serverinfo, " SQLite/%s", sqlite3_libversion()); /* Generate token based on compile date/time of this source file, the number of available RSCALEs and the config file size/mtime */ stat(config_filename, &sbuf); server_info_lastmod = MAX(compile_time, sbuf.st_mtime); buf_printf(&server_info_token, TIME_T_FMT "-" TIME_T_FMT "-" OFF_T_FMT, compile_time, sbuf.st_mtime, sbuf.st_size); message_guid_generate(&guid, buf_cstring(&server_info_token), buf_len(&server_info_token)); buf_setcstr(&server_info_token, message_guid_encode(&guid)); /* Generate link header contents */ buf_printf(&server_info_link, "<%s/%s>; rel=\"server-info\"; token=\"%s\"", namespace_principal.prefix, SERVER_INFO, buf_cstring(&server_info_token)); /* Start construction of our server-info */ if (!(root = init_xml_response("server-info", NS_DAV, NULL, ns))) { syslog(LOG_ERR, "Unable to create server-info XML"); return; } /* Add token */ xmlNewTextChild(root, ns[NS_DAV], BAD_CAST "token", BAD_CAST buf_cstring(&server_info_token)); /* Add server */ if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) { node = xmlNewChild(root, NULL, BAD_CAST "server", NULL); xmlNewChild(node, ns[NS_DAV], BAD_CAST "name", BAD_CAST "Cyrus-HTTP"); xmlNewTextChild(node, ns[NS_DAV], BAD_CAST "version", BAD_CAST CYRUS_VERSION); } /* Add global DAV features */ node = xmlNewChild(root, NULL, BAD_CAST "features", NULL); xmlNewChild(node, ns[NS_DAV], BAD_CAST "class-1", NULL); xmlNewChild(node, ns[NS_DAV], BAD_CAST "class-2", NULL); xmlNewChild(node, ns[NS_DAV], BAD_CAST "class-3", NULL); xmlNewChild(node, ns[NS_DAV], BAD_CAST "access-control", NULL); xmlNewChild(node, ns[NS_DAV], BAD_CAST "extended-mkcol", NULL); xmlNewChild(node, ns[NS_DAV], BAD_CAST "quota", NULL); xmlNewChild(node, ns[NS_DAV], BAD_CAST "sync-collection", NULL); xmlNewChild(node, ns[NS_DAV], BAD_CAST "add-member", NULL); apps = xmlNewChild(root, NULL, BAD_CAST "applications", NULL); if (namespace_calendar.enabled) { app = xmlNewChild(apps, NULL, BAD_CAST "application", NULL); ensure_ns(ns, NS_CALDAV, app, XML_NS_CALDAV, "C"); xmlNewChild(app, NULL, BAD_CAST "name", BAD_CAST "caldav"); /* Add CalDAV features */ node = xmlNewChild(app, NULL, BAD_CAST "features", NULL); xmlNewChild(node, ns[NS_CALDAV], BAD_CAST "calendar-access", NULL); if (namespace_calendar.allow & ALLOW_CAL_SCHED) xmlNewChild(node, ns[NS_CALDAV], BAD_CAST "calendar-auto-schedule", NULL); if (namespace_calendar.allow & ALLOW_CAL_NOTZ) xmlNewChild(node, ns[NS_CALDAV], BAD_CAST "calendar-no-timezone", NULL); if (namespace_calendar.allow & ALLOW_CAL_AVAIL) xmlNewChild(node, ns[NS_CALDAV], BAD_CAST "calendar-availability", NULL); if (namespace_calendar.allow & ALLOW_CAL_ATTACH) { xmlNewChild(node, ns[NS_CALDAV], BAD_CAST "calendar-managed-attachments", NULL); xmlNewChild(node, ns[NS_CALDAV], BAD_CAST "calendar-managed-attachments-no-recurrence", NULL); } } if (namespace_addressbook.enabled) { app = xmlNewChild(apps, NULL, BAD_CAST "application", NULL); ensure_ns(ns, NS_CARDDAV, app, XML_NS_CARDDAV, "A"); xmlNewChild(app, NULL, BAD_CAST "name", BAD_CAST "carddav"); /* Add CardDAV features */ node = xmlNewChild(app, NULL, BAD_CAST "features", NULL); xmlNewChild(node, ns[NS_CARDDAV], BAD_CAST "addressbook", NULL); } /* Dump XML response tree into a text buffer */ xmlDocDumpFormatMemoryEnc(root->doc, &server_info, &server_info_size, "utf-8", 1); xmlFreeDoc(root->doc); if (!server_info) { syslog(LOG_ERR, "Unable to dump server-info XML tree"); } return; } static void my_dav_shutdown(void) { if (server_info) xmlFree(server_info); buf_free(&server_info_token); buf_free(&server_info_link); } static int get_server_info(struct transaction_t *txn) { int precond; const char **hdr, *etag; if (!server_info) return HTTP_NOT_FOUND; if (!httpd_userid) return HTTP_UNAUTHORIZED; if ((hdr = spool_getheader(txn->req_hdrs, "Accept")) && strcmp(hdr[0], "application/server-info+xml")) return HTTP_NOT_ACCEPTABLE; /* Check any preconditions, including range request */ txn->flags.ranges = 1; etag = buf_cstring(&server_info_token); precond = check_precond(txn, etag, server_info_lastmod); switch (precond) { case HTTP_OK: case HTTP_PARTIAL: case HTTP_NOT_MODIFIED: /* Fill in Etag, Last-Modified, and Expires */ txn->resp_body.etag = etag; txn->resp_body.lastmod = server_info_lastmod; txn->resp_body.maxage = 86400; /* 24 hrs */ txn->flags.cc |= CC_MAXAGE; if (precond != HTTP_NOT_MODIFIED) break; GCC_FALLTHROUGH default: /* We failed a precondition - don't perform the request */ return precond; } /* Output the XML response */ txn->resp_body.type = "application/server-info+xml; charset=utf-8"; write_body(precond, txn, (char *) server_info, server_info_size); return 0; } static void dav_parse_textmatch(xmlNodePtr node, struct text_match_t **match, struct filter_profile_t *profile, struct error_t *error) { unsigned negate = 0; unsigned type = MATCH_TYPE_CONTAINS; unsigned collation = profile->collation; xmlChar *attr; *match = NULL; attr = xmlGetProp(node, BAD_CAST "negate-condition"); if (attr) { if (!xmlStrcmp(attr, BAD_CAST "yes")) negate = 1; else if (xmlStrcmp(attr, BAD_CAST "no")) { error->precond = profile->filter_precond; error->desc = "negate-condition is a yes/no option"; error->node = xmlCopyNode(node->parent, 1); } xmlFree(attr); } if (!error->precond) { attr = xmlGetProp(node, BAD_CAST "match-type"); if (attr) { const struct match_type_t *match; for (match = dav_match_types; match->name; match++) { if (!xmlStrcmp(attr, BAD_CAST match->name)) break; } if (match->name) type = match->value; else { error->precond = profile->filter_precond; error->desc = "Unsupported match-type"; error->node = xmlCopyNode(node->parent, 1); } xmlFree(attr); } } if (!error->precond) { attr = xmlGetProp(node, BAD_CAST "collation"); if (attr) { if (xmlStrcmp(attr, BAD_CAST "default")) { const struct collation_t *col; for (col = dav_collations; col->name; col++) { if (!xmlStrcmp(attr, BAD_CAST col->name)) break; } if (col->name) collation = col->value; else error->precond = profile->collation_precond; } xmlFree(attr); } } if (!error->precond) { *match = xzmalloc(sizeof(struct text_match_t)); (*match)->text = xmlNodeGetContent(node); (*match)->negate = negate; (*match)->type = type; (*match)->collation = collation; } } static void dav_parse_paramfilter(xmlNodePtr root, struct param_filter **param, struct filter_profile_t *profile, struct error_t *error) { xmlChar *attr; xmlNodePtr node; attr = xmlGetProp(root, BAD_CAST "name"); if (!attr) { error->precond = profile->filter_precond; error->desc = "Missing 'name' attribute"; error->node = xmlCopyNode(root, 2); } else { *param = xzmalloc(sizeof(struct param_filter)); (*param)->name = attr; if (profile->param_string_to_kind) { (*param)->kind = profile->param_string_to_kind((const char *) attr); if ((*param)->kind == profile->no_param_value) { error->precond = profile->filter_precond; error->desc = "Unsupported parameter"; error->node = xmlCopyNode(root, 2); } } } for (node = xmlFirstElementChild(root); node && !error->precond; node = xmlNextElementSibling(node)) { if ((*param)->not_defined) { error->precond = profile->filter_precond; error->desc = DAV_FILTER_ISNOTDEF_ERR; error->node = xmlCopyNode(root, 1); } else if (!xmlStrcmp(node->name, BAD_CAST "is-not-defined")) { if ((*param)->match) { error->precond = profile->filter_precond; error->desc = DAV_FILTER_ISNOTDEF_ERR; error->node = xmlCopyNode(root, 1); } else (*param)->not_defined = 1; } else if (!xmlStrcmp(node->name, BAD_CAST "text-match")) { if ((*param)->match) { error->precond = profile->filter_precond; error->desc = "Multiple text-match"; error->node = xmlCopyNode(root, 1); } else { struct text_match_t *match = NULL; dav_parse_textmatch(node, &match, profile, error); (*param)->match = match; } } else { error->precond = profile->filter_precond; error->desc = "Unsupported element in param-filter"; error->node = xmlCopyNode(root, 1); } } } void dav_parse_propfilter(xmlNodePtr root, struct prop_filter **prop, struct filter_profile_t *profile, struct error_t *error) { xmlChar *attr; xmlNodePtr node; attr = xmlGetProp(root, BAD_CAST "name"); if (!attr) { error->precond = profile->filter_precond; error->desc = "Missing 'name' attribute"; error->node = xmlCopyNode(root, 2); } else { *prop = xzmalloc(sizeof(struct prop_filter)); (*prop)->name = attr; (*prop)->allof = profile->allof; if (profile->prop_string_to_kind) { (*prop)->kind = profile->prop_string_to_kind((const char *) attr); if ((*prop)->kind == profile->no_prop_value) { error->precond = profile->filter_precond; error->desc = "Unsupported property"; error->node = xmlCopyNode(root, 2); } } if (!error->precond) { attr = xmlGetProp(root, BAD_CAST "test"); if (attr) { if (!xmlStrcmp(attr, BAD_CAST "allof")) (*prop)->allof = 1; else if (!xmlStrcmp(attr, BAD_CAST "anyof")) (*prop)->allof = 0; else { error->precond = profile->filter_precond; error->desc = "Unsupported test"; error->node = xmlCopyNode(root, 2); } xmlFree(attr); } } } for (node = xmlFirstElementChild(root); node && !error->precond; node = xmlNextElementSibling(node)) { if ((*prop)->not_defined) { error->precond = profile->filter_precond; error->desc = DAV_FILTER_ISNOTDEF_ERR; error->node = xmlCopyNode(root, 1); } else if (!xmlStrcmp(node->name, BAD_CAST "is-not-defined")) { if ((*prop)->other || (*prop)->match || (*prop)->param) { error->precond = profile->filter_precond; error->desc = DAV_FILTER_ISNOTDEF_ERR; error->node = xmlCopyNode(root, 1); } else (*prop)->not_defined = 1; } else if (!xmlStrcmp(node->name, BAD_CAST "text-match")) { struct text_match_t *match = NULL; dav_parse_textmatch(node, &match, profile, error); if (match) { if ((*prop)->match) match->next = (*prop)->match; (*prop)->match = match; } } else if (!xmlStrcmp(node->name, BAD_CAST "param-filter")) { struct param_filter *param = NULL; dav_parse_paramfilter(node, ¶m, profile, error); if (param) { if ((*prop)->param) param->next = (*prop)->param; (*prop)->param = param; } } else if (profile->parse_propfilter) { profile->parse_propfilter(node, *prop, error); } else { error->precond = profile->filter_precond; error->desc = "Unsupported element in prop-filter"; error->node = xmlCopyNode(root, 1); } } } void dav_free_propfilter(struct prop_filter *prop) { struct param_filter *param, *next; xmlFree(prop->name); if (prop->match) { xmlFree(prop->match->text); free(prop->match); } for (param = prop->param; param; param = next) { next = param->next; xmlFree(param->name); if (param->match) { xmlFree(param->match->text); free(param->match); } free(param); } free(prop); } int dav_apply_textmatch(xmlChar *text, struct text_match_t *match) { const xmlChar *cp = NULL; int textlen, matchlen; int r = 0; switch (match->type) { case MATCH_TYPE_CONTAINS: switch (match->collation) { case COLLATION_UNICODE: /* XXX how to do this? */ case COLLATION_ASCII: cp = xmlStrcasestr(text, match->text); break; case COLLATION_OCTET: cp = xmlStrstr(text, match->text); break; } r = (cp != NULL); break; case MATCH_TYPE_EQUALS: switch (match->collation) { case COLLATION_UNICODE: /* XXX how to do this? */ case COLLATION_ASCII: r = !xmlStrcasecmp(text, match->text); break; case COLLATION_OCTET: r = xmlStrEqual(text, match->text); break; } break; case MATCH_TYPE_PREFIX: matchlen = xmlStrlen(match->text); switch (match->collation) { case COLLATION_UNICODE: /* XXX how to do this? */ case COLLATION_ASCII: r = !xmlStrncasecmp(text, match->text, matchlen); break; case COLLATION_OCTET: r = !xmlStrncmp(text, match->text, matchlen); break; } break; case MATCH_TYPE_SUFFIX: textlen = xmlStrlen(text); matchlen = xmlStrlen(match->text); if (textlen < matchlen) r = 0; else { cp = text += (textlen - matchlen); switch (match->collation) { case COLLATION_UNICODE: /* XXX how to do this? */ case COLLATION_ASCII: r = !xmlStrcasecmp(cp, match->text); break; case COLLATION_OCTET: r = xmlStrEqual(cp, match->text); break; } } break; } if (match->negate) r = !r; return r; }