Page MenuHomePhorge

http_dav.c
No OneTemporary

Authored By
Unknown
Size
350 KB
Referenced Files
None
Subscribers
None

http_dav.c

This file is larger than 256 KB, so syntax highlighting was skipped.
/* 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 "http_dav.h"
#include "annotate.h"
#include "acl.h"
#include "append.h"
#include "caldav_db.h"
#include "dlist.h"
#include "exitcodes.h"
#include "global.h"
#include "http_proxy.h"
#include "index.h"
#include "proxy.h"
#include "times.h"
#include "syslog.h"
#include "strhash.h"
#include "tok.h"
#include "util.h"
#include "version.h"
#include "webdav_db.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 <libxml/uri.h>
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" },
};
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 }
};
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 struct webdav_db *auth_webdavdb = NULL;
static void my_dav_init(struct buf *serverinfo);
static void my_dav_auth(const char *userid);
static void my_dav_reset(void);
static void my_dav_shutdown(void);
static int get_server_info(struct transaction_t *txn);
static void get_synctoken(struct mailbox *mailbox,
struct buf *buf, const char *prefix);
static int principal_parse_path(const char *path, struct request_target_t *tgt,
const char **errstr);
static int propfind_displayname(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop, xmlNodePtr resp,
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 propfind_notifyurl(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int propfind_csnotify_collection(struct propfind_ctx *fctx,
xmlNodePtr props);
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_displayname, NULL, 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 },
/* 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, 0, NULL, NULL, NULL },
{ "group-membership", NS_DAV, 0, 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, NULL, 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 },
{ NULL, 0, 0, NULL, NULL, NULL }
};
static 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, "/dav/principals", NULL,
http_allow_noauth_get, /*authschemes*/0,
/*mbtype */ 0,
ALLOW_READ | ALLOW_DAV,
&my_dav_init, &my_dav_auth, &my_dav_reset, &my_dav_shutdown, &dav_premethod,
/*bearer*/NULL,
{
{ NULL, NULL }, /* ACL */
{ NULL, NULL }, /* BIND */
{ 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 */
{ NULL, NULL }, /* 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 },
/* 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 },
/* 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 }
};
/* Check ACL on userid's principal (Inbox): LOOKUP right gives access */
static int principal_acl_check(const char *userid, struct auth_state *authstate)
{
int r = 0;
if (!httpd_userisadmin) {
char *inboxname = mboxname_user_mbox(userid, NULL);
mbentry_t *mbentry = NULL;
r = http_mlookup(inboxname, &mbentry, NULL);
if (r) {
syslog(LOG_ERR, "mlookup(%s) failed: %s",
inboxname, error_message(r));
r = HTTP_NOT_FOUND;
}
else if (!(httpd_myrights(authstate, mbentry) & ACL_LOOKUP)) {
r = HTTP_NOT_FOUND;
}
mboxlist_entry_free(&mbentry);
free(inboxname);
}
return r;
}
/* Parse request-target path in DAV principals namespace */
static int principal_parse_path(const char *path, struct request_target_t *tgt,
const char **errstr)
{
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] != '/')) {
*errstr = "Namespace mismatch request target path";
return HTTP_FORBIDDEN;
}
/* Skip namespace */
p += len;
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)) {
p += len;
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;
}
p += len;
if (!*p || !*++p) return 0;
}
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) {
// *errstr = "Too many segments in request target path";
return HTTP_NOT_FOUND;
}
return 0;
}
/* Parse request-target path in Cal/CardDAV namespace */
EXPORTED int calcarddav_parse_path(const char *path,
struct request_target_t *tgt,
const char *mboxprefix,
const char **errstr)
{
char *p, *owner = NULL, *collection = NULL, *freeme = NULL;
size_t len;
const char *mboxname;
mbname_t *mbname = NULL;
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] != '/')) {
*errstr = "Namespace mismatch request target path";
return HTTP_FORBIDDEN;
}
tgt->mboxprefix = mboxprefix;
/* Default to bare-bones Allow bits */
tgt->allow &= ALLOW_READ_MASK;
/* Skip namespace */
p += len;
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)) {
p += len;
if (!*p || !*++p) return 0;
/* Get user id */
len = strcspn(p, "/");
tgt->userid = xstrndup(p, len);
p += len;
if (!*p || !*++p) {
/* Make sure home-set is terminated with '/' */
if (p[-1] != '/') *p++ = '/';
goto done;
}
len = strcspn(p, "/");
}
/* Get collection */
tgt->collection = p;
tgt->collen = len;
p += len;
if (!*p || !*++p) {
/* Make sure collection is terminated with '/' */
if (p[-1] != '/') *p++ = '/';
goto done;
}
/* Get resource */
len = strcspn(p, "/");
tgt->resource = p;
tgt->reslen = len;
p += len;
if (*p) {
// *errstr = "Too many segments in request target path";
return HTTP_NOT_FOUND;
}
done:
/* Create mailbox name from the parsed path */
owner = tgt->userid;
if (tgt->collen) {
collection = freeme = xstrndup(tgt->collection, tgt->collen);
/* Shared collection encoded as: <owner> "." <mboxname> */
if (!tgt->mbentry && /* Not MKCOL or COPY/MOVE destination */
(p = strrchr(collection, SHARED_COLLECTION_DELIM))) {
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);
free(freeme);
}
/* 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))
return HTTP_NOT_FOUND;
mbname_set_domain(mbname, NULL);
}
mboxname = mbname_intname(mbname);
if (tgt->mbentry) {
/* Just return the mboxname (MKCOL or COPY/MOVE destination) */
tgt->mbentry->name = xstrdup(mboxname);
if (!mboxlist_createmailboxcheck(mboxname, 0, NULL, httpd_userisadmin,
httpd_userid, httpd_authstate,
NULL, NULL, 0 /* force */)) {
tgt->allow |= ALLOW_MKCOL;
}
}
else if (*mboxname) {
/* Locate the mailbox */
int r = http_mlookup(mboxname, &tgt->mbentry, NULL);
if (r) {
syslog(LOG_ERR, "mlookup(%s) failed: %s",
mboxname, error_message(r));
*errstr = error_message(r);
mbname_free(&mbname);
switch (r) {
case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN;
case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND;
default: return HTTP_SERVER_ERROR;
}
}
}
/* 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;
mbname_free(&mbname);
return 0;
}
EXPORTED int dav_get_validators(struct mailbox *mailbox, void *data,
const char *userid __attribute__((unused)),
struct index_record *record,
const char **etag, time_t *lastmod)
{
const struct dav_data *ddata = (const struct dav_data *) data;
memset(record, 0, sizeof(struct index_record));
if (!ddata->alive) {
/* New resource */
if (etag) *etag = NULL;
if (lastmod) *lastmod = 0;
}
else if (ddata->imap_uid) {
/* Mapped URL */
int r;
/* Fetch index record for the resource */
r = mailbox_find_index_record(mailbox, ddata->imap_uid, record);
if (r) {
syslog(LOG_ERR, "mailbox_find_index_record(%s, %u) failed: %s",
mailbox->name, ddata->imap_uid, error_message(r));
return r;
}
if (etag) *etag = message_guid_encode(&record->guid);
if (lastmod) *lastmod = record->internaldate;
}
else {
/* Unmapped URL (empty resource) */
if (etag) *etag = NULL;
if (lastmod) *lastmod = ddata->creationdate;
}
return 0;
}
EXPORTED modseq_t dav_get_modseq(struct mailbox *mailbox __attribute__((unused)),
const struct index_record *record,
const char *userid __attribute__((unused)),
void *davdb __attribute__((unused)))
{
return record->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;
get_synctoken(mailbox, &buf, SYNC_TOKEN_URL_SCHEME);
r = !strcmp(cond, buf_cstring(&buf));
if (!r) {
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,
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));
if (!params->parse_path(uri->path, &tag_tgt, &err)) {
if (tag_tgt.mbentry && !tag_tgt.mbentry->server) {
if (tgt_mailbox &&
!strcmp(tgt_mailbox->name, 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,
mailbox->name,
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,
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, 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)) {
txn->resp_body.link = 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_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)) ret = m;
}
}
free(e->token);
}
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))) 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) {
/* Last char of token signals href (1) or text (0) */
if (data->lock_token[strlen(data->lock_token)-1] == '1') {
node1 = xmlNewChild(active, NULL, BAD_CAST "owner", NULL);
xml_add_href(node1, NULL, data->lock_owner);
}
else {
xmlNewTextChild(active, NULL, BAD_CAST "owner",
BAD_CAST data->lock_owner);
}
}
snprintf(tbuf, sizeof(tbuf), "Second-%lu", 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)
{
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(code));
if (precond) {
xmlNodePtr error = xmlNewChild(resp, NULL, BAD_CAST "error", NULL);
xmlNewChild(error, NULL, BAD_CAST preconds[precond].name, NULL);
}
}
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;
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(fctx->mailbox->name, 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(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;
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, int precond,
const char *data, unsigned long datalen)
{
int ret = 0;
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 (!propstat) {
/* Prescreen "property" request */
if (!mime->content_type) {
fctx->txn->error.precond = precond;
ret = *fctx->ret = HTTP_FORBIDDEN;
}
}
else {
/* Add "property" */
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, *outbuf;
if (!fctx->obj) {
buf_init_ro(&inbuf, data, datalen);
fctx->obj = mime_types->to_object(&inbuf);
buf_free(&inbuf);
}
outbuf = mime->from_object(fctx->obj);
datalen = buf_len(outbuf);
data = freeme = buf_release(outbuf);
buf_destroy(outbuf);
}
if (type) {
xmlSetProp(prop, BAD_CAST "content-type", type);
if (ver) xmlSetProp(prop, BAD_CAST "version", ver);
}
xmlAddChild(prop,
xmlNewCDataBlock(fctx->root->doc, BAD_CAST data, datalen));
fctx->flags.fetcheddata = 1;
if (freeme) free(freeme);
}
if (type) xmlFree(type);
if (ver) xmlFree(ver);
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 fetch DAV:displayname */
static int propfind_displayname(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: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;
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-collection-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;
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);
}
}
}
}
/* 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 (!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, fctx->mailbox->name)) {
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 (!fctx->mailbox) return HTTP_NOT_FOUND;
/* owner has implicit admin rights */
if (!mboxname_userownsmailbox(httpd_userid, fctx->mailbox->name)) {
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 = xstrdup(fctx->mailbox->acl);
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, fctx->mailbox->name)) {
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], *qr = NULL;
if (fctx->mailbox) {
/* Use the quotaroot as specified in mailbox header */
qr = fctx->mailbox->quotaroot;
}
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(&fctx->quota, NULL, 0);
}
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)) {
/* 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;
}
static void 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;
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<<userflag%31);
if (set) {
if (isset) goto done;
pctx->record->user_flags[userflag/32] |= (1<<userflag%31);
}
else {
if (!isset) goto done;
pctx->record->user_flags[userflag/32] &= ~(1<<userflag%31);
}
r = mailbox_rewrite_index_record(pctx->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);
}
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<<userflag%31);
if (isset)
buf_setcstr(&attrib, "1");
goto done;
}
/* otherwise it's a DB annotation */
buf_reset(&fctx->buf);
buf_printf(&fctx->buf, DAV_ANNOT_NS "<%s>%s",
(const char *) ns->href, name);
r = annotatemore_msg_lookup(fctx->mailbox->name, 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(fctx->mbentry->name,
buf_cstring(&fctx->buf),
httpd_userid, &attrib))) {
if (!buf_len(&attrib) &&
!xmlStrcmp(name, BAD_CAST "displayname")) {
/* Special case empty displayname -- use last segment of path */
buf_setcstr(&attrib, strrchr(fctx->mbentry->name, '.') + 1);
}
}
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);
}
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 its 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 ( <href>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);
if (!ns) {
char prefix[5];
snprintf(prefix, sizeof(prefix), "X%u", arock->fctx->prefix_count++);
ns = xmlNewNs(arock->fctx->root, BAD_CAST href, BAD_CAST prefix);
hash_insert(href, ns, 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 (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(prop->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
*/
static 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 = prop->ns;
const char *ns_href = (const char *) ns->href;
unsigned i;
if (fctx->mode == PROPFIND_EXPAND) {
/* Get name/namespace from <property> */
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 for both collections
* 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) ||
strcmp((const char *) prop->ns->href,
known_namespaces[entry->ns].href));
entry++);
if (entry->name) {
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 {
/* Write "live" property */
entry->put(prop, set, pctx, propstat, entry->rock);
}
}
else {
/* 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(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_body(httpd_in, httpd_out,
txn->req_hdrs, &txn->req_body, &txn->error.desc);
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 <deny> element as providing
* "negative" rights (-identifier) in IMAP-speak. Additionally, we treat
* the special DAV identifiers as follows:
*
* <all> == IMAP "anyone"
* <unauthenticated> == IMAP "anonymous"
* <authenticated> == 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 */
if ((r = aparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) 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\r\n";
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\r\n";
ret = HTTP_BAD_REQUEST;
}
if (ret) goto done;
indoc = root->doc;
/* Make sure its an DAV:acl element */
if (xmlStrcmp(root->name, BAD_CAST "acl")) {
txn->error.desc = "Missing acl element in ACL request\r\n";
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\r\n";
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\r\n";
ret = HTTP_BAD_REQUEST;
goto done;
}
}
else if (!xmlStrcmp(child->name, BAD_CAST "grant")) {
if (privs) {
txn->error.desc = "Multiple grant|deny in ACE\r\n";
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\r\n";
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\r\n";
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);
}
}
mailbox_set_acl(mailbox, buf_cstring(&acl), 1);
r = mboxlist_sync_setacls(txn->req_tgt.mbentry->name, buf_cstring(&acl));
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->name, buf_cstring(&mrock->newname),
NULL /* partition */, 0 /* uidvalidity */,
1 /* admin */, httpd_userid, httpd_authstate,
NULL, 0, 0, 1 /* ignorequota */);
}
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 <multistatus> */
mrock->root =
init_xml_response("multistatus", NS_DAV, NULL, mrock->ns);
if (!mrock->root) return HTTP_SERVER_ERROR;
}
/* Add new <response> 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 <href> 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 <status> element */
xmlNewChild(resp, NULL, BAD_CAST "status",
BAD_CAST http_statusline(code));
/* Add <error> 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, 1 /* checkacl */,
0 /* localonly */, 0 /* force */);
}
else {
r = mboxlist_deletemailbox(mbentry->name, 1, /* admin */
httpd_userid, httpd_authstate,
NULL, 1 /* checkacl */,
0 /* localonly */, 0 /* force */);
}
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;
}
}
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, 1 /* checkacl */,
0 /* localonly*/, 0 /* force */);
}
else {
r = mboxlist_deletemailbox(newmailboxname,
httpd_userisadmin,
httpd_userid, httpd_authstate,
mboxevent, 1 /* checkacl */,
0 /* localonly*/, 0 /* force */);
}
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(oldmailboxname, newmailboxname,
NULL /* partition */, 0 /* uidvalidity */,
httpd_userisadmin, httpd_userid, httpd_authstate,
mboxevent, 0, 0, 1 /* ignorequota */);
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) {
xml_response(HTTP_MULTI_STATUS, txn, mrock.root->doc);
xmlFreeDoc(mrock.root->doc);
return 0;
}
}
done:
switch (r) {
case 0:
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, body_buf;
memset(&dest_tgt, 0, sizeof(struct request_target_t));
/* Response should not be cached */
txn->flags.cc |= CC_NOCACHE;
/* Parse the source path */
if ((r = cparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) 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;
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->name,
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->name,
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.system_flags |= FLAG_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);
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;
}
/* Perform a DELETE request */
int meth_delete(struct transaction_t *txn, void *params)
{
struct meth_params *dparams = (struct meth_params *) params;
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;
/* Response should not be cached */
txn->flags.cc |= CC_NOCACHE;
/* Parse the path */
r = dparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc);
if (r) return r;
/* Make sure method is allowed */
if (!(txn->req_tgt.allow & ALLOW_DELETE)) return HTTP_NOT_ALLOWED;
/* if FastMail sharing, we need to remove ACLs */
if (config_getswitch(IMAPOPT_FASTMAILSHARING) &&!txn->req_tgt.resource &&
!mboxname_userownsmailbox(httpd_userid, 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);
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;
}
return HTTP_OK;
}
/* Special case of deleting a shared collection */
if (!txn->req_tgt.resource && (txn->req_tgt.flags == TGT_DAV_SHARED)) {
char *inboxname = mboxname_user_mbox(txn->req_tgt.userid, NULL);
mbentry_t *mbentry = NULL;
r = http_mlookup(inboxname, &mbentry, 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);
return ret;
}
/* Check ACL for current user */
rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
needrights = txn->req_tgt.resource ? DACL_RMRSRC : 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 */
if (!txn->req_tgt.resource) {
/* DELETE collection */
if (dparams->delete) {
/* Do special processing on all resources */
struct delete_rock drock = { txn, NULL, dparams->delete };
/* 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;
}
/* Open the DAV DB corresponding to the mailbox */
davdb = dparams->davdb.open_db(mailbox);
drock.mailbox = mailbox;
r = dparams->davdb.foreach_resource(davdb, mailbox->name,
&delete_cb, &drock);
/* we need the mailbox closed before we delete it */
mailbox_close(&mailbox);
if (r) {
txn->error.desc = error_message(r);
return HTTP_SERVER_ERROR;
}
}
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,
/*checkack*/1, /*localonly*/0, /*force*/0);
}
else {
r = mboxlist_deletemailbox(txn->req_tgt.mbentry->name,
httpd_userisadmin || httpd_userisproxyadmin,
httpd_userid, httpd_authstate, mboxevent,
/*checkack*/1, /*localonly*/0, /*force*/0);
}
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;
goto done;
}
/* DELETE resource */
/* 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;
}
/* 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->name,
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, params, 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.system_flags |= FLAG_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;
goto done;
}
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);
}
done:
if (davdb) dparams->davdb.close_db(davdb);
mailbox_close(&mailbox);
if (!r)
mboxevent_notify(&mboxevent);
mboxevent_free(&mboxevent);
return ret;
}
/* 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 */
ret = gparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc);
if (ret) return ret;
if (txn->req_tgt.namespace->id == URL_NS_PRINCIPAL) {
/* Special "principal" */
if (txn->req_tgt.flags == TGT_SERVER_INFO) return get_server_info(txn);
/* No content for principals (yet) */
return HTTP_NO_CONTENT;
}
if (!txn->req_tgt.resource) {
/* Do any collection processing */
if (gparams->get) return gparams->get(txn, NULL, NULL, NULL, NULL);
/* We don't handle GET on a collection */
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.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->name,
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 (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);
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_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];
xmlChar *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 */
if ((r = lparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) 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->name,
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 = mailbox->name;
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;
unsigned owner_is_href = 0;
/* 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;
/* Check for correct root element */
indoc = root->doc;
if (xmlStrcmp(root->name, BAD_CAST "lockinfo")) {
txn->error.desc = "Incorrect root element in XML request\r\n";
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 its 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 its 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 */
for (sub = node->children;
sub && sub->type != XML_ELEMENT_NODE; sub = sub->next);
if (!sub) {
owner = xmlNodeGetContent(node);
}
/* Make sure its a href element */
else if (xmlStrcmp(sub->name, BAD_CAST "href")) {
ret = HTTP_BAD_REQUEST;
goto done;
}
else {
owner_is_href = 1;
owner = xmlNodeGetContent(sub);
}
}
}
ddata->lock_ownerid = httpd_userid;
if (owner) ddata->lock_owner = (const char *) owner;
/* Construct lock-token */
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, XML_NS_CYRUS "lock/%s-%x-%u",
mailbox->uniqueid, strhash(txn->req_tgt.resource),
owner_is_href);
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\r\n";
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) xmlFree(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;
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();
if ((r = mparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) {
txn->error.precond = mparams->mkcol.location_precond;
return HTTP_FORBIDDEN;
}
/* Make sure method is allowed (only allowed on home-set) */
if (!(txn->req_tgt.allow & ALLOW_MKCOL)) {
txn->error.precond = mparams->mkcol.location_precond;
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 ) */
indoc = root->doc;
buf_setcstr(&txn->buf, http_methods[txn->meth].name);
r = xmlStrcmp(root->name, BAD_CAST buf_lcase(&txn->buf));
if (r) {
txn->error.desc = "Incorrect root element in XML request\r\n";
ret = HTTP_BAD_MEDIATYPE;
goto done;
}
instr = root->children;
}
/* Create the mailbox */
r = mboxlist_createmailbox(txn->req_tgt.mbentry->name,
mparams->mkcol.mbtype, partition,
httpd_userisadmin || httpd_userisproxyadmin,
httpd_userid, httpd_authstate,
/*localonly*/0, /*forceuser*/0,
/*dbonly*/0, /*notify*/0,
&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\r\n";
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_close(&mailbox);
mboxlist_deletemailbox(txn->req_tgt.mbentry->name,
/*isadmin*/1, NULL, NULL, NULL,
/*checkacl*/0, /*localonly*/0, /*force*/1);
if (!ret) {
/* Output the XML response */
xml_response(r, txn, outdoc);
}
goto done;
}
}
if (!r) {
assert(!buf_len(&txn->buf));
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);
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);
}
else if (!fctx->filter || fctx->filter(fctx, data)) {
/* Add response for target */
ret = xml_add_response(fctx, 0, 0);
}
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->mailbox->name,
fctx->req_tgt->resource, (void **) &ddata, 0);
if (!ddata->rowid) {
/* Add response for missing target */
xml_add_response(fctx, HTTP_NOT_FOUND, 0);
return HTTP_NOT_FOUND;
}
r = fctx->proc_by_resource(fctx, ddata);
}
else {
/* Add responses for all contained resources */
fctx->foreach_resource(fctx->davdb, fctx->mailbox->name,
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;
}
static size_t make_collection_url(struct buf *buf, const char *urlprefix,
const char *mboxname, const char *userid,
char **mbox_owner)
{
mbname_t *mbname = NULL;
const strarray_t *boxes;
int n, size;
size_t len;
mbname = mbname_from_intname(mboxname);
buf_reset(buf);
buf_printf(buf, "%s/", urlprefix);
if (userid) {
if (!mbname_domain(mbname)) mbname_set_domain(mbname, httpd_extradomain);
const char *owner = mbname_userid(mbname);
if (!owner) owner = "";
if (mbox_owner) *mbox_owner = xstrdup(owner);
if (config_getswitch(IMAPOPT_FASTMAILSHARING)) {
if (strcmp(owner, userid) && strstr(urlprefix, "addressbooks"))
buf_printf(buf, "%s/%s/", "zzzz", owner);
else
buf_printf(buf, "%s/%s/", 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: <owner> "." <mboxname> */
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, '/');
}
mbname_free(&mbname);
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 (!(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;
case URL_NS_CALENDAR:
/* Inbox and Outbox can't appear unless they are the target */
if (!fctx->req_tgt->flags) {
if (!strncmp(p, SCHED_INBOX, strlen(SCHED_INBOX) - 1)) goto done;
if (!strncmp(p, SCHED_OUTBOX, strlen(SCHED_OUTBOX) - 1)) goto done;
}
/* fall through */
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->txn->error.desc = error_message(r);
*fctx->ret = HTTP_SERVER_ERROR;
goto done;
}
fctx->mbentry = mbentry;
fctx->mailbox = mailbox;
fctx->record = NULL;
if (!fctx->req_tgt->resource) {
len = make_collection_url(&writebuf, fctx->req_tgt->namespace->prefix,
mboxname, fctx->req_tgt->userid, NULL);
/* 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 (!fctx->filter_crit && !(fctx->prefer & PREFER_NOROOT) &&
(r = xml_add_response(fctx, 0, 0))) 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 r;
}
/* 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 = { 0, NULL, NULL };
struct propfind_ctx fctx;
struct propfind_entry_list *elist = NULL;
memset(&fctx, 0, sizeof(struct propfind_ctx));
/* Parse the path */
if (fparams->parse_path) {
r = fparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc);
if (r) 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\r\n";
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 its a propfind element */
if (xmlStrcmp(root->name, BAD_CAST "propfind")) {
txn->error.desc = "Missing 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 its 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.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 */
preload_proplist(props, &fctx);
/* 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);
}
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);
}
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);
/* 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, 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:
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);
}
break;
case URL_NS_CALENDAR:
if (fctx.flags.cs_sharing) {
/* Add response for notification collection */
r = propfind_csnotify_collection(&fctx, props);
}
/* Fall through */
case URL_NS_ADDRESSBOOK:
/* 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);
/* Output the XML response */
if (!ret) {
/* iCalendar data in response should not be transformed */
if (fctx.flags.fetcheddata) txn->flags.cc |= CC_NOTRANSFORM;
xml_response(HTTP_MULTI_STATUS, txn, outdoc);
}
done:
/* Free the entry list */
elist = fctx.elist;
while (elist) {
struct propfind_entry_list *freeme = elist;
elist = elist->next;
xmlFree(freeme->name);
free(freeme);
}
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, rights;
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 */
if ((r = pparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) return r;
if (!txn->req_tgt.collection && !txn->req_tgt.userid) {
txn->error.desc = "PROPPATCH requires a collection";
return HTTP_NOT_ALLOWED;
}
/* Check ACL for current user */
rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
if (!(rights & DACL_PROPCOL)) {
/* DAV:need-privileges */
txn->error.precond = DAV_NEED_PRIVS;
txn->error.resource = txn->req_tgt.path;
txn->error.rights = DACL_PROPCOL;
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 */
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\r\n";
ret = HTTP_BAD_REQUEST;
}
if (ret) goto done;
indoc = root->doc;
/* Make sure its a propertyupdate element */
if (xmlStrcmp(root->name, BAD_CAST "propertyupdate")) {
txn->error.desc =
"Missing propertyupdate element in PROPPATCH request\r\n";
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\r\n";
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->name,
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);
/* Output the XML response */
if (!ret) {
if (!r && (get_preferences(txn) & PREFER_MIN)) ret = HTTP_OK;
else xml_response(HTTP_MULTI_STATUS, txn, outdoc);
}
done:
if (davdb) pparams->davdb.close_db(davdb);
mailbox_close(&mailbox);
buf_free(&pctx.buf);
if (outdoc) xmlFreeDoc(outdoc);
if (indoc) xmlFreeDoc(indoc);
return ret;
}
enum {
SHARE_NONE = 0,
SHARE_READONLY,
SHARE_READWRITE
};
static const char *access_types[] = { "no-access", "read", "read-write" };
static int set_share_access(const char *mboxname,
const char *userid, int access)
{
char r, rightstr[100];
/* Set access rights */
rightstr[0] = (access == SHARE_READWRITE) ? '+' : '-';
cyrus_acl_masktostr(DACL_SHARERW, rightstr+1);
r = mboxlist_setacl(&httpd_namespace, mboxname, userid, rightstr,
httpd_userisadmin || httpd_userisproxyadmin,
httpd_userid, httpd_authstate);
if (!r && access == SHARE_READONLY) {
rightstr[0] = '+';
cyrus_acl_masktostr(DACL_SHARE, rightstr+1);
r = mboxlist_setacl(&httpd_namespace, mboxname, userid, rightstr,
httpd_userisadmin || httpd_userisproxyadmin,
httpd_userid, httpd_authstate);
}
return r;
}
static xmlNodePtr get_props(struct request_target_t *req_tgt,
const char *prop_names[],
xmlNodePtr root, xmlNsPtr ns[],
const struct prop_entry prop_list[])
{
struct propstat propstat = { NULL, 0, 0 };
struct propfind_ctx fctx;
const struct prop_entry *prop;
const char **name;
xmlNodePtr node;
memset(&fctx, 0, sizeof(struct propfind_ctx));
fctx.req_tgt = req_tgt;
fctx.mbentry = req_tgt->mbentry;
fctx.root = root;
fctx.ns = ns;
for (prop = prop_list; prop->name; prop++) {
for (name = prop_names; *name; name++) {
if (!strcmp(*name, prop->name)) {
prop->get(BAD_CAST prop->name, ns[NS_DAV],
&fctx, NULL, NULL, &propstat, NULL);
}
}
}
buf_free(&fctx.buf);
node = propstat.root->children;
xmlUnlinkNode(node);
xmlFreeNode(propstat.root);
return node;
}
static int create_notify_collection(const char *userid,
struct mailbox **mailbox);
static int notify_put(struct transaction_t *txn, void *obj,
struct mailbox *mailbox, const char *resource,
void *davdb, unsigned flags);
#define DAVSHARING_CONTENT_TYPE "application/davsharing+xml"
#define DAVNOTIFICATION_CONTENT_TYPE \
"application/davnotification+xml; charset=utf-8"
#define SYSTEM_STATUS_NOTIFICATION "systemstatus"
#define SHARE_INVITE_NOTIFICATION "share-invite-notification"
#define SHARE_REPLY_NOTIFICATION "share-reply-notification"
static int send_notification(struct transaction_t *top_txn, xmlDocPtr doc,
const char *userid, const char *resource)
{
struct mailbox *mailbox = NULL;
struct webdav_db *webdavdb = NULL;
struct transaction_t txn;
int r;
/* XXX Need to find location of user.
If remote need to do a PUT or possibly email */
/* Open notifications collection for writing */
r = create_notify_collection(userid, &mailbox);
if (r == IMAP_INVALID_USER) {
syslog(LOG_NOTICE,
"send_notification(%s) failed: %s", userid, error_message(r));
return 0;
}
else if (r) {
syslog(LOG_ERR,
"send_notification: create_notify_collection(%s) failed: %s",
userid, error_message(r));
return r;
}
/* Open the WebDAV DB corresponding to collection */
webdavdb = webdav_open_mailbox(mailbox);
if (!webdavdb) {
syslog(LOG_ERR, "send_notification: unable to open WebDAV DB (%s)",
mailbox->name);
r = HTTP_SERVER_ERROR;
goto done;
}
/* Start with an empty (clean) transaction */
memset(&txn, 0, sizeof(struct transaction_t));
txn.req_tgt.namespace = top_txn->req_tgt.namespace;
txn.req_tgt.mboxprefix = top_txn->req_tgt.mboxprefix;
/* Create header cache */
if (!(txn.req_hdrs = spool_new_hdrcache())) {
syslog(LOG_ERR, "send_notification: unable to create header cache");
r = HTTP_SERVER_ERROR;
goto done;
}
spool_cache_header(xstrdup("Content-Type"),
xstrdup(DAVNOTIFICATION_CONTENT_TYPE), txn.req_hdrs);
r = notify_put(&txn, doc, mailbox, resource, webdavdb, 0);
if (r != HTTP_CREATED && r != HTTP_NO_CONTENT) {
syslog(LOG_ERR,
"send_notification: notify_put(%s, %s) failed: %s",
mailbox->name, resource, error_message(r));
}
done:
spool_free_hdrcache(txn.req_hdrs);
buf_free(&txn.buf);
webdav_close(webdavdb);
mailbox_close(&mailbox);
return r;
}
static int dav_post_share(struct transaction_t *txn,
struct meth_params *pparams)
{
xmlNodePtr root = NULL, node, sharee, princ;
int rights, ret, legacy = 0;
struct buf resource = BUF_INITIALIZER;
char dtstamp[RFC3339_DATETIME_MAX];
xmlNodePtr notify = NULL, type, resp, share, comment;
xmlNsPtr ns[NUM_NAMESPACE];
const char *invite_principal_props[] = { "displayname", NULL };
const char *invite_collection_props[] = { "displayname", "resourcetype",
"supported-calendar-component-set",
NULL };
/* 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 */
/* Read body */
ret = parse_xml_body(txn, &root, DAVSHARING_CONTENT_TYPE);
if (!ret && !root) {
txn->error.desc = "Missing request body";
ret = HTTP_BAD_REQUEST;
}
if (ret) goto done;
/* Make sure its a share-resource element */
if (!xmlStrcmp(root->name, BAD_CAST "share")) legacy = 1;
else if (xmlStrcmp(root->name, BAD_CAST "share-resource")) {
txn->error.desc =
"Missing share-resource element in POST request";
ret = HTTP_BAD_REQUEST;
goto done;
}
/* Create share-invite-notification -
response and share-access will be replaced for each sharee */
notify = init_xml_response("notification", NS_DAV, NULL, ns);
time_to_rfc3339(time(0), dtstamp, RFC3339_DATETIME_MAX);
xmlNewChild(notify, NULL, BAD_CAST "dtstamp", BAD_CAST dtstamp);
type = xmlNewChild(notify, NULL, BAD_CAST SHARE_INVITE_NOTIFICATION, NULL);
princ = xmlNewChild(type, NULL, BAD_CAST "principal", NULL);
buf_printf(&resource, "%s/%s/%s/", namespace_principal.prefix,
USER_COLLECTION_PREFIX, txn->req_tgt.userid);
xml_add_href(princ, NULL, buf_cstring(&resource));
node = get_props(&txn->req_tgt, invite_principal_props,
notify, ns, princ_params.propfind.lprops);
xmlAddChild(princ, node);
resp = xmlNewChild(type, NULL, BAD_CAST "invite-noresponse", NULL);
node = xmlNewChild(type, NULL, BAD_CAST "sharer-resource-uri", NULL);
xml_add_href(node, NULL, txn->req_tgt.path);
node = xmlNewChild(type, NULL, BAD_CAST "share-access", NULL);
share = xmlNewChild(node, NULL, BAD_CAST "no-access", NULL);
node = get_props(&txn->req_tgt, invite_collection_props,
notify, ns, pparams->propfind.lprops);
xmlAddChild(type, node);
comment = xmlNewChild(type, NULL, BAD_CAST "comment", NULL);
/* Process each sharee */
for (sharee = xmlFirstElementChild(root); sharee;
sharee = xmlNextElementSibling(sharee)) {
xmlChar *href = NULL, *content;
int access = SHARE_READONLY;
xmlNodeSetContent(comment, BAD_CAST "");
if (legacy) {
if (!xmlStrcmp(sharee->name, BAD_CAST "remove")) {
access = SHARE_NONE;
}
else if (xmlStrcmp(sharee->name, BAD_CAST "set")) continue;
}
else if (xmlStrcmp(sharee->name, BAD_CAST "sharee")) continue;
for (node = xmlFirstElementChild(sharee); node;
node = xmlNextElementSibling(node)) {
if (!xmlStrcmp(node->name, BAD_CAST "href")) {
href = xmlNodeGetContent(node);
if (access == SHARE_NONE) break;
}
if (legacy) {
if (!xmlStrcmp(node->name, BAD_CAST "read-write")) {
access = SHARE_READWRITE;
}
else if (!xmlStrcmp(node->name, BAD_CAST "summary")) {
content = xmlNodeGetContent(node);
xmlNodeSetContent(comment, content);
xmlFree(content);
}
}
else if (!xmlStrcmp(node->name, BAD_CAST "share-access")) {
xmlNodePtr share = xmlFirstElementChild(node);
if (!xmlStrcmp(share->name, BAD_CAST "no-access")) {
access = SHARE_NONE;
}
else if (!xmlStrcmp(share->name, BAD_CAST "read-write")) {
access = SHARE_READWRITE;
}
}
else if (!xmlStrcmp(node->name, BAD_CAST "comment")) {
content = xmlNodeGetContent(node);
xmlNodeSetContent(comment, content);
xmlFree(content);
}
}
if (href) {
char *userid = NULL, *at;
int r;
if (!xmlStrncmp(href, BAD_CAST "mailto:", 7)) {
userid = xstrdup((char *) href + 7);
if ((at = strchr(userid, '@'))) {
if (!config_virtdomains || !strcmp(at+1, config_defdomain)){
*at = '\0';
}
}
}
else if (!xmlStrncmp(href, BAD_CAST "DAV:", 4)) {
if (!xmlStrcmp(href + 4, BAD_CAST "all")) {
userid = xstrdup("anyone");
}
else if (!xmlStrcmp(href + 4, BAD_CAST "unauthenticated")) {
userid = xstrdup("anonymous");
}
else if (!xmlStrcmp(href + 4, BAD_CAST "authenticated")) {
/* This needs to be done as anyone - anonymous */
r = set_share_access(txn->req_tgt.mbentry->name,
"anyone", access);
if (r) {
syslog(LOG_NOTICE,
"failed to set share access for"
" 'anyone' on '%s': %s",
txn->req_tgt.mbentry->name, error_message(r));
}
else userid = xstrdup("-anonymous");
}
}
else {
const char *errstr = NULL;
xmlURIPtr uri = parse_uri(METH_UNKNOWN,
(const char *) href, 1, &errstr);
if (uri) {
struct request_target_t principal;
memset(&principal, 0, sizeof(struct request_target_t));
r = principal_parse_path((const char *) uri->path,
&principal, &errstr);
if (!r && principal.userid) userid = principal.userid;
else if (principal.userid) free(principal.userid);
xmlFreeURI(uri);
}
}
if (!userid) {
/* XXX set invite-invalid ? */
}
else {
/* Set access rights */
r = set_share_access(txn->req_tgt.mbentry->name,
userid, access);
if (r) {
syslog(LOG_NOTICE,
"failed to set share access for '%s' on '%s': %s",
userid, txn->req_tgt.mbentry->name,
error_message(r));
}
else {
/* Notify sharee - patch in response and share-access */
const char *annot =
DAV_ANNOT_NS "<" XML_NS_DAV ">invite-status";
const char *response = "invite-noresponse";
struct buf value = BUF_INITIALIZER;
int r;
/* Lookup invite status */
r = annotatemore_lookupmask(txn->req_tgt.mbentry->name,
annot, userid, &value);
if (!r && buf_len(&value)) response = buf_cstring(&value);
node = xmlNewNode(ns[NS_DAV], BAD_CAST response);
buf_free(&value);
xmlReplaceNode(resp, node);
xmlFreeNode(resp);
resp = node;
node = xmlNewNode(ns[NS_DAV], BAD_CAST access_types[access]);
xmlReplaceNode(share, node);
xmlFreeNode(share);
share = node;
/* Create a resource name for the notifications -
We use a consistent naming scheme so that multiple
notifications of the same type for the same resource
are coalesced (overwritten) */
buf_reset(&resource);
buf_printf(&resource, "%x-%x-%x-%x.xml",
strhash(XML_NS_DAV),
strhash(SHARE_INVITE_NOTIFICATION),
strhash(txn->req_tgt.mbentry->name),
strhash(userid));
r = send_notification(txn, notify->doc,
userid, buf_cstring(&resource));
}
free(userid);
}
xmlFree(href);
}
ret = HTTP_NO_CONTENT;
}
done:
if (root) xmlFreeDoc(root->doc);
if (notify) xmlFreeDoc(notify->doc);
buf_free(&resource);
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_INITIALIZER;
void *davdb = NULL, *obj = NULL;
xmlDocPtr outdoc = NULL;
xmlNodePtr root;
xmlNsPtr ns[NUM_NAMESPACE];
/* 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_body(httpd_in, httpd_out,
txn->req_hdrs, &txn->req_body, &txn->error.desc);
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);
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;
}
/* 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");
outdoc = root->doc;
/* Parse, validate, and store the resource */
obj = mime->to_object(&txn->req_body.payload);
ret = pparams->post.import(txn, obj, mailbox, davdb,
root, ns, get_preferences(txn));
/* Validators */
assert(!buf_len(&txn->buf));
get_synctoken(mailbox, &txn->buf, "");
txn->resp_body.ctag = buf_cstring(&txn->buf);
txn->resp_body.etag = NULL;
txn->resp_body.lastmod = 0;
/* Output the XML response */
if (!ret) xml_response(HTTP_MULTI_STATUS, txn, outdoc);
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;
static unsigned post_count = 0;
struct strlist *action;
int r, ret;
size_t len;
/* Response should not be cached */
txn->flags.cc |= CC_NOCACHE;
/* Parse the path */
if ((r = pparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) 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.allowed & POST_BULK) && hdr) {
if (is_mediatype(hdr[0], "application/xml")) {
/* Bulk CRUD */
return HTTP_FORBIDDEN;
}
else {
/* 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,
"%x-%d-%ld-%u.ics",
strhash(txn->req_tgt.path), getpid(), time(0), post_count++);
/* 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 */
if ((r = pparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) {
return HTTP_FORBIDDEN;
}
/* 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_body(httpd_in, httpd_out,
txn->req_hdrs, &txn->req_body, &txn->error.desc);
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->name,
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;
/* 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_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 */
if ((r = pparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) {
return HTTP_FORBIDDEN;
}
/* Make sure method is allowed (only allowed on resources) */
if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED;
reqd_rights = DACL_WRITECONT;
if (txn->req_tgt.allow & ALLOW_USERDATA) reqd_rights |= DACL_PROPRSRC;
}
/* Make sure mailbox type is correct */
if (txn->req_tgt.mbentry->mbtype != txn->req_tgt.namespace->mboxtype)
return HTTP_FORBIDDEN;
/* 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 */
/* Read body */
txn->req_body.flags |= BODY_DECODE;
ret = http_read_body(httpd_in, httpd_out,
txn->req_hdrs, &txn->req_body, &txn->error.desc);
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);
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->name,
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);
if (r != HTTP_CONTINUE) flags &= ~PREFER_REP;
}
else {
unsigned offset;
struct buf buf;
/* 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 */
/* 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;
}
/* Compare modseq in index maps -- used for sorting */
static int map_modseq_cmp(const struct index_map *m1,
const struct index_map *m2)
{
if (m1->modseq < m2->modseq) return -1;
if (m1->modseq > m2->modseq) return 1;
return 0;
}
/* 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;
/* 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;
/* Parse the URI */
uri = parse_uri(METH_REPORT, (const char *) href,
1 /* path required */, &fctx->txn->error.desc);
xmlFree(href);
if (!uri) {
ret = HTTP_FORBIDDEN;
goto done;
}
/* Parse the path */
memset(&tgt, 0, sizeof(struct request_target_t));
tgt.namespace = txn->req_tgt.namespace;
r = rparams->parse_path(uri->path, &tgt, &fctx->txn->error.desc);
xmlFreeURI(uri);
if (r) {
ret = r;
goto done;
}
fctx->req_tgt = &tgt;
fctx->mbentry = tgt.mbentry;
/* Check if we already have this mailbox open */
if (!mailbox || strcmp(mailbox->name, 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));
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
goto done;
}
fctx->mailbox = mailbox;
}
if (!fctx->mailbox || !tgt.resource) {
/* Add response for missing target */
xml_add_response(fctx, HTTP_NOT_FOUND, 0);
continue;
}
/* 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, tgt.mbentry->name,
tgt.resource, (void **) &ddata, 0);
ddata->resource = tgt.resource;
/* XXX Check errors */
fctx->proc_by_resource(fctx, ddata);
/* XXX - split this into a req_tgt cleanup */
free(tgt.userid);
mboxlist_entry_free(&tgt.mbentry);
rparams->davdb.close_db(fctx->davdb);
}
}
done:
mailbox_close(&mailbox);
return (ret ? ret : HTTP_MULTI_STATUS);
}
/* 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, i, unbind_flag = -1, unchanged_flag = -1;
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 = -1;
uint32_t msgno;
uint32_t nresp = 0;
xmlNodePtr node;
struct index_state istate;
char tokenuri[MAX_MAILBOX_PATH+1];
/* XXX Handle Depth (cal-home-set at toplevel) */
memset(&istate, 0, sizeof(struct index_state));
istate.map = NULL;
/* 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;
mailbox_user_flag(mailbox, DFLAG_UNBIND, &unbind_flag, 1);
mailbox_user_flag(mailbox, DFLAG_UNCHANGED, &unchanged_flag, 1);
/* 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))) {
/* 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")) {
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))) {
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;
}
/* Construct array of records for sorting and/or fetching cached header */
istate.mailbox = mailbox;
istate.map = xzmalloc(mailbox->i.num_records * sizeof(struct index_map));
/* Open the DAV DB corresponding to the mailbox */
fctx->davdb = rparams->davdb.open_db(fctx->mailbox);
/* Find which resources we need to report */
struct mailbox_iter *iter = mailbox_iter_init(mailbox, syncmodseq, 0);
const message_t *msg;
while ((msg = mailbox_iter_step(iter))) {
const struct index_record *record = msg_record(msg);
modseq_t modseq;
if ((unbind_flag >= 0) &&
record->user_flags[unbind_flag / 32] & (1 << (unbind_flag & 31))) {
/* Resource replaced by a PUT, COPY, or MOVE - ignore it */
continue;
}
/* Fetch modseq for record (could be per-user) */
modseq = rparams->get_modseq(mailbox, record, httpd_userid, fctx->davdb);
if (modseq <= syncmodseq) {
/* Resource not added/removed since last sync */
continue;
}
if ((modseq - syncmodseq == 1) &&
(unchanged_flag >= 0) &&
(record->user_flags[unchanged_flag / 32] &
(1 << (unchanged_flag & 31)))) {
/* Resource has just had VTIMEZONEs stripped - ignore it */
continue;
}
if ((modseq <= basemodseq) &&
(record->system_flags & FLAG_EXPUNGED)) {
/* Initial sync - ignore unmapped resources */
continue;
}
/* copy data into map (just like index.c - XXX helper fn? */
istate.map[nresp].recno = record->recno;
istate.map[nresp].uid = record->uid;
istate.map[nresp].modseq = modseq;
istate.map[nresp].system_flags = record->system_flags;
for (i = 0; i < MAX_USER_FLAGS/32; i++)
istate.map[nresp].user_flags[i] = record->user_flags[i];
istate.map[nresp].cache_offset = record->cache_offset;
nresp++;
}
mailbox_iter_done(&iter);
if (limit < nresp) {
/* Need to truncate the responses */
struct index_map *map = istate.map;
/* Sort the response records by modseq */
qsort(map, nresp, sizeof(struct index_map),
(int (*)(const void *, const void *)) &map_modseq_cmp);
/* Our last response MUST be the last record with its modseq */
for (nresp = limit;
nresp && map[nresp-1].modseq == map[nresp].modseq;
nresp--);
if (!nresp) {
/* DAV:number-of-matches-within-limits */
fctx->txn->error.desc = "Unable to truncate results";
txn->error.precond = DAV_OVER_LIMIT;
ret = HTTP_NO_STORAGE;
goto done;
}
/* respmodseq will be modseq of last record we return */
respmodseq = map[nresp-1].modseq;
/* Tell client we truncated the responses */
xml_add_response(fctx, HTTP_NO_STORAGE, DAV_OVER_LIMIT);
}
else {
/* Full response - respmodseq will be highestmodseq of mailbox */
respmodseq = highestmodseq;
}
/* Report the resources within the client requested limit (if any) */
for (msgno = 0; msgno < nresp; msgno++) {
struct dav_data *ddata;
/* Find name of the resource */
r = rparams->davdb.lookup_imapuid(fctx->davdb, fctx->mailbox->name,
istate.map[msgno].uid,
(void **) &ddata,
/* tombstones */ 1);
if (r) continue;
if (!ddata->alive) {
/* report 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;
}
fctx->proc_by_resource(fctx, ddata);
fctx->record = NULL;
}
/* Add sync-token element */
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);
}
xmlNewChild(fctx->root, NULL, BAD_CAST "sync-token", BAD_CAST tokenuri);
done:
if (fctx->davdb) rparams->davdb.close_db(fctx->davdb);
if (istate.map) free(istate.map);
mailbox_close(&mailbox);
return (ret ? ret : HTTP_MULTI_STATUS);
}
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);
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 */
while (fctx->elist) {
struct propfind_entry_list *freeme = fctx->elist;
fctx->elist = freeme->next;
xmlFree(freeme->name);
free(freeme);
}
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, struct propfind_ctx *fctx)
{
int ret = 0;
struct request_target_t req_tgt;
mbentry_t *mbentry = fctx->req_tgt->mbentry;
char *userid, *nextid;
xmlNodePtr cur;
/* 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 children element of report */
for (cur = inroot->children; cur; cur = cur->next) {
if (cur->type == XML_ELEMENT_NODE &&
!xmlStrcmp(cur->name, BAD_CAST "prop")) {
if ((ret = preload_proplist(cur->children, fctx))) goto done;
break;
}
}
/* 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);
free(req_tgt.userid);
}
}
done:
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);
}
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(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 = { 0, NULL, NULL };
struct propfind_ctx fctx;
struct propfind_entry_list *elist = NULL;
memset(&fctx, 0, sizeof(struct propfind_ctx));
/* Parse the path */
if ((r = rparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) 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\r\n";
return HTTP_BAD_REQUEST;
}
}
/* Parse the REPORT body */
ret = parse_xml_body(txn, &inroot, NULL);
if (!ret && !inroot) {
txn->error.desc = "Missing request body\r\n";
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 (!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) {
if (cur->type == XML_ELEMENT_NODE) {
if (!xmlStrcmp(cur->name, BAD_CAST "allprop")) {
fctx.mode = PROPFIND_ALL;
prop = cur;
break;
}
else if (!xmlStrcmp(cur->name, BAD_CAST "propname")) {
fctx.mode = PROPFIND_NAME;
fctx.prefer = PREFER_MIN; /* Don't want 404 (Not Found) */
prop = cur;
break;
}
else if (!xmlStrcmp(cur->name, BAD_CAST "prop")) {
fctx.mode = PROPFIND_PROP;
prop = cur;
props = cur->children;
break;
}
}
}
if (!prop && (report->flags & REPORT_NEED_PROPS)) {
txn->error.desc = "Missing <prop> element in REPORT\r\n";
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\r\n";
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.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) {
fctx.proc_by_resource = &propfind_by_resource;
ret = preload_proplist(props, &fctx);
}
/* Process the requested report */
if (!ret) ret = (*report->proc)(txn, rparams, inroot, &fctx);
/* Output the XML response */
if (outroot) {
switch (ret) {
case HTTP_OK:
case HTTP_MULTI_STATUS:
/* iCalendar 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 */
elist = fctx.elist;
while (elist) {
struct propfind_entry_list *freeme = elist;
elist = elist->next;
if (freeme->flags & PROP_CLEANUP) {
freeme->get(freeme->name, NULL, &fctx,
NULL, NULL, NULL, freeme->rock);
}
xmlFree(freeme->name);
free(freeme);
}
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 */
if ((r = lparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) 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->name,
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;
}
int dav_store_resource(struct transaction_t *txn,
const char *data, size_t datalen,
struct mailbox *mailbox, struct index_record *oldrecord,
strarray_t *imapflags)
{
int ret = HTTP_CREATED, r;
hdrcache_t hdrcache = txn->req_hdrs;
struct stagemsg *stage;
FILE *f = NULL;
const char **hdr, *cte;
quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_DONTCARE_INITIALIZER;
time_t now = time(NULL);
struct appendstate as;
/* Prepare to stage the message */
if (!(f = append_newstage(mailbox->name, now, 0, &stage))) {
syslog(LOG_ERR, "append_newstage(%s) failed", mailbox->name);
txn->error.desc = "append_newstage() failed\r\n";
return HTTP_SERVER_ERROR;
}
/* Create RFC 5322 header for resource */
if ((hdr = spool_getheader(hdrcache, "User-Agent"))) {
fprintf(f, "User-Agent: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "From"))) {
fprintf(f, "From: %s\r\n", hdr[0]);
}
else {
char *mimehdr;
assert(!buf_len(&txn->buf));
if (strchr(httpd_userid, '@')) {
/* XXX This needs to be done via an LDAP/DB lookup */
buf_printf(&txn->buf, "<%s>", httpd_userid);
}
else {
buf_printf(&txn->buf, "<%s@%s>", httpd_userid, config_servername);
}
mimehdr = charset_encode_mimeheader(buf_cstring(&txn->buf),
buf_len(&txn->buf));
fprintf(f, "From: %s\r\n", mimehdr);
free(mimehdr);
buf_reset(&txn->buf);
}
if ((hdr = spool_getheader(hdrcache, "Subject"))) {
fprintf(f, "Subject: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "Date"))) {
fprintf(f, "Date: %s\r\n", hdr[0]);
}
else {
char datestr[80]; /* XXX: Why do we need 80 character buffer? */
time_to_rfc5322(now, datestr, sizeof(datestr));
fprintf(f, "Date: %s\r\n", datestr);
}
if ((hdr = spool_getheader(hdrcache, "Message-ID"))) {
fprintf(f, "Message-ID: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "X-Schedule-User-Address"))) {
fprintf(f, "X-Schedule-User-Address: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "Content-Type"))) {
fprintf(f, "Content-Type: %s\r\n", hdr[0]);
}
else fputs("Content-Type: application/octet-stream\r\n", f);
if (!datalen) {
datalen = strlen(data);
cte = "8bit";
}
else {
cte = strnchr(data, '\0', datalen) ? "binary" : "8bit";
}
fprintf(f, "Content-Transfer-Encoding: %s\r\n", cte);
if ((hdr = spool_getheader(hdrcache, "Content-Disposition"))) {
fprintf(f, "Content-Disposition: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "Content-Description"))) {
fprintf(f, "Content-Description: %s\r\n", hdr[0]);
}
fprintf(f, "Content-Length: %u\r\n", (unsigned) datalen);
fputs("MIME-Version: 1.0\r\n\r\n", f);
/* Write the data to the file */
fwrite(data, datalen, 1, f);
qdiffs[QUOTA_STORAGE] = ftell(f);
fclose(f);
qdiffs[QUOTA_MESSAGE] = 1;
/* Prepare to append the message to the mailbox */
if ((r = append_setup_mbox(&as, mailbox, httpd_userid, httpd_authstate,
0, qdiffs, 0, 0, EVENT_MESSAGE_NEW|EVENT_CALENDAR))) {
syslog(LOG_ERR, "append_setup(%s) failed: %s",
mailbox->name, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_setup() failed\r\n";
}
else {
struct body *body = NULL;
strarray_t *flaglist = NULL;
struct entryattlist *annots = NULL;
if (oldrecord) {
flaglist = mailbox_extract_flags(mailbox, oldrecord, httpd_userid);
annots = mailbox_extract_annots(mailbox, oldrecord);
}
/* XXX - casemerge? Doesn't matter with flags */
if (imapflags) {
if (flaglist)
strarray_cat(flaglist, imapflags);
else
flaglist = strarray_dup(imapflags);
}
/* Append the message to the mailbox */
if ((r = append_fromstage(&as, &body, stage, now, flaglist, 0, annots))) {
syslog(LOG_ERR, "append_fromstage(%s) failed: %s",
mailbox->name, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_fromstage() failed\r\n";
}
if (body) {
message_free_body(body);
free(body);
}
strarray_free(flaglist);
freeentryatts(annots);
if (r) append_abort(&as);
else {
/* Commit the append to the mailbox */
if ((r = append_commit(&as))) {
syslog(LOG_ERR, "append_commit(%s) failed: %s",
mailbox->name, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_commit() failed\r\n";
}
else {
/* Read index record for new message (always the last one) */
struct index_record newrecord;
struct dav_data ddata;
ddata.alive = 1;
ddata.imap_uid = mailbox->i.last_uid;
dav_get_validators(mailbox, &ddata, httpd_userid, &newrecord,
&txn->resp_body.etag, &txn->resp_body.lastmod);
if (oldrecord) {
/* Now that we have the replacement message in place
expunge the old one. */
int userflag;
ret = HTTP_NO_CONTENT;
/* Perform the actual expunge */
r = mailbox_user_flag(mailbox, DFLAG_UNBIND, &userflag, 1);
if (!r) {
oldrecord->user_flags[userflag/32] |= 1 << (userflag & 31);
oldrecord->system_flags |= FLAG_EXPUNGED;
r = mailbox_rewrite_index_record(mailbox, oldrecord);
}
if (r) {
syslog(LOG_ERR, "expunging record (%s) failed: %s",
mailbox->name, error_message(r));
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
}
}
}
}
}
append_removestage(stage);
return ret;
}
static void my_dav_init(struct buf *serverinfo __attribute__((unused)))
{
if (!namespace_principal.enabled) return;
if (!config_getstring(IMAPOPT_DAVNOTIFICATIONSPREFIX)) {
fatal("Required 'davnotificationsprefix' option is not set", EC_CONFIG);
}
namespace_notify.enabled = 1;
webdav_init();
}
static int lookup_notify_collection(const char *userid, mbentry_t **mbentry)
{
mbname_t *mbname;
const char *notifyname;
int r;
/* Create notification mailbox name from the parsed path */
mbname = mbname_from_userid(userid);
mbname_push_boxes(mbname, config_getstring(IMAPOPT_DAVNOTIFICATIONSPREFIX));
/* 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)) {
r = HTTP_NOT_FOUND;
goto done;
}
mbname_set_domain(mbname, NULL);
}
/* Locate the mailbox */
notifyname = mbname_intname(mbname);
r = http_mlookup(notifyname, mbentry, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* Find location of INBOX */
char *inboxname = mboxname_user_mbox(userid, NULL);
int r1 = http_mlookup(inboxname, mbentry, NULL);
free(inboxname);
if (r1 == IMAP_MAILBOX_NONEXISTENT) {
r = IMAP_INVALID_USER;
goto done;
}
if (*mbentry) free((*mbentry)->name);
else *mbentry = mboxlist_entry_create();
(*mbentry)->name = xstrdup(notifyname);
}
done:
mbname_free(&mbname);
return r;
}
static int create_notify_collection(const char *userid, struct mailbox **mailbox)
{
/* notifications collection */
mbentry_t *mbentry = NULL;
int r = lookup_notify_collection(userid, &mbentry);
if (r == IMAP_INVALID_USER) {
goto done;
}
else if (r == IMAP_MAILBOX_NONEXISTENT) {
if (!mbentry) goto done;
else if (mbentry->server) {
proxy_findserver(mbentry->server, &http_protocol, httpd_userid,
&backend_cached, NULL, NULL, httpd_in);
goto done;
}
r = mboxlist_createmailbox(mbentry->name, MBTYPE_COLLECTION,
NULL, 1 /* admin */, userid, NULL,
0, 0, 0, 0, mailbox);
/* we lost the race, that's OK */
if (r == IMAP_MAILBOX_LOCKED) r = 0;
if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
mbentry->name, error_message(r));
}
else if (mailbox) {
/* Open mailbox for writing */
r = mailbox_open_iwl(mbentry->name, mailbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mbentry->name, error_message(r));
}
}
done:
mboxlist_entry_free(&mbentry);
return r;
}
static void my_dav_auth(const char *userid)
{
if (httpd_userisadmin || httpd_userisanonymous ||
global_authisa(httpd_authstate, IMAPOPT_PROXYSERVERS)) {
/* admin, anonymous, or proxy from frontend - won't have DAV database */
return;
}
else if (config_mupdate_server && !config_getstring(IMAPOPT_PROXYSERVERS)) {
/* proxy-only server - won't have DAV databases */
}
else {
/* Open WebDAV DB for 'userid' */
my_dav_reset();
auth_webdavdb = webdav_open_userid(userid);
if (!auth_webdavdb) fatal("Unable to open WebDAV DB", EC_IOERR);
}
/* Auto-provision a notifications collection for 'userid' */
create_notify_collection(userid, NULL);
if (!server_info) {
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];
/* 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, "%ld-%ld-%ld", (long) 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");
}
}
}
static void my_dav_reset(void)
{
if (auth_webdavdb) webdav_close(auth_webdavdb);
auth_webdavdb = NULL;
}
static void my_dav_shutdown(void)
{
my_dav_reset();
webdav_done();
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 int notify_parse_path(const char *path,
struct request_target_t *tgt, const char **errstr);
static int notify_get(struct transaction_t *txn, struct mailbox *mailbox,
struct index_record *record, void *data, void **obj);
static int propfind_notifytype(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static struct buf *from_xml(xmlDocPtr doc)
{
struct buf *buf = buf_new();
xmlChar *xml = NULL;
int len;
/* Dump XML response tree into a text buffer */
xmlDocDumpFormatMemoryEnc(doc, &xml, &len, "utf-8",
config_httpprettytelemetry);
if (xml) buf_initm(buf, (char *) xml, len);
else buf_init(buf);
return buf;
}
static xmlDocPtr to_xml(const struct buf *buf)
{
xmlParserCtxtPtr ctxt;
xmlDocPtr doc = NULL;
ctxt = xmlNewParserCtxt();
if (ctxt) {
doc = xmlCtxtReadMemory(ctxt, buf_base(buf), buf_len(buf), NULL, NULL,
XML_PARSE_NOWARNING);
xmlFreeParserCtxt(ctxt);
}
return doc;
}
static struct mime_type_t notify_mime_types[] = {
/* First item MUST be the default type and storage format */
{ DAVNOTIFICATION_CONTENT_TYPE, NULL, "xml",
(struct buf* (*)(void *)) &from_xml,
(void * (*)(const struct buf*)) &to_xml,
(void (*)(void *)) &xmlFreeDoc, NULL, NULL
},
{ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};
/* Array of supported REPORTs */
static const struct report_type_t notify_reports[] = {
/* WebDAV Versioning (RFC 3253) REPORTs */
{ "expand-property", NS_DAV, "multistatus", &report_expand_prop,
DACL_READ, 0 },
/* WebDAV ACL (RFC 3744) REPORTs */
{ "acl-principal-prop-set", NS_DAV, "multistatus", &report_acl_prin_prop,
DACL_ADMIN, REPORT_NEED_MBOX | REPORT_DEPTH_ZERO },
/* WebDAV Sync (RFC 6578) REPORTs */
{ "sync-collection", NS_DAV, "multistatus", &report_sync_col,
DACL_READ, REPORT_NEED_MBOX | REPORT_NEED_PROPS },
{ NULL, 0, NULL, NULL, 0, 0 }
};
/* Array of known "live" properties */
static const struct prop_entry notify_props[] = {
/* WebDAV (RFC 4918) properties */
{ "creationdate", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_creationdate, NULL, NULL },
{ "displayname", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_fromdb, proppatch_todb, NULL },
{ "getcontentlanguage", NS_DAV, PROP_ALLPROP | PROP_RESOURCE,
propfind_fromhdr, NULL, "Content-Language" },
{ "getcontentlength", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_getlength, NULL, NULL },
{ "getcontenttype", NS_DAV, PROP_ALLPROP | PROP_RESOURCE,
propfind_fromhdr, NULL, "Content-Type" },
{ "getetag", NS_DAV, PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_getetag, NULL, NULL },
{ "getlastmodified", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_getlastmod, NULL, NULL },
{ "resourcetype", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_restype, NULL, NULL },
/* WebDAV Versioning (RFC 3253) properties */
{ "supported-report-set", NS_DAV, PROP_COLLECTION,
propfind_reportset, NULL, (void *) notify_reports },
/* WebDAV ACL (RFC 3744) properties */
{ "owner", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_owner, NULL, NULL },
{ "group", NS_DAV, 0, NULL, NULL, NULL },
{ "supported-privilege-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_supprivset, NULL, NULL },
{ "current-user-privilege-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_curprivset, NULL, NULL },
{ "acl", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_acl, NULL, NULL },
{ "acl-restrictions", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_aclrestrict, NULL, NULL },
{ "inherited-acl-set", NS_DAV, 0, NULL, NULL, NULL },
{ "principal-collection-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_princolset, NULL, NULL },
/* WebDAV Current Principal (RFC 5397) properties */
{ "current-user-principal", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE,
propfind_curprin, NULL, NULL },
/* WebDAV Sync (RFC 6578) properties */
{ "sync-token", NS_DAV, PROP_COLLECTION,
propfind_sync_token, NULL, SYNC_TOKEN_URL_SCHEME },
/* WebDAV Notifications (draft-pot-webdav-notifications) properties */
{ "notificationtype", NS_DAV, PROP_RESOURCE,
propfind_notifytype, NULL, NULL },
/* Backwards compatibility with Apple notifications clients */
{ "notificationtype", NS_CS, PROP_RESOURCE,
propfind_notifytype, NULL, "calendarserver-sharing" },
/* Apple Calendar Server properties */
{ "getctag", NS_CS, PROP_ALLPROP | PROP_COLLECTION,
propfind_sync_token, NULL, "" },
{ NULL, 0, 0, NULL, NULL, NULL }
};
struct meth_params notify_params = {
notify_mime_types,
&notify_parse_path,
&dav_get_validators,
&dav_get_modseq,
&dav_check_precond,
{ (db_open_proc_t) &webdav_open_mailbox,
(db_close_proc_t) &webdav_close,
(db_proc_t) &webdav_begin,
(db_proc_t) &webdav_commit,
(db_proc_t) &webdav_abort,
(db_lookup_proc_t) &webdav_lookup_resource,
(db_imapuid_proc_t) &webdav_lookup_imapuid,
(db_foreach_proc_t) &webdav_foreach,
(db_write_proc_t) &webdav_write,
(db_delete_proc_t) &webdav_delete },
NULL, /* No ACL extensions */
{ 0, &notify_put },
NULL, /* No special DELETE handling */
&notify_get,
{ 0, 0 }, /* No MKCOL handling */
NULL, /* No PATCH handling */
{ 0, &notify_post, NULL }, /* No generic POST handling */
{ 0, &notify_put },
{ DAV_FINITE_DEPTH, notify_props},
notify_reports
};
/* Namespace for WebDAV notification collections */
struct namespace_t namespace_notify = {
URL_NS_NOTIFY, 0, "/dav/notifications", NULL,
http_allow_noauth_get, /*authschemes*/0,
MBTYPE_COLLECTION,
(ALLOW_READ | ALLOW_POST | ALLOW_DELETE |
ALLOW_DAV | ALLOW_PROPPATCH | ALLOW_ACL),
NULL, NULL, NULL, NULL,
&dav_premethod, /*bearer*/NULL,
{
{ &meth_acl, &notify_params }, /* ACL */
{ NULL, NULL }, /* BIND */
{ NULL, NULL }, /* COPY */
{ &meth_delete, &notify_params }, /* DELETE */
{ &meth_get_head, &notify_params }, /* GET */
{ &meth_get_head, &notify_params }, /* HEAD */
{ NULL, NULL }, /* LOCK */
{ NULL, NULL }, /* MKCALENDAR */
{ NULL, NULL }, /* MKCOL */
{ NULL, NULL }, /* MOVE */
{ &meth_options, &notify_parse_path }, /* OPTIONS */
{ NULL, NULL }, /* PATCH */
{ &meth_post, &notify_params }, /* POST */
{ &meth_propfind, &notify_params }, /* PROPFIND */
{ NULL, NULL }, /* PROPPATCH */
{ NULL, NULL }, /* PUT */
{ &meth_report, &notify_params }, /* REPORT */
{ &meth_trace, &notify_parse_path }, /* TRACE */
{ NULL, NULL }, /* UNBIND */
{ NULL, NULL }, /* UNLOCK */
}
};
/* Perform a GET/HEAD request on a WebDAV notification resource */
static int notify_get(struct transaction_t *txn, struct mailbox *mailbox,
struct index_record *record, void *data,
void **obj __attribute__((unused)))
{
const char **hdr;
struct webdav_data *wdata = (struct webdav_data *) data;
struct dlist *dl = NULL, *al;
const char *type_str;
struct buf msg_buf = BUF_INITIALIZER;
struct buf inbuf, *outbuf = NULL;
xmlDocPtr indoc = NULL, outdoc;
xmlNodePtr notify = NULL, root, node, type;
xmlNodePtr resp = NULL, sharedurl = NULL, node2;
xmlNsPtr ns[NUM_NAMESPACE];
xmlChar *dtstamp = NULL, *comment = NULL;
char datestr[RFC3339_DATETIME_MAX];
time_t t;
enum {
SYSTEM_STATUS,
SHARE_INVITE,
SHARE_REPLY
} notify_type;
int r, ret = 0;
if (!record || !record->uid) return HTTP_NO_CONTENT;
if ((hdr = spool_getheader(txn->req_hdrs, "Accept")) &&
is_mediatype(DAVSHARING_CONTENT_TYPE, hdr[0])) {
return HTTP_CONTINUE;
}
/* If no Accept header is given or its not application/davsharing+xml,
assume its a legacy notification client and do a mime type translation
from application/davnotification+xml to application/xml */
/* Parse dlist representing notification type, and data */
dlist_parsemap(&dl, 1, 0, wdata->filename, strlen(wdata->filename));
dlist_getatom(dl, "T", &type_str);
dlist_getlist(dl, "D", &al);
if (!strcmp(type_str, SYSTEM_STATUS_NOTIFICATION)) {
notify_type = SYSTEM_STATUS;
}
else if (!strcmp(type_str, SHARE_INVITE_NOTIFICATION)) {
notify_type = SHARE_INVITE;
}
else if (!strcmp(type_str, SHARE_REPLY_NOTIFICATION)) {
notify_type = SHARE_REPLY;
}
else {
ret = HTTP_NOT_ACCEPTABLE;
goto done;
}
txn->resp_body.type = "application/xml";
if (txn->meth == METH_HEAD) {
ret = HTTP_CONTINUE;
goto done;
}
/* Load message containing the resource */
r = mailbox_map_record(mailbox, record, &msg_buf);
if (r) {
txn->error.desc = error_message(r);
return HTTP_SERVER_ERROR;
}
/* Parse message body into XML tree */
buf_init_ro(&inbuf, buf_base(&msg_buf) + record->header_size,
record->size - record->header_size);
indoc = to_xml(&inbuf);
buf_free(&inbuf);
buf_free(&msg_buf);
root = xmlDocGetRootElement(indoc);
node = xmlFirstElementChild(root);
dtstamp = xmlNodeGetContent(node);
type = xmlNextElementSibling(node);
/* Translate DAV notification into CS notification */
notify = init_xml_response("notification", NS_CS, NULL, ns);
outdoc = notify->doc;
/* Calendar.app doesn't like separators in date-time */
time_from_iso8601((const char *) dtstamp, &t);
time_to_iso8601(t, datestr, RFC3339_DATETIME_MAX, 0);
xmlNewChild(notify, NULL, BAD_CAST "dtstamp", BAD_CAST datestr);
if (notify_type == SYSTEM_STATUS) {
}
else if (notify_type == SHARE_INVITE) {
xmlNodePtr invite, sharer = NULL, access = NULL, calcompset = NULL;
xmlChar *name = NULL;
struct buf buf = BUF_INITIALIZER;
/* Grab DAV elements that we need to construct CS notification */
for (node = xmlFirstElementChild(type); node;
node = xmlNextElementSibling(node)) {
if (!xmlStrncmp(node->name, BAD_CAST "invite-", 7)) {
resp = node;
}
else if (!xmlStrcmp(node->name, BAD_CAST "sharer-resource-uri")) {
sharedurl = xmlFirstElementChild(node);
}
else if (!xmlStrcmp(node->name, BAD_CAST "principal")) {
sharer = xmlFirstElementChild(node);
node2 = xmlNextElementSibling(sharer);
if (!xmlStrcmp(node2->name, BAD_CAST "prop")) {
for (node2 = xmlFirstElementChild(node2); node2;
node2 = xmlNextElementSibling(node2)) {
if (!xmlStrcmp(node2->name, BAD_CAST "displayname")) {
name = xmlNodeGetContent(node2);
}
}
}
}
else if (!xmlStrcmp(node->name, BAD_CAST "share-access")) {
access = xmlFirstElementChild(node);
}
else if (!xmlStrcmp(node->name, BAD_CAST "comment")) {
comment = xmlNodeGetContent(node);
}
else if (!xmlStrcmp(node->name, BAD_CAST "prop")) {
for (node2 = xmlFirstElementChild(node); node2;
node2 = xmlNextElementSibling(node2)) {
if (!xmlStrcmp(node2->name,
BAD_CAST "supported-calendar-component-set")) {
calcompset = node2;
}
}
}
}
invite = xmlNewChild(notify, NULL, BAD_CAST "invite-notification", NULL);
xmlNewChild(invite, NULL, BAD_CAST "uid", BAD_CAST wdata->dav.resource);
/* Sharee href */
buf_reset(&buf);
buf_printf(&buf, "%s/%s/%s/", namespace_principal.prefix,
USER_COLLECTION_PREFIX, txn->req_tgt.userid);
node = xml_add_href(invite, NULL, buf_cstring(&buf));
ensure_ns(ns, NS_DAV, node, XML_NS_DAV, "D");
xmlSetNs(node, ns[NS_DAV]);
#if 0 /* XXX Apple clients seem to always want "noresponse" */
xmlNewChild(invite, NULL, resp->name, NULL);
#else
xmlNewChild(invite, NULL, BAD_CAST "invite-noresponse", NULL);
#endif
node = xmlNewChild(invite, NULL, BAD_CAST "access", NULL);
xmlNewChild(node, NULL, access->name, NULL);
node = xmlNewChild(invite, NULL, BAD_CAST "hosturl", NULL);
xmlAddChild(node, xmlCopyNode(sharedurl, 1));
node = xmlNewChild(invite, NULL, BAD_CAST "organizer", NULL);
xmlAddChild(node, xmlCopyNode(sharer, 1));
if (name) {
xmlNewChild(node, NULL, BAD_CAST "common-name", name);
xmlFree(name);
}
if (comment) {
xmlNewChild(invite, NULL, BAD_CAST "summary", comment);
xmlFree(comment);
}
if (calcompset) {
xmlAddChild(invite, xmlCopyNode(calcompset, 1));
}
buf_free(&buf);
}
else if (notify_type == SHARE_REPLY) {
xmlNodePtr reply, sharee = NULL;
/* Grab DAV elements that we need to construct CS notification */
for (node = xmlFirstElementChild(type); node;
node = xmlNextElementSibling(node)) {
if (!xmlStrcmp(node->name, BAD_CAST "sharee")) {
for (node2 = xmlFirstElementChild(node); node2;
node2 = xmlNextElementSibling(node2)) {
if (!xmlStrcmp(node2->name, BAD_CAST "href")) {
sharee = node2;
}
else if (!xmlStrncmp(node2->name, BAD_CAST "invite-", 7)) {
resp = node2;
}
}
}
else if (!xmlStrcmp(node->name, BAD_CAST "href")) {
sharedurl = node;
}
else if (!xmlStrcmp(node->name, BAD_CAST "comment")) {
comment = xmlNodeGetContent(node);
}
}
reply = xmlNewChild(notify, NULL, BAD_CAST "invite-reply", NULL);
xmlAddChild(reply, xmlCopyNode(sharee, 1));
xmlNewChild(reply, NULL, resp->name, NULL);
node = xmlNewChild(reply, NULL, BAD_CAST "hosturl", NULL);
xmlAddChild(node, xmlCopyNode(sharedurl, 1));
xmlNewChild(reply, NULL,
BAD_CAST "in-reply-to", BAD_CAST wdata->dav.resource);
if (comment) {
xmlNewChild(reply, NULL, BAD_CAST "summary", comment);
xmlFree(comment);
}
}
else {
/* Unknown type - return as-is */
xmlFreeDoc(notify->doc);
notify = NULL;
outdoc = indoc;
}
/* Dump XML tree into a text buffer */
outbuf = from_xml(outdoc);
write_body(HTTP_OK, txn, buf_cstring(outbuf), buf_len(outbuf));
done:
if (dtstamp) xmlFree(dtstamp);
if (notify) xmlFreeDoc(notify->doc);
if (indoc) xmlFreeDoc(indoc);
buf_destroy(outbuf);
dlist_free(&dl);
return ret;
}
/* Perform a POST request on a WebDAV notification resource */
int notify_post(struct transaction_t *txn)
{
xmlNodePtr root = NULL, node, resp = NULL;
int rights, ret, r, legacy = 0, add = 0;
struct mailbox *mailbox = NULL, *shared = NULL;
struct webdav_db *webdavdb = NULL;
struct webdav_data *wdata;
struct dlist *dl = NULL, *data;
const char *type_str, *mboxname, *url_prefix;
char dtstamp[RFC3339_DATETIME_MAX], *owner = NULL, *resource = NULL;
xmlNodePtr notify = NULL, type, sharee;
xmlNsPtr ns[NUM_NAMESPACE];
xmlChar *comment = NULL, *freeme = NULL;
/* 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 */
/* Read body */
ret = parse_xml_body(txn, &root, DAVSHARING_CONTENT_TYPE);
if (!ret && !root) {
txn->error.desc = "Missing request body";
ret = HTTP_BAD_REQUEST;
}
if (ret) goto done;
/* Make sure its a invite-reply element */
if (xmlStrcmp(root->name, BAD_CAST "invite-reply")) {
txn->error.desc =
"Missing invite-reply element in POST request";
ret = HTTP_BAD_REQUEST;
goto done;
}
resource = txn->req_tgt.resource;
if (!resource) legacy = 1;
/* Fetch needed elements */
for (node = xmlFirstElementChild(root); node;
node = xmlNextElementSibling(node)) {
if (!xmlStrncmp(node->name, BAD_CAST "invite-", 7)) {
if (!xmlStrcmp(node->name, BAD_CAST "invite-accepted")) add = 1;
resp = node;
}
else if (legacy) {
if (!xmlStrcmp(node->name, BAD_CAST "in-reply-to")) {
freeme = xmlNodeGetContent(node);
resource = (char *) freeme;
}
else if (!xmlStrcmp(node->name, BAD_CAST "summary")) {
comment = xmlNodeGetContent(node);
}
}
else if (!xmlStrcmp(node->name, BAD_CAST "comment")) {
comment = xmlNodeGetContent(node);
}
}
if (!resp) {
txn->error.desc = "Missing invite response element in POST request";
ret = HTTP_BAD_REQUEST;
goto done;
}
if (!resource) {
txn->error.desc = "Missing in-reply-to element in POST request";
ret = HTTP_BAD_REQUEST;
goto done;
}
if (legacy) {
/* Locate notification mailbox for target user */
mboxlist_entry_free(&txn->req_tgt.mbentry);
txn->req_tgt.mbentry = NULL;
r = lookup_notify_collection(txn->req_tgt.userid, &txn->req_tgt.mbentry);
if (r) {
syslog(LOG_ERR, "lookup_notify_collection(%s) failed: %s",
txn->req_tgt.userid, error_message(r));
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
goto done;
}
}
/* Open notification mailbox for reading */
r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_irl(%s) failed: %s",
txn->req_tgt.mbentry->name, error_message(r));
goto done;
}
/* Open the WebDAV DB corresponding to the mailbox */
webdavdb = webdav_open_mailbox(mailbox);
/* Find message UID for the resource */
webdav_lookup_resource(webdavdb, txn->req_tgt.mbentry->name,
resource, &wdata, 0);
if (!wdata->dav.imap_uid) {
ret = HTTP_NOT_FOUND;
goto done;
}
/* Parse dlist representing notification type, and data */
dlist_parsemap(&dl, 1, 0, wdata->filename, strlen(wdata->filename));
dlist_getatom(dl, "T", &type_str);
if (strcmp(type_str, SHARE_INVITE_NOTIFICATION)) {
ret = HTTP_NOT_ALLOWED;
goto done;
}
dlist_getlist(dl, "D", &data);
dlist_getatom(data, "M", &mboxname);
/* [Un]subscribe */
r = mboxlist_changesub(mboxname, txn->req_tgt.userid,
httpd_authstate, add, 0, 0);
if (r) {
syslog(LOG_ERR, "mboxlist_changesub(%s, %s) failed: %s",
mboxname, txn->req_tgt.userid, error_message(r));
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
goto done;
}
/* Set invite status */
r = mailbox_open_iwl(mboxname, &shared);
if (r) {
syslog(LOG_ERR, "IOERROR: failed to open mailbox %s for share reply",
mboxname);
}
else {
annotate_state_t *astate = NULL;
r = mailbox_get_annotate_state(shared, 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, (char *) resp->name);
r = annotate_state_writemask(astate, annot,
txn->req_tgt.userid, &value);
if (shared->mbtype == MBTYPE_CALENDAR) {
/* Sharee's copy of calendar SHOULD default to transparent */
annot =
DAV_ANNOT_NS "<" XML_NS_CALDAV ">schedule-calendar-transp";
buf_init_ro_cstr(&value, "transparent");
r = annotate_state_writemask(astate, annot,
txn->req_tgt.userid, &value);
}
}
mailbox_close(&shared);
}
/* Create share-reply-notification */
notify = init_xml_response("notification", NS_DAV, NULL, ns);
time_to_rfc3339(time(0), dtstamp, RFC3339_DATETIME_MAX);
xmlNewChild(notify, NULL, BAD_CAST "dtstamp", BAD_CAST dtstamp);
type = xmlNewChild(notify, NULL, BAD_CAST SHARE_REPLY_NOTIFICATION, NULL);
sharee = xmlNewChild(type, NULL, BAD_CAST "sharee", NULL);
buf_reset(&txn->buf);
buf_printf(&txn->buf, "%s/%s/%s/", namespace_principal.prefix,
USER_COLLECTION_PREFIX, txn->req_tgt.userid);
xml_add_href(sharee, NULL, buf_cstring(&txn->buf));
node = xmlNewChild(sharee, NULL, resp->name, NULL);
/* shared-url */
url_prefix = strstr(mboxname, config_getstring(IMAPOPT_CALENDARPREFIX)) ?
namespace_calendar.prefix : namespace_addressbook.prefix;
make_collection_url(&txn->buf, url_prefix, mboxname, "", &owner);
xml_add_href(type, NULL, buf_cstring(&txn->buf));
if (comment) {
xmlNewChild(type, NULL, BAD_CAST "comment", comment);
xmlFree(comment);
}
/* Create a resource name for the notifications -
We use a consistent naming scheme so that multiple notifications
of the same type for the same resource are coalesced (overwritten) */
buf_reset(&txn->buf);
buf_printf(&txn->buf, "%x-%x-%x-%x.xml",
strhash(XML_NS_DAV), strhash(SHARE_REPLY_NOTIFICATION),
strhash(mboxname), strhash(txn->req_tgt.userid));
r = send_notification(txn, notify->doc, owner, buf_cstring(&txn->buf));
if (add) {
/* Accepted - create URL of sharee's new collection */
make_collection_url(&txn->buf, url_prefix,
mboxname, txn->req_tgt.userid, NULL);
if (legacy) {
/* Create CS:shared-as XML body */
xmlNodePtr shared_as = init_xml_response("shared-as", NS_CS, NULL, ns);
if (!shared_as) {
ret = HTTP_SERVER_ERROR;
goto done;
}
node = xml_add_href(shared_as, NULL, buf_cstring(&txn->buf));
ensure_ns(ns, NS_DAV, node, XML_NS_DAV, "D");
xmlSetNs(node, ns[NS_DAV]);
xml_response(HTTP_OK, txn, shared_as->doc);
xmlFreeDoc(shared_as->doc);
ret = 0;
}
else {
/* Add Location header */
txn->location = buf_cstring(&txn->buf);
ret = HTTP_CREATED;
}
}
else {
/* Declined */
ret = HTTP_NO_CONTENT;
}
done:
if (freeme) xmlFree(freeme);
if (root) xmlFreeDoc(root->doc);
if (notify) xmlFreeDoc(notify->doc);
webdav_close(webdavdb);
mailbox_close(&mailbox);
dlist_free(&dl);
free(owner);
return ret;
}
/* Perform a PUT request on a WebDAV notification resource */
static int notify_put(struct transaction_t *txn, void *obj,
struct mailbox *mailbox, const char *resource,
void *destdb, unsigned flags __attribute__((unused)))
{
struct webdav_db *db = (struct webdav_db *)destdb;
xmlDocPtr doc = (xmlDocPtr) obj;
xmlNodePtr root, dtstamp, type = NULL, node;
struct webdav_data *wdata;
struct index_record *oldrecord = NULL, record;
struct buf *xmlbuf;
int r;
/* Validate the data */
if (!doc) return HTTP_FORBIDDEN;
/* Find message UID for the resource */
webdav_lookup_resource(db, mailbox->name, resource, &wdata, 0);
if (wdata->dav.imap_uid) {
/* Fetch index record for the resource */
oldrecord = &record;
mailbox_find_index_record(mailbox, wdata->dav.imap_uid, oldrecord);
}
/* Get type of notification */
if ((root = xmlDocGetRootElement(doc)) &&
(dtstamp = xmlFirstElementChild(root))) {
type = xmlNextElementSibling(dtstamp);
}
/* Create and cache RFC 5322 header fields for resource */
if (type) {
struct buf buf = BUF_INITIALIZER;
xmlChar *value;
time_t t;
struct dlist *dl, *al;
xmlAttrPtr attr;
spool_replace_header(xstrdup("Subject"),
xstrdup((char *) type->name), txn->req_hdrs);
/* Create a dlist representing type, namespace, and attribute(s) */
value = xmlNodeGetContent(dtstamp);
time_from_iso8601((const char *) value, &t);
xmlFree(value);
dl = dlist_newkvlist(NULL, "N");
dlist_setdate(dl, "S", t);
dlist_setatom(dl, "NS", (char *) type->ns->href);
dlist_setatom(dl, "T", (char *) type->name);
/* Add any attributes */
al = dlist_newkvlist(dl, "A");
for (attr = type->properties; attr; attr = attr->next) {
value = xmlNodeGetContent((xmlNodePtr) attr);
dlist_setmap(al, (char *) attr->name,
(char *) value, xmlStrlen(value));
xmlFree(value);
}
/* Add any additional data */
al = dlist_newkvlist(dl, "D");
if (!xmlStrcmp(type->name, BAD_CAST SHARE_INVITE_NOTIFICATION)) {
for (node = xmlFirstElementChild(type); node;
node = xmlNextElementSibling(node)) {
if (!xmlStrcmp(node->name, BAD_CAST "sharer-resource-uri")) {
struct request_target_t tgt;
const char *errstr;
memset(&tgt, 0, sizeof(struct request_target_t));
tgt.namespace = txn->req_tgt.namespace;
value = xmlNodeGetContent(xmlFirstElementChild(node));
calcarddav_parse_path((const char *) value, &tgt,
txn->req_tgt.mboxprefix, &errstr);
xmlFree(value);
free(tgt.userid);
dlist_setatom(al, "M", tgt.mbentry->name);
mboxlist_entry_free(&tgt.mbentry);
break;
}
}
}
dlist_printbuf(dl, 1, &buf);
dlist_free(&dl);
spool_replace_header(xstrdup("Content-Description"),
buf_release(&buf), txn->req_hdrs);
}
buf_reset(&txn->buf);
buf_printf(&txn->buf, "<%s-%ld@%s>", resource, time(0), config_servername);
spool_replace_header(xstrdup("Message-ID"),
buf_release(&txn->buf), txn->req_hdrs);
buf_printf(&txn->buf, "attachment;\r\n\tfilename=\"%s\"", resource);
spool_replace_header(xstrdup("Content-Disposition"),
buf_release(&txn->buf), txn->req_hdrs);
/* Dump XML response tree into a text buffer */
xmlbuf = from_xml(doc);
if (!buf_len(xmlbuf)) r = HTTP_SERVER_ERROR;
else {
/* Store the resource */
r = dav_store_resource(txn, buf_cstring(xmlbuf), buf_len(xmlbuf),
mailbox, oldrecord, NULL);
}
buf_destroy(xmlbuf);
return r;
}
/* Callback to fetch DAV:notification-URL and CS:notification-URL */
static int propfind_notifyurl(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
xmlNodePtr node;
if (!(namespace_principal.enabled && fctx->req_tgt->userid))
return HTTP_NOT_FOUND;
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/%s/", namespace_notify.prefix,
USER_COLLECTION_PREFIX, fctx->req_tgt->userid);
if ((fctx->mode == PROPFIND_EXPAND) && xmlFirstElementChild(prop)) {
/* Return properties for this URL */
expand_property(prop, fctx, &namespace_notify, buf_cstring(&fctx->buf),
&notify_parse_path, notify_props, node, 0);
}
else {
/* Return just the URL */
xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
}
return 0;
}
/* Apple push notifications
https://github.com/apple/ccs-calendarserver/blob/master/doc/Extensions/caldav-pubsubdiscovery.txt
*/
int propfind_push_transports(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop __attribute__((unused)),
xmlNodePtr resp,
struct propstat propstat[],
void *rock __attribute__((unused)))
{
xmlNodePtr node, transport, subscription_url;
assert(fctx->req_tgt->namespace->id == URL_NS_CALENDAR ||
fctx->req_tgt->namespace->id == URL_NS_ADDRESSBOOK);
if (!namespace_applepush.enabled) return HTTP_NOT_FOUND;
/* Only on home sets */
if (fctx->req_tgt->collection) return HTTP_NOT_FOUND;
const char *aps_topic =
config_getstring(fctx->req_tgt->namespace->id == URL_NS_CALENDAR ?
IMAPOPT_APS_TOPIC_CALDAV : IMAPOPT_APS_TOPIC_CARDDAV);
if (!aps_topic) {
syslog(LOG_DEBUG, "aps_topic_%s not configured,"
" can't build CS:push-transports response",
fctx->req_tgt->namespace->id == URL_NS_CALENDAR ?
"caldav" : "carddav");
return HTTP_NOT_FOUND;
}
node = xml_add_prop(HTTP_OK, fctx->ns[NS_CS], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
transport = xmlNewChild(node, NULL, BAD_CAST "transport", NULL);
xmlNewProp(transport, BAD_CAST "type", BAD_CAST "APSD");
subscription_url =
xmlNewChild(transport, NULL, BAD_CAST "subscription-url", NULL);
xml_add_href(subscription_url, fctx->ns[NS_DAV], namespace_applepush.prefix);
xmlNewChild(transport, NULL, BAD_CAST "apsbundleid", BAD_CAST aps_topic);
// XXX from config, I think?
ensure_ns(fctx->ns, NS_MOBME, resp->parent, XML_NS_MOBME, "MM");
xmlNewChild(transport, fctx->ns[NS_MOBME],
BAD_CAST "env", BAD_CAST "PRODUCTION");
// XXX from config
xmlNewChild(transport, NULL,
BAD_CAST "refresh-interval", BAD_CAST "86400");
return 0;
}
int propfind_pushkey(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 (!namespace_applepush.enabled) return HTTP_NOT_FOUND;
/* Only on collections */
if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;
/* key is userid and mailbox uniqueid */
buf_reset(&fctx->buf);
buf_printf(&fctx->buf, "%s/%s",
fctx->req_tgt->userid, fctx->mailbox->uniqueid);
xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);
return 0;
}
struct userid_rights {
long positive;
long negative;
};
static void parse_acl(hash_table *table, const char *origacl)
{
char *acl = xstrdupsafe(origacl);
char *thisid, *rights, *nextid;
for (thisid = acl; *thisid; thisid = nextid) {
struct userid_rights *id_rights;
int is_negative = 0;
int mask;
rights = strchr(thisid, '\t');
if (!rights) {
break;
}
*rights++ = '\0';
nextid = strchr(rights, '\t');
if (!nextid) {
rights[-1] = '\t';
break;
}
*nextid++ = '\0';
if (*thisid == '-') {
is_negative = 1;
thisid++;
}
id_rights = hash_lookup(thisid, table);
if (!id_rights) {
id_rights = xzmalloc(sizeof(struct userid_rights));
hash_insert(thisid, id_rights, table);
}
cyrus_acl_strtomask(rights, &mask);
/* XXX and if strtomask fails? */
if (is_negative) id_rights->negative |= mask;
else id_rights->positive |= mask;
}
free(acl);
}
struct invite_rock {
const char *owner;
int is_shared;
struct propfind_ctx *fctx;
xmlNodePtr node;
int legacy;
};
static void xml_add_sharee(const char *userid, void *data, void *rock)
{
struct userid_rights *id_rights = (struct userid_rights *) data;
struct invite_rock *irock = (struct invite_rock *) rock;
int rights = id_rights->positive & ~id_rights->negative;
struct auth_state *authstate;
int isadmin;
if ((rights & DACL_SHARE) != DACL_SHARE) return; /* not shared */
if (!strcmp(userid, irock->owner)) return; /* user is owner */
authstate = auth_newstate(userid);
isadmin = global_authisa(authstate, IMAPOPT_ADMINS);
auth_freestate(authstate);
if (isadmin) return; /* user is an admin */
irock->is_shared = 1;
if (irock->node) {
xmlNodePtr sharee, access;
const char *annot = DAV_ANNOT_NS "<" XML_NS_DAV ">invite-status";
const char *resp = "invite-noresponse";
struct buf value = BUF_INITIALIZER;
int r;
sharee = xmlNewChild(irock->node, NULL,
BAD_CAST (irock->legacy ? "user" : "sharee"), NULL);
buf_reset(&irock->fctx->buf);
if (strchr(userid, '@')) {
buf_printf(&irock->fctx->buf, "mailto:%s", userid);
}
else {
const char *domain = httpd_extradomain;
if (!domain) domain = config_defdomain;
if (!domain) domain = config_servername;
buf_printf(&irock->fctx->buf, "mailto:%s@%s", userid, domain);
}
xml_add_href(sharee, irock->fctx->ns[NS_DAV],
buf_cstring(&irock->fctx->buf));
/* Lookup invite status */
r = annotatemore_lookupmask(irock->fctx->mbentry->name,
annot, userid, &value);
if (!r && buf_len(&value)) resp = buf_cstring(&value);
xmlNewChild(sharee, NULL, BAD_CAST resp, NULL);
buf_free(&value);
access = xmlNewChild(sharee, NULL,
BAD_CAST (irock->legacy ? "access" :
"share-access"), NULL);
if ((rights & DACL_SHARERW) == DACL_SHARERW)
xmlNewChild(access, NULL, BAD_CAST "read-write", NULL);
else xmlNewChild(access, NULL, BAD_CAST "read", NULL);
}
}
void xml_add_shareaccess(struct propfind_ctx *fctx,
xmlNodePtr resp, xmlNodePtr node, int legacy)
{
if (mboxname_userownsmailbox(fctx->req_tgt->userid, fctx->mbentry->name)) {
hash_table table;
struct invite_rock irock = { fctx->req_tgt->userid, 0, NULL, NULL, 0 };
construct_hash_table(&table, 10, 1);
parse_acl(&table, fctx->mbentry->acl);
hash_enumerate(&table, &xml_add_sharee, &irock);
if (irock.is_shared) {
xmlNsPtr ns = fctx->ns[NS_DAV];
if (legacy) {
ensure_ns(fctx->ns, NS_CS, resp->parent, XML_NS_CS, "CS");
ns = fctx->ns[NS_CS];
}
xmlNewChild(node, ns, BAD_CAST "shared-owner", NULL);
}
else if (!legacy)
xmlNewChild(node, NULL, BAD_CAST "not-shared", NULL);
free_hash_table(&table, &free);
}
else if (legacy) {
ensure_ns(fctx->ns, NS_CS, resp->parent, XML_NS_CS, "CS");
xmlNewChild(node, fctx->ns[NS_CS], BAD_CAST "shared", NULL);
}
else {
int rights = httpd_myrights(httpd_authstate, fctx->mbentry);
if ((rights & DACL_SHARERW) == DACL_SHARERW)
xmlNewChild(node, NULL, BAD_CAST "read-write", NULL);
else
xmlNewChild(node, NULL, BAD_CAST "read", NULL);
}
}
/* Callback to fetch DAV:share-access */
int propfind_shareaccess(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop __attribute__((unused)),
xmlNodePtr resp,
struct propstat propstat[],
void *rock __attribute__((unused)))
{
xmlNodePtr node;
if (!fctx->mbentry) return HTTP_NOT_FOUND;
node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
&propstat[PROPSTAT_OK], name, ns, NULL, 0);
xml_add_shareaccess(fctx, resp, node, 0 /* legacy */);
return 0;
}
/* Callback to fetch DAV:invite and CS:invite */
int propfind_invite(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop __attribute__((unused)),
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[], void *rock)
{
struct invite_rock irock = { fctx->req_tgt->userid, 0 /* is_shared */,
fctx, NULL, rock != 0 /* legacy */ };
xmlNodePtr node;
fctx->flags.cs_sharing = (rock != 0);
if (!fctx->mbentry) return HTTP_NOT_FOUND;
node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
&propstat[PROPSTAT_OK], name, ns, NULL, 0);
irock.node = node;
if (mboxname_userownsmailbox(fctx->req_tgt->userid, fctx->mbentry->name)) {
hash_table table;
construct_hash_table(&table, 10, 1);
parse_acl(&table, fctx->mbentry->acl);
hash_enumerate(&table, &xml_add_sharee, &irock);
free_hash_table(&table, &free);
}
else {
struct userid_rights id_rights =
{ httpd_myrights(httpd_authstate, fctx->mbentry), 0 /* neg */};
irock.owner = "";
xml_add_sharee(fctx->req_tgt->userid, &id_rights, &irock);
}
return 0;
}
/* Callback to fetch DAV:sharer-resource-uri and CS:shared-url */
int propfind_sharedurl(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop __attribute__((unused)),
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[], void *rock)
{
mbname_t *mbname;
const strarray_t *boxes;
int n, size;
xmlNodePtr node;
fctx->flags.cs_sharing = (rock != 0);
mbname = mbname_from_intname(fctx->mailbox->name);
if (!strcmpsafe(mbname_userid(mbname), fctx->req_tgt->userid)) {
mbname_free(&mbname);
return HTTP_NOT_FOUND;
}
buf_setcstr(&fctx->buf, fctx->req_tgt->namespace->prefix);
if (mbname_localpart(mbname)) {
const char *domain =
mbname_domain(mbname) ? mbname_domain(mbname) : httpd_extradomain;
buf_printf(&fctx->buf, "/%s/%s",
USER_COLLECTION_PREFIX, mbname_localpart(mbname));
if (domain) buf_printf(&fctx->buf, "@%s", domain);
}
buf_putc(&fctx->buf, '/');
boxes = mbname_boxes(mbname);
size = strarray_size(boxes);
for (n = 1; n < size; n++) {
buf_appendcstr(&fctx->buf, strarray_nth(boxes, n));
buf_putc(&fctx->buf, '/');
}
node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
&propstat[PROPSTAT_OK], name, ns, NULL, 0);
xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
mbname_free(&mbname);
return 0;
}
/* Callback to fetch DAV:notificationtype */
static int propfind_notifytype(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop __attribute__((unused)),
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[], void *rock)
{
struct webdav_data *wdata = (struct webdav_data *) fctx->data;
xmlNodePtr node;
xmlNsPtr type_ns = NULL;
struct dlist *dl = NULL, *al, *item;
const char *ns_href, *type;
int i;
if (!wdata->filename) return HTTP_NOT_FOUND;
node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
/* Parse dlist representing notification type, namespace, and attributes */
dlist_parsemap(&dl, 1, 0, wdata->filename, strlen(wdata->filename));
dlist_getatom(dl, "T", &type);
dlist_getatom(dl, "NS", &ns_href);
dlist_getlist(dl, "A", &al);
if (rock /* calendarserver-sharing */) {
if (!strcmp(type, SHARE_INVITE_NOTIFICATION))
type = "invite-notification";
else if (!strcmp(type, SHARE_REPLY_NOTIFICATION))
type = "invite-reply";
}
else {
/* Check if we already have this ns-href, otherwise create a new one */
type_ns = xmlSearchNsByHref(node->doc, node, BAD_CAST ns_href);
if (!type_ns) {
char prefix[20];
snprintf(prefix, sizeof(prefix), "X%X", strhash(ns_href) & 0xffff);
type_ns = xmlNewNs(node, BAD_CAST ns_href, BAD_CAST prefix);
}
}
/* Create node for type */
node = xmlNewChild(node, type_ns, BAD_CAST type, NULL);
/* Add attributes */
for (i = 0; (item = dlist_getchildn(al, i)); i++) {
xmlNewProp(node, BAD_CAST item->name, BAD_CAST dlist_cstring(item));
}
dlist_free(&dl);
return 0;
}
/* Parse request-target path in DAV notifications namespace */
static int notify_parse_path(const char *path, struct request_target_t *tgt,
const char **errstr)
{
char *p;
size_t len;
mbname_t *mbname = NULL;
/* 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_notify.prefix);
if (strlen(p) < len ||
strncmp(namespace_notify.prefix, p, len) ||
(path[len] && path[len] != '/')) {
*errstr = "Namespace mismatch request target path";
return HTTP_FORBIDDEN;
}
tgt->mboxprefix = config_getstring(IMAPOPT_DAVNOTIFICATIONSPREFIX);
/* Default to bare-bones Allow bits */
tgt->allow &= ALLOW_READ_MASK;
/* Skip namespace */
p += len;
if (!*p || !*++p) return 0;
/* Check if we're in user space */
len = strcspn(p, "/");
if (!strncmp(p, USER_COLLECTION_PREFIX, len)) {
p += len;
if (!*p || !*++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;
}
p += len;
if (!*p || !*++p) goto done;
}
else return HTTP_NOT_FOUND; /* need to specify a userid */
/* Get resource */
len = strcspn(p, "/");
tgt->resource = p;
tgt->reslen = len;
p += len;
if (*p) {
// *errstr = "Too many segments in request target path";
return HTTP_NOT_FOUND;
}
done:
/* Create mailbox name from the parsed path */
mbname = mbname_from_userid(tgt->userid);
mbname_push_boxes(mbname, config_getstring(IMAPOPT_DAVNOTIFICATIONSPREFIX));
/* 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))
return HTTP_NOT_FOUND;
mbname_set_domain(mbname, NULL);
}
const char *mboxname = mbname_intname(mbname);
if (*mboxname) {
/* Locate the mailbox */
int r = http_mlookup(mboxname, &tgt->mbentry, NULL);
if (r) {
syslog(LOG_ERR, "mlookup(%s) failed: %s",
mboxname, error_message(r));
*errstr = error_message(r);
mbname_free(&mbname);
switch (r) {
case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN;
case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND;
default: return HTTP_SERVER_ERROR;
}
}
}
mbname_free(&mbname);
/* Set proper Allow bits based on path components */
tgt->allow |= ALLOW_ACL;
if (tgt->resource) tgt->allow |= ALLOW_POST | ALLOW_DELETE;
return 0;
}
static int propfind_csnotify_collection(struct propfind_ctx *fctx,
xmlNodePtr props)
{
struct propfind_ctx my_fctx;
struct request_target_t tgt;
struct propfind_entry_list *elist;
const char *err = NULL;
/* Populate our propfind context for notification collection */
memset(&my_fctx, 0, sizeof(struct propfind_ctx));
buf_printf(&my_fctx.buf, "%s/%s/%s/", namespace_notify.prefix,
USER_COLLECTION_PREFIX, fctx->req_tgt->userid);
memset(&tgt, 0, sizeof(struct request_target_t));
tgt.namespace = &namespace_notify;
notify_parse_path(buf_cstring(&my_fctx.buf), &tgt, &err);
my_fctx.txn = fctx->txn;
my_fctx.req_tgt = &tgt;
my_fctx.mode = fctx->mode;
my_fctx.depth = fctx->depth;
my_fctx.prefer = fctx->prefer & PREFER_MIN;
my_fctx.userid = httpd_userid;
my_fctx.userisadmin = httpd_userisadmin;
my_fctx.authstate = httpd_authstate;
my_fctx.mbentry = NULL;
my_fctx.mailbox = NULL;
my_fctx.record = NULL;
my_fctx.reqd_privs = DACL_READ;
my_fctx.filter = NULL;
my_fctx.filter_crit = NULL;
my_fctx.open_db = notify_params.davdb.open_db;
my_fctx.close_db = notify_params.davdb.close_db;
my_fctx.lookup_resource = notify_params.davdb.lookup_resource;
my_fctx.foreach_resource = notify_params.davdb.foreach_resource;
my_fctx.proc_by_resource = &propfind_by_resource;
my_fctx.elist = NULL;
my_fctx.lprops = notify_params.propfind.lprops;
my_fctx.root = fctx->root;
my_fctx.ns = fctx->ns;
my_fctx.ns_table = fctx->ns_table;
my_fctx.ret = fctx->ret;
/* Parse the list of properties and build a list of callbacks */
preload_proplist(props, &my_fctx);
/* Add response for target collection */
propfind_by_collection(tgt.mbentry, &my_fctx);
free(tgt.userid);
mboxlist_entry_free(&tgt.mbentry);
/* Free the entry list */
elist = my_fctx.elist;
while (elist) {
struct propfind_entry_list *freeme = elist;
elist = elist->next;
xmlFree(freeme->name);
free(freeme);
}
buf_free(&my_fctx.buf);
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, &param, 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;
}

File Metadata

Mime Type
text/x-c
Expires
Mon, Apr 6, 2:25 AM (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18832054
Default Alt Text
http_dav.c (350 KB)

Event Timeline