/* http_carddav.c -- Routines for handling CardDAV collections in httpd
*
* Copyright (c) 1994-2013 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:
* Support <filter> for addressbook-query Report
*
*/
#include <config.h>
#include <sysexits.h>
#include <syslog.h>
#include <libxml/tree.h>
#include <libxml/uri.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "acl.h"
#include "append.h"
#include "carddav_db.h"
#include "global.h"
#include "hash.h"
#include "httpd.h"
#include "http_dav.h"
#include "http_dav_sharing.h"
#include "http_proxy.h"
#include "index.h"
#include "mailbox.h"
#include "mboxlist.h"
#include "message.h"
#include "message_guid.h"
#include "proxy.h"
#include "smtpclient.h"
#include "spool.h"
#include "strhash.h"
#include "stristr.h"
#include "times.h"
#include "user.h"
#include "util.h"
#include "vcard_support.h"
#include "version.h"
#include "vparse.h"
#include "xmalloc.h"
#include "xml_support.h"
#include "xstrlcat.h"
#include "xstrlcpy.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include "imap/imap_err.h"
static struct carddav_db *auth_carddavdb = NULL;
static time_t compile_time;
static void my_carddav_init(struct buf *serverinfo);
static int my_carddav_auth(const char *userid);
static void my_carddav_reset(void);
static void my_carddav_shutdown(void);
static int carddav_parse_path(const char *path, struct request_target_t *tgt,
const char **resultstr);
static int carddav_copy(struct transaction_t *txn, void *obj,
struct mailbox *mailbox, const char *resource,
void *destdb, unsigned flags);
static int carddav_get(struct transaction_t *txn, struct mailbox *mailbox,
struct index_record *record, void *data, void **obj);
static int carddav_put(struct transaction_t *txn, void *obj,
struct mailbox *mailbox, const char *resource,
void *destdb, unsigned flags);
static int carddav_import(struct transaction_t *txn, void *obj,
struct mailbox *mailbox, void *destdb,
xmlNodePtr root, xmlNsPtr *ns, unsigned flags);
static int propfind_getcontenttype(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_addrdata(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int propfind_suppaddrdata(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int propfind_addrgroups(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int report_card_query(struct transaction_t *txn,
struct meth_params *rparams,
xmlNodePtr inroot, struct propfind_ctx *fctx);
static struct mime_type_t carddav_mime_types[] = {
/* First item MUST be the default type and storage format */
{ "text/vcard; charset=utf-8", "3.0", "vcf",
(struct buf* (*)(void *)) &vcard_as_buf,
(void * (*)(const struct buf*)) &vcard_parse_buf,
(void (*)(void *)) &vparse_free_card, NULL, NULL
},
{ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};
/* Array of supported REPORTs */
static const struct report_type_t carddav_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_NEED_PROPS | REPORT_DEPTH_ZERO },
/* WebDAV Sync (RFC 6578) REPORTs */
{ "sync-collection", NS_DAV, "multistatus", &report_sync_col,
DACL_READ, REPORT_NEED_MBOX | REPORT_NEED_PROPS },
/* CardDAV (RFC 6352) REPORTs */
{ "addressbook-query", NS_CARDDAV, "multistatus", &report_card_query,
DACL_READ, REPORT_NEED_MBOX | REPORT_ALLOW_PROPS },
{ "addressbook-multiget", NS_CARDDAV, "multistatus", &report_multiget,
DACL_READ, REPORT_NEED_MBOX | REPORT_ALLOW_PROPS },
{ NULL, 0, NULL, NULL, 0, 0 }
};
/* Array of known "live" properties */
static const struct prop_entry carddav_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 | PROP_PERUSER,
propfind_collectionname, 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_COLLECTION | PROP_RESOURCE,
propfind_getcontenttype, 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 },
{ "lockdiscovery", NS_DAV,
PROP_ALLPROP | PROP_RESOURCE,
propfind_lockdisc, NULL, NULL },
{ "resourcetype", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE | PROP_PRESCREEN,
propfind_restype, proppatch_restype, "addressbook" },
{ "supportedlock", NS_DAV,
PROP_ALLPROP | PROP_RESOURCE,
propfind_suplock, NULL, NULL },
/* WebDAV Versioning (RFC 3253) properties */
{ "supported-report-set", NS_DAV,
PROP_COLLECTION | PROP_PRESCREEN,
propfind_reportset, NULL, (void *) carddav_reports },
{ "supported-method-set", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE,
propfind_methodset, NULL, (void *) &calcarddav_allow_cb },
/* WebDAV ACL (RFC 3744) properties */
{ "owner", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE,
propfind_owner, NULL, NULL },
{ "group", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE,
NULL, NULL, NULL },
{ "supported-privilege-set", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE | PROP_PRESCREEN,
propfind_supprivset, NULL, NULL },
{ "current-user-privilege-set", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE | PROP_PRESCREEN,
propfind_curprivset, NULL, NULL },
{ "acl", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE | PROP_PRESCREEN,
propfind_acl, NULL, NULL },
{ "acl-restrictions", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE,
propfind_aclrestrict, NULL, NULL },
{ "inherited-acl-set", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE,
NULL, NULL, NULL },
{ "principal-collection-set", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE,
propfind_princolset, NULL, NULL },
/* WebDAV Quota (RFC 4331) properties */
{ "quota-available-bytes", NS_DAV,
PROP_COLLECTION,
propfind_quota, NULL, NULL },
{ "quota-used-bytes", NS_DAV,
PROP_COLLECTION,
propfind_quota, NULL, NULL },
/* WebDAV Current Principal (RFC 5397) properties */
{ "current-user-principal", NS_DAV,
PROP_COLLECTION | PROP_RESOURCE,
propfind_curprin, NULL, NULL },
/* WebDAV POST (RFC 5995) properties */
{ "add-member", NS_DAV,
PROP_COLLECTION,
propfind_addmember, NULL, NULL },
/* WebDAV Sync (RFC 6578) properties */
{ "sync-token", NS_DAV,
PROP_COLLECTION,
propfind_sync_token, NULL, SYNC_TOKEN_URL_SCHEME },
/* WebDAV Sharing (draft-pot-webdav-resource-sharing) properties */
{ "share-access", NS_DAV,
PROP_COLLECTION,
propfind_shareaccess, NULL, NULL },
{ "invite", NS_DAV,
PROP_COLLECTION,
propfind_invite, NULL, NULL },
{ "sharer-resource-uri", NS_DAV,
PROP_COLLECTION,
propfind_sharedurl, NULL, NULL },
/* CardDAV (RFC 6352) properties */
{ "address-data", NS_CARDDAV,
PROP_RESOURCE | PROP_PRESCREEN | PROP_CLEANUP,
propfind_addrdata, NULL, (void *) CARDDAV_SUPP_DATA },
{ "addressbook-description", NS_CARDDAV,
PROP_COLLECTION | PROP_PERUSER,
propfind_fromdb, proppatch_todb, NULL },
{ "supported-address-data", NS_CARDDAV,
PROP_COLLECTION,
propfind_suppaddrdata, NULL, NULL },
{ "supported-collation-set", NS_CARDDAV,
PROP_COLLECTION,
propfind_collationset, NULL, NULL },
{ "max-resource-size", NS_CARDDAV,
PROP_COLLECTION,
NULL, NULL, NULL },
/* Apple Calendar Server properties */
{ "getctag", NS_CS,
PROP_ALLPROP | PROP_COLLECTION,
propfind_sync_token, NULL, "" },
/* Apple Push Notifications Service properties */
{ "push-transports", NS_CS,
PROP_COLLECTION | PROP_PRESCREEN,
propfind_push_transports, NULL, (void *) MBTYPE_ADDRESSBOOK },
{ "pushkey", NS_CS,
PROP_COLLECTION,
propfind_pushkey, NULL, NULL },
/* Cyrus properties */
{ "address-groups", NS_CYRUS,
PROP_RESOURCE,
propfind_addrgroups, NULL, NULL },
{ NULL, 0, 0, NULL, NULL, NULL }
};
static struct meth_params carddav_params = {
carddav_mime_types,
&carddav_parse_path,
&dav_get_validators,
&dav_get_modseq,
&dav_check_precond,
{ (db_open_proc_t) &carddav_open_mailbox,
(db_close_proc_t) &carddav_close,
(db_proc_t) &carddav_begin,
(db_proc_t) &carddav_commit,
(db_proc_t) &carddav_abort,
(db_lookup_proc_t) &carddav_lookup_resource,
(db_imapuid_proc_t) &carddav_lookup_imapuid,
(db_foreach_proc_t) &carddav_foreach,
(db_updates_proc_t) &carddav_get_updates,
(db_write_proc_t) &carddav_write,
(db_delete_proc_t) &carddav_delete },
NULL, /* No ACL extensions */
{ CARDDAV_UID_CONFLICT, &carddav_copy },
NULL, /* No special DELETE handling */
&carddav_get,
{ CARDDAV_LOCATION_OK, MBTYPE_ADDRESSBOOK, NULL },
NULL, /* No PATCH handling */
{ POST_ADDMEMBER | POST_SHARE, NULL, /* No special POST handling */
{ NS_CARDDAV, "addressbook-data", &carddav_import } },
{ CARDDAV_SUPP_DATA, &carddav_put },
{ DAV_FINITE_DEPTH, carddav_props }, /* Disable infinite depth */
carddav_reports
};
/* Namespace for Carddav collections */
struct namespace_t namespace_addressbook = {
URL_NS_ADDRESSBOOK, 0, "addressbook", "/dav/addressbooks", "/.well-known/carddav",
http_allow_noauth_get, /*authschemes*/0,
MBTYPE_ADDRESSBOOK,
(ALLOW_READ | ALLOW_POST | ALLOW_WRITE | ALLOW_DELETE |
ALLOW_DAV | ALLOW_PROPPATCH | ALLOW_MKCOL | ALLOW_ACL | ALLOW_CARD),
&my_carddav_init, &my_carddav_auth, my_carddav_reset, &my_carddav_shutdown,
&dav_premethod, /*bearer*/NULL,
{
{ &meth_acl, &carddav_params }, /* ACL */
{ NULL, NULL }, /* BIND */
{ NULL, NULL }, /* CONNECT */
{ &meth_copy_move, &carddav_params }, /* COPY */
{ &meth_delete, &carddav_params }, /* DELETE */
{ &meth_get_head, &carddav_params }, /* GET */
{ &meth_get_head, &carddav_params }, /* HEAD */
{ &meth_lock, &carddav_params }, /* LOCK */
{ NULL, NULL }, /* MKCALENDAR */
{ &meth_mkcol, &carddav_params }, /* MKCOL */
{ &meth_copy_move, &carddav_params }, /* MOVE */
{ &meth_options, &carddav_parse_path }, /* OPTIONS */
{ NULL, NULL }, /* PATCH */
{ &meth_post, &carddav_params }, /* POST */
{ &meth_propfind, &carddav_params }, /* PROPFIND */
{ &meth_proppatch, &carddav_params }, /* PROPPATCH */
{ &meth_put, &carddav_params }, /* PUT */
{ &meth_report, &carddav_params }, /* REPORT */
{ &meth_trace, &carddav_parse_path }, /* TRACE */
{ NULL, NULL }, /* UNBIND */
{ &meth_unlock, &carddav_params } /* UNLOCK */
}
};
static void my_carddav_init(struct buf *serverinfo __attribute__((unused)))
{
namespace_addressbook.enabled =
config_httpmodules & IMAP_ENUM_HTTPMODULES_CARDDAV;
if (!namespace_addressbook.enabled) return;
if (!config_getstring(IMAPOPT_ADDRESSBOOKPREFIX)) {
fatal("Required 'addressbookprefix' option is not set", EX_CONFIG);
}
carddav_init();
namespace_principal.enabled = 1;
/* Apple clients check principal resources for these DAV tokens */
namespace_principal.allow |= ALLOW_CARD;
compile_time = calc_compile_time(__TIME__, __DATE__);
}
#define DEFAULT_ADDRBOOK "Default"
static int _create_mailbox(const char *userid, const char *mailboxname, int type,
const char *displayname, struct mboxlock **namespacelockp)
{
int r = 0;
struct mailbox *mailbox = NULL;
r = mboxlist_lookup(mailboxname, NULL, NULL);
if (!r) return 0;
if (r != IMAP_MAILBOX_NONEXISTENT) return r;
if (!*namespacelockp) {
*namespacelockp = mboxname_usernamespacelock(mailboxname);
// maybe we lost the race on this one
r = mboxlist_lookup(mailboxname, NULL, NULL);
if (!r) return 0;
if (r != IMAP_MAILBOX_NONEXISTENT) return r;
}
/* Create locally */
r = mboxlist_createmailbox(mailboxname, type,
NULL, 0,
userid, httpd_authstate,
0, 0, 0, 0, displayname ? &mailbox : NULL);
if (!r && displayname) {
annotate_state_t *astate = NULL;
r = mailbox_get_annotate_state(mailbox, 0, &astate);
if (!r) {
const char *disp_annot = DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
struct buf value = BUF_INITIALIZER;
buf_init_ro_cstr(&value, displayname);
r = annotate_state_writemask(astate, disp_annot, userid, &value);
buf_free(&value);
}
mailbox_close(&mailbox);
}
if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
mailboxname, error_message(r));
return r;
}
EXPORTED int carddav_create_defaultaddressbook(const char *userid) {
struct mboxlock *namespacelock = NULL;
/* addressbook-home-set */
mbname_t *mbname = mbname_from_userid(userid);
mbname_push_boxes(mbname, config_getstring(IMAPOPT_ADDRESSBOOKPREFIX));
int r = mboxlist_lookup(mbname_intname(mbname), NULL, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* Find location of INBOX */
char *inboxname = mboxname_user_mbox(userid, NULL);
mbentry_t *mbentry = NULL;
r = http_mlookup(inboxname, &mbentry, NULL);
free(inboxname);
if (r == IMAP_MAILBOX_NONEXISTENT) r = IMAP_INVALID_USER;
if (!r && mbentry->server) {
proxy_findserver(mbentry->server, &http_protocol, httpd_userid,
&backend_cached, NULL, NULL, httpd_in);
mboxlist_entry_free(&mbentry);
goto done;
}
mboxlist_entry_free(&mbentry);
if (!r) r = _create_mailbox(userid, mbname_intname(mbname),
MBTYPE_ADDRESSBOOK, NULL,
&namespacelock);
}
if (r) goto done;
/* Default addressbook */
mbname_push_boxes(mbname, DEFAULT_ADDRBOOK);
r = mboxlist_lookup(mbname_intname(mbname), NULL, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) {
r = _create_mailbox(userid, mbname_intname(mbname),
MBTYPE_ADDRESSBOOK, "personal",
&namespacelock);
}
done:
mboxname_release(&namespacelock);
mbname_free(&mbname);
return r;
}
static int my_carddav_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 0;
}
else if (config_mupdate_server && !config_getstring(IMAPOPT_PROXYSERVERS)) {
/* proxy-only server - won't have DAV databases */
return 0;
}
else {
/* Open CardDAV DB for 'userid' */
my_carddav_reset();
auth_carddavdb = carddav_open_userid(userid);
if (!auth_carddavdb) {
syslog(LOG_ERR, "Unable to open CardDAV DB for userid: %s", userid);
return HTTP_UNAVAILABLE;
}
}
/* Auto-provision an addressbook for 'userid' */
int r = carddav_create_defaultaddressbook(userid);
if (r) {
syslog(LOG_ERR, "could not autoprovision addressbook for userid %s: %s",
userid, error_message(r));
return HTTP_SERVER_ERROR;
}
return 0;
}
static void my_carddav_reset(void)
{
if (auth_carddavdb) carddav_close(auth_carddavdb);
auth_carddavdb = NULL;
}
static void my_carddav_shutdown(void)
{
my_carddav_reset();
carddav_done();
}
/* Parse request-target path in CardDAV namespace */
static int carddav_parse_path(const char *path, struct request_target_t *tgt,
const char **resultstr)
{
return calcarddav_parse_path(path, tgt,
config_getstring(IMAPOPT_ADDRESSBOOKPREFIX),
resultstr);
}
/* Perform a COPY/MOVE/PUT request
*
* preconditions:
* CARDDAV:valid-address-data
* CARDDAV:no-uid-conflict (DAV:href)
* CARDDAV:max-resource-size
*/
static int store_resource(struct transaction_t *txn,
struct vparse_card *vcard,
struct mailbox *mailbox, const char *resource,
struct carddav_db *davdb, int dupcheck)
{
struct vparse_entry *ventry;
struct carddav_data *cdata;
const char *version = NULL, *uid = NULL, *fullname = NULL;
struct index_record *oldrecord = NULL, record;
char *mimehdr;
/* Validate the vCard data */
if (!vcard ||
!vcard->objects ||
!vcard->objects->type ||
strcasecmp(vcard->objects->type, "vcard")) {
txn->error.precond = CARDDAV_VALID_DATA;
return HTTP_FORBIDDEN;
}
/* Fetch some important properties */
for (ventry = vcard->objects->properties; ventry; ventry = ventry->next) {
const char *name = ventry->name;
const char *propval = ventry->v.value;
if (!name) continue;
if (!propval) continue;
if (!strcasecmp(name, "version")) {
version = propval;
if (strcmp(version, "3.0")) {
txn->error.precond = CARDDAV_SUPP_DATA;
return HTTP_FORBIDDEN;
}
}
else if (!strcasecmp(name, "uid"))
uid = propval;
else if (!strcasecmp(name, "fn"))
fullname = propval;
}
/* Sanity check data */
if (!version || !uid || !fullname) {
txn->error.precond = CARDDAV_VALID_DATA;
return HTTP_FORBIDDEN;
}
/* Check for changed UID on existing resource */
carddav_lookup_resource(davdb, mailbox->name, resource, &cdata, 0);
if (cdata->dav.imap_uid && strcmpsafe(cdata->vcard_uid, uid)) {
char *owner = mboxname_to_userid(cdata->dav.mailbox);
txn->error.precond = CARDDAV_UID_CONFLICT;
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "%s/%s/%s/%s/%s",
namespace_addressbook.prefix,
USER_COLLECTION_PREFIX, owner,
strrchr(cdata->dav.mailbox, '.')+1,
cdata->dav.resource);
txn->error.resource = buf_cstring(&txn->buf);
free(owner);
return HTTP_FORBIDDEN;
}
if (dupcheck) {
/* Check for different resource with same UID */
carddav_lookup_uid(davdb, uid, &cdata);
if (cdata->dav.imap_uid && (strcmp(cdata->dav.mailbox, mailbox->name) ||
strcmp(cdata->dav.resource, resource))) {
/* CARDDAV:no-uid-conflict */
char *owner = mboxname_to_userid(cdata->dav.mailbox);
txn->error.precond = CARDDAV_UID_CONFLICT;
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "%s/%s/%s/%s/%s",
namespace_addressbook.prefix,
USER_COLLECTION_PREFIX, owner,
strrchr(cdata->dav.mailbox, '.')+1,
cdata->dav.resource);
txn->error.resource = buf_cstring(&txn->buf);
free(owner);
return HTTP_FORBIDDEN;
}
}
if (cdata->dav.imap_uid) {
/* Fetch index record for the resource */
oldrecord = &record;
mailbox_find_index_record(mailbox, cdata->dav.imap_uid, oldrecord);
}
/* Create and cache RFC 5322 header fields for resource */
mimehdr = charset_encode_mimeheader(fullname, 0, 0);
spool_replace_header(xstrdup("Subject"), mimehdr, txn->req_hdrs);
/* XXX - validate uid for mime safety? */
if (strchr(uid, '@')) {
spool_replace_header(xstrdup("Message-ID"),
xstrdup(uid), txn->req_hdrs);
}
else {
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "<%s@%s>", uid, config_servername);
spool_replace_header(xstrdup("Message-ID"),
buf_release(&txn->buf), txn->req_hdrs);
}
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "text/vcard; version=%s; charset=utf-8", version);
spool_replace_header(xstrdup("Content-Type"),
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);
spool_remove_header(xstrdup("Content-Description"), txn->req_hdrs);
/* Store the resource */
struct buf *buf = vcard_as_buf(vcard);
int r = dav_store_resource(txn, buf_cstring(buf), 0,
mailbox, oldrecord, cdata->dav.createdmodseq, NULL);
buf_destroy(buf);
return r;
}
static int carddav_copy(struct transaction_t *txn, void *obj,
struct mailbox *mailbox, const char *resource,
void *destdb, unsigned flags __attribute__((unused)))
{
struct carddav_db *db = (struct carddav_db *)destdb;
struct vparse_card *vcard = (struct vparse_card *)obj;
return store_resource(txn, vcard, mailbox, resource, db, /*dupcheck*/0);
}
static int export_addressbook(struct transaction_t *txn)
{
int ret = 0, r, precond;
struct resp_body_t *resp_body = &txn->resp_body;
struct buf *buf = &resp_body->payload;
struct mailbox *mailbox = NULL;
static char etag[33];
static const char *displayname_annot =
DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
struct buf attrib = BUF_INITIALIZER;
const char **hdr, *sep = "";
struct mime_type_t *mime = NULL;
/* Check requested MIME type:
1st entry in carddav_mime_types array MUST be default MIME type */
if ((hdr = spool_getheader(txn->req_hdrs, "Accept")))
mime = get_accept_type(hdr, carddav_mime_types);
else mime = carddav_mime_types;
if (!mime) return HTTP_NOT_ACCEPTABLE;
/* 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;
}
/* Check any preconditions */
sprintf(etag, "%u-%u-%u",
mailbox->i.uidvalidity, mailbox->i.last_uid, mailbox->i.exists);
precond = check_precond(txn, etag, mailbox->index_mtime);
switch (precond) {
case HTTP_OK:
case HTTP_NOT_MODIFIED:
/* Fill in ETag, Last-Modified, Expires, and Cache-Control */
txn->resp_body.etag = etag;
txn->resp_body.lastmod = mailbox->index_mtime;
txn->resp_body.maxage = 3600; /* 1 hr */
txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE; /* don't use stale data */
if (httpd_userid) txn->flags.cc |= CC_PRIVATE;
if (precond != HTTP_NOT_MODIFIED) break;
GCC_FALLTHROUGH
default:
/* We failed a precondition - don't perform the request */
ret = precond;
goto done;
}
/* Setup for chunked response */
txn->flags.te |= TE_CHUNKED;
txn->flags.vary |= VARY_ACCEPT;
txn->resp_body.type = mime->content_type;
/* Set filename of resource */
r = annotatemore_lookupmask(mailbox->name, displayname_annot,
httpd_userid, &attrib);
/* fall back to last part of mailbox name */
if (r || !attrib.len) buf_setcstr(&attrib, strrchr(mailbox->name, '.') + 1);
buf_reset(&txn->buf);
buf_printf(&txn->buf, "%s.%s", buf_cstring(&attrib), mime->file_ext);
txn->resp_body.dispo.fname = buf_cstring(&txn->buf);
/* Short-circuit for HEAD request */
if (txn->meth == METH_HEAD) {
response_header(HTTP_OK, txn);
ret = 0;
goto done;
}
/* vCard data in response should not be transformed */
txn->flags.cc |= CC_NOTRANSFORM;
/* Begin (converted) vCard stream */
if (mime->begin_stream)
sep = mime->begin_stream(buf, mailbox, NULL, NULL, NULL, NULL);
else buf_reset(buf);
write_body(HTTP_OK, txn, buf_cstring(buf), buf_len(buf));
struct mailbox_iter *iter =
mailbox_iter_init(mailbox, 0, ITER_SKIP_EXPUNGED|ITER_SKIP_DELETED);
const message_t *msg;
while ((msg = mailbox_iter_step(iter))) {
const struct index_record *record = msg_record(msg);
struct vparse_card *vcard;
/* Map and parse existing vCard resource */
vcard = record_to_vcard(mailbox, record);
if (vcard) {
if (r++ && *sep) {
/* Add separator, if necessary */
buf_reset(buf);
buf_printf_markup(buf, 0, "%s", sep);
write_body(0, txn, buf_cstring(buf), buf_len(buf));
}
struct buf *card_str = mime->from_object(vcard);
write_body(0, txn, buf_base(card_str), buf_len(card_str));
buf_destroy(card_str);
vparse_free_card(vcard);
}
}
mailbox_iter_done(&iter);
/* End (converted) vCard stream */
if (mime->end_stream) {
mime->end_stream(buf);
write_body(0, txn, buf_cstring(buf), buf_len(buf));
}
/* End of output */
write_body(0, txn, NULL, 0);
done:
buf_free(&attrib);
mailbox_close(&mailbox);
return ret;
}
/*
* mboxlist_findall() callback function to list addressbooks
*/
struct addr_info {
char shortname[MAX_MAILBOX_NAME];
char displayname[MAX_MAILBOX_NAME];
unsigned flags;
};
enum {
ADDR_IS_DEFAULT = (1<<0),
ADDR_CAN_DELETE = (1<<1),
ADDR_CAN_ADMIN = (1<<2),
ADDR_IS_PUBLIC = (1<<3)
};
struct list_addr_rock {
struct addr_info *addr;
unsigned len;
unsigned alloc;
};
static int list_addr_cb(const mbentry_t *mbentry, void *rock)
{
struct list_addr_rock *lrock = (struct list_addr_rock *) rock;
struct addr_info *addr;
static size_t defaultlen = 0;
char *shortname;
size_t len;
int r, rights, any_rights = 0;
static const char *displayname_annot =
DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
struct buf displayname = BUF_INITIALIZER;
if (!defaultlen) defaultlen = strlen(DEFAULT_ADDRBOOK);
/* Make sure its a addrendar */
if (mbentry->mbtype != MBTYPE_ADDRESSBOOK) goto done;
/* Make sure its readable */
rights = httpd_myrights(httpd_authstate, mbentry);
if ((rights & DACL_READ) != DACL_READ) goto done;
shortname = strrchr(mbentry->name, '.') + 1;
len = strlen(shortname);
/* Lookup DAV:displayname */
r = annotatemore_lookupmask(mbentry->name, displayname_annot,
httpd_userid, &displayname);
/* fall back to the last part of the mailbox name */
if (r || !displayname.len) buf_setcstr(&displayname, shortname);
/* Make sure we have room in our array */
if (lrock->len == lrock->alloc) {
lrock->alloc += 100;
lrock->addr = xrealloc(lrock->addr,
lrock->alloc * sizeof(struct addr_info));
}
/* Add our addressbook to the array */
addr = &lrock->addr[lrock->len];
strlcpy(addr->shortname, shortname, MAX_MAILBOX_NAME);
strlcpy(addr->displayname, buf_cstring(&displayname), MAX_MAILBOX_NAME);
addr->flags = 0;
/* Is this the default addressbook? */
if (len == defaultlen && !strncmp(shortname, SCHED_DEFAULT, defaultlen)) {
addr->flags |= ADDR_IS_DEFAULT;
}
/* Can we delete this addrendar? */
else if (rights & DACL_RMCOL) {
addr->flags |= ADDR_CAN_DELETE;
}
/* Can we admin this addressbook? */
if (rights & DACL_ADMIN) {
addr->flags |= ADDR_CAN_ADMIN;
}
/* Is this addressbook public? */
if (mbentry->acl) {
struct auth_state *auth_anyone = auth_newstate("anyone");
any_rights = cyrus_acl_myrights(auth_anyone, mbentry->acl);
auth_freestate(auth_anyone);
}
if ((any_rights & DACL_READ) == DACL_READ) {
addr->flags |= ADDR_IS_PUBLIC;
}
lrock->len++;
done:
buf_free(&displayname);
return 0;
}
static int addr_compare(const void *a, const void *b)
{
struct addr_info *c1 = (struct addr_info *) a;
struct addr_info *c2 = (struct addr_info *) b;
return strcmp(c1->displayname, c2->displayname);
}
/* Create a HTML document listing all addressbooks available to the user */
static int list_addressbooks(struct transaction_t *txn)
{
int ret = 0, precond, rights;
char mboxlist[MAX_MAILBOX_PATH+1];
struct stat sbuf;
time_t lastmod;
const char *etag, *base_path = txn->req_tgt.path;
unsigned level = 0, i;
struct buf *body = &txn->resp_body.payload;
struct list_addr_rock lrock;
const char *proto = NULL;
const char *host = NULL;
#include "imap/http_carddav_js.h"
/* stat() mailboxes.db for Last-Modified and ETag */
snprintf(mboxlist, MAX_MAILBOX_PATH, "%s%s", config_dir, FNAME_MBOXLIST);
stat(mboxlist, &sbuf);
lastmod = MAX(compile_time, sbuf.st_mtime);
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, TIME_T_FMT "-" TIME_T_FMT "-" OFF_T_FMT,
compile_time, sbuf.st_mtime, sbuf.st_size);
/* stat() config file for Last-Modified and ETag */
stat(config_filename, &sbuf);
lastmod = MAX(lastmod, sbuf.st_mtime);
buf_printf(&txn->buf, "-" TIME_T_FMT "-" OFF_T_FMT, sbuf.st_mtime, sbuf.st_size);
etag = buf_cstring(&txn->buf);
/* Check any preconditions */
precond = check_precond(txn, etag, lastmod);
switch (precond) {
case HTTP_OK:
case HTTP_NOT_MODIFIED:
/* Fill in ETag, Last-Modified, and Expires */
txn->resp_body.etag = etag;
txn->resp_body.lastmod = lastmod;
txn->flags.cc |= CC_REVALIDATE;
if (precond != HTTP_NOT_MODIFIED) break;
GCC_FALLTHROUGH
default:
/* We failed a precondition - don't perform the request */
ret = precond;
goto done;
}
/* Setup for chunked response */
txn->flags.te |= TE_CHUNKED;
txn->resp_body.type = "text/html; charset=utf-8";
/* Short-circuit for HEAD request */
if (txn->meth == METH_HEAD) {
response_header(HTTP_OK, txn);
goto done;
}
/* Send HTML header */
buf_reset(body);
buf_printf_markup(body, level, HTML_DOCTYPE);
buf_printf_markup(body, level++, "<html>");
buf_printf_markup(body, level++, "<head>");
buf_printf_markup(body, level, "<title>%s</title>", "Available Addressbooks");
buf_printf_markup(body, level++, "<script type=\"text/javascript\">");
buf_appendcstr(body, "//<![CDATA[\n");
buf_printf(body, (const char *) http_carddav_js,
CYRUS_VERSION, http_carddav_js_len);
buf_appendcstr(body, "//]]>\n");
buf_printf_markup(body, --level, "</script>");
buf_printf_markup(body, level++, "<noscript>");
buf_printf_markup(body, level, "<i>*** %s ***</i>",
"JavaScript required to create/modify/delete addressbooks");
buf_printf_markup(body, --level, "</noscript>");
buf_printf_markup(body, --level, "</head>");
buf_printf_markup(body, level++, "<body>");
write_body(HTTP_OK, txn, buf_cstring(body), buf_len(body));
buf_reset(body);
/* Check ACL for current user */
rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
if (rights & DACL_MKCOL) {
/* Add "create" form */
buf_printf_markup(body, level, "<h2>%s</h2>", "Create New Addressbook");
buf_printf_markup(body, level++, "<form name='create'>");
buf_printf_markup(body, level++, "<table cellpadding=5>");
buf_printf_markup(body, level++, "<tr>");
buf_printf_markup(body, level, "<td align=right>Name:</td>");
buf_printf_markup(body, level,
"<td><input name=name size=30 maxlength=40></td>");
buf_printf_markup(body, --level, "</tr>");
buf_printf_markup(body, level++, "<tr>");
buf_printf_markup(body, level, "<td align=right>Description:</td>");
buf_printf_markup(body, level,
"<td><input name=desc size=75 maxlength=120></td>");
buf_printf_markup(body, --level, "</tr>");
buf_printf_markup(body, level++, "<tr>");
buf_printf_markup(body, level, "<td></td>");
buf_printf_markup(body, level,
"<td><br><input type=button value='Create'"
" onclick=\"createAddressbook('%s')\">"
" <input type=reset></td>",
base_path);
buf_printf_markup(body, --level, "</tr>");
buf_printf_markup(body, --level, "</table>");
buf_printf_markup(body, --level, "</form>");
buf_printf_markup(body, level, "<br><hr><br>");
write_body(0, txn, buf_cstring(body), buf_len(body));
buf_reset(body);
}
buf_printf_markup(body, level, "<h2>%s</h2>", "Available Addressbooks");
buf_printf_markup(body, level++, "<table border cellpadding=5>");
/* Create base URL for addressbooks */
http_proto_host(txn->req_hdrs, &proto, &host);
buf_reset(&txn->buf);
buf_printf(&txn->buf, "%s://%s%s", proto, host, txn->req_tgt.path);
memset(&lrock, 0, sizeof(struct list_addr_rock));
mboxlist_mboxtree(txn->req_tgt.mbentry->name,
list_addr_cb, &lrock, MBOXTREE_SKIP_ROOT);
/* Sort addressbooks by displayname */
qsort(lrock.addr, lrock.len, sizeof(struct addr_info), &addr_compare);
/* Add available addressbooks with action items */
for (i = 0; i < lrock.len; i++) {
struct addr_info *addr = &lrock.addr[i];
/* Send a body chunk once in a while */
if (buf_len(body) > PROT_BUFSIZE) {
write_body(0, txn, buf_cstring(body), buf_len(body));
buf_reset(body);
}
/* Addressbook name */
buf_printf_markup(body, level++, "<tr>");
buf_printf_markup(body, level, "<td>%s%s%s",
(addr->flags & ADDR_IS_DEFAULT) ? "<b>" : "",
addr->displayname,
(addr->flags & ADDR_IS_DEFAULT) ? "</b>" : "");
/* Download link */
buf_printf_markup(body, level, "<td><a href=\"%s%s\">Download</a></td>",
base_path, addr->shortname);
/* Delete button */
buf_printf_markup(body, level,
"<td><input type=button%s value='Delete'"
" onclick=\"deleteAddressbook('%s%s', '%s')\"></td>",
!(addr->flags & ADDR_CAN_DELETE) ? " disabled" : "",
base_path, addr->shortname, addr->displayname);
/* Public (shared) checkbox */
buf_printf_markup(body, level,
"<td><input type=checkbox%s%s name=share"
" onclick=\"shareAddressbook('%s%s', this.checked)\">"
"Public</td>",
!(addr->flags & ADDR_CAN_ADMIN) ? " disabled" : "",
(addr->flags & ADDR_IS_PUBLIC) ? " checked" : "",
base_path, addr->shortname);
buf_printf_markup(body, --level, "</tr>");
}
free(lrock.addr);
/* Finish list */
buf_printf_markup(body, --level, "</table>");
/* Finish HTML */
buf_printf_markup(body, --level, "</body>");
buf_printf_markup(body, --level, "</html>");
write_body(0, txn, buf_cstring(body), buf_len(body));
/* End of output */
write_body(0, txn, NULL, 0);
done:
return ret;
}
/* Perform a GET/HEAD request on a CardDAV resource */
static int carddav_get(struct transaction_t *txn,
struct mailbox *mailbox __attribute__((unused)),
struct index_record *record,
void *data __attribute__((unused)),
void **obj __attribute__((unused)))
{
int rights;
if (!(txn->req_tgt.collection || txn->req_tgt.userid))
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 (record && record->uid) {
/* GET on a resource */
return HTTP_CONTINUE;
}
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.collection) {
/* Download an entire addressbook collection */
return export_addressbook(txn);
}
else if (txn->req_tgt.userid &&
config_getswitch(IMAPOPT_CARDDAV_ALLOWADDRESSBOOKADMIN)) {
/* GET a list of addressbook under addressbook-home-set */
return list_addressbooks(txn);
}
/* Unknown action */
return HTTP_NO_CONTENT;
}
static int carddav_put(struct transaction_t *txn, void *obj,
struct mailbox *mailbox, const char *resource,
void *destdb, unsigned flags __attribute__((unused)))
{
struct carddav_db *db = (struct carddav_db *)destdb;
struct vparse_card *vcard = (struct vparse_card *)obj;
if (!(vcard && vcard->objects &&
vparse_restriction_check(vcard->objects))) {
txn->error.precond = CARDDAV_VALID_DATA;
return HTTP_FORBIDDEN;
}
return store_resource(txn, vcard, mailbox, resource, db, /*dupcheck*/1);
}
/* Perform a bulk import */
static int carddav_import(struct transaction_t *txn, void *obj,
struct mailbox *mailbox, void *destdb,
xmlNodePtr root, xmlNsPtr *ns, unsigned flags)
{
struct vparse_card *vcard = obj;
xmlBufferPtr xmlbuf = NULL;
size_t baselen;
if (!root) {
/* Validate the vCard data */
if (!vcard ||
!vcard->objects ||
!vcard->objects->type ||
strcasecmp(vcard->objects->type, "vcard")) {
txn->error.precond = CARDDAV_VALID_DATA;
return HTTP_FORBIDDEN;
}
return 0;
}
/* Setup for appending resource name to request path */
baselen = strlen(txn->req_tgt.path);
txn->req_tgt.resource = txn->req_tgt.path + baselen;
/* Import vCards */
while (vcard->objects) {
struct vparse_card *this, *next;
xmlNodePtr resp, node;
struct vparse_entry *entry;
const char *resource = makeuuid(), *uid, *myuid = NULL;
int r;
/* Create DAV:response element */
resp = xmlNewChild(root, ns[NS_DAV], BAD_CAST "response", NULL);
if (!resp) {
syslog(LOG_ERR,
"import_resource()): Unable to add response XML element");
fatal("import_resource()): Unable to add response XML element",
EX_SOFTWARE);
}
/* Isolate this card */
this = vcard->objects;
next = this->next;
this->next = NULL;
/* Get/create UID property */
entry = vparse_get_entry(this, NULL, "UID");
if (entry) {
uid = entry->v.value;
}
else {
myuid = uid = resource;
vparse_add_entry(this, NULL, "UID", uid);
}
/* Append a unique resource name to URL and perform a PUT */
txn->req_tgt.reslen =
snprintf(txn->req_tgt.resource, MAX_MAILBOX_PATH - baselen,
"%s.vcf", resource);
r = carddav_put(txn, vcard, mailbox,
txn->req_tgt.resource, destdb, flags);
switch (r) {
case HTTP_OK:
case HTTP_CREATED:
case HTTP_NO_CONTENT:
/* Success: Add DAV:href and DAV:propstat elements */
xml_add_href(resp, NULL, txn->req_tgt.path);
node = xmlNewChild(resp, ns[NS_DAV], BAD_CAST "propstat", NULL);
xmlNewChild(node, ns[NS_DAV], BAD_CAST "status",
BAD_CAST http_statusline(VER_1_1, HTTP_OK));
node = xmlNewChild(node, ns[NS_DAV], BAD_CAST "prop", NULL);
if (txn->resp_body.etag) {
/* Add DAV:getetag property */
xmlNewTextChild(node, ns[NS_DAV], BAD_CAST "getetag",
BAD_CAST txn->resp_body.etag);
}
if ((flags & PREFER_REP) && myuid /* we added a UID */) {
/* Add CARDDAV:addressbook-data property */
struct buf *vcardbuf = vcard_as_buf(this);
xmlNodePtr cdata = xmlNewChild(node, ns[NS_CARDDAV],
BAD_CAST "addressbook-data", NULL);
xmlAddChild(cdata, xmlNewCDataBlock(root->doc,
BAD_CAST buf_cstring(vcardbuf),
buf_len(vcardbuf)));
buf_free(vcardbuf);
}
break;
default:
/* Failure: Add DAV:href, DAV:status, and DAV:error elements */
xml_add_href(resp, NULL, NULL);
xmlNewChild(resp, ns[NS_DAV], BAD_CAST "status",
BAD_CAST http_statusline(VER_1_1, r));
node = xml_add_error(resp, &txn->error, ns);
break;
}
/* Add CS:uid property */
xmlNewTextChild(node, ns[NS_CS], BAD_CAST "uid", BAD_CAST uid);
/* Add DAV:response element for this resource to output buffer.
Only output the xmlBuffer every PROT_BUFSIZE bytes */
xml_partial_response((xmlBufferLength(xmlbuf) > PROT_BUFSIZE) ? txn : NULL,
root->doc, resp, 1, &xmlbuf);
/* Remove DAV:response element from root (no need to keep in memory) */
xmlReplaceNode(resp, NULL);
xmlFreeNode(resp);
/* Remove this vcard from the head of the list */
vparse_free_card(this);
vcard->objects = next;
/* Clear the buffer used for constructing href */
buf_reset(&txn->buf);
}
/* End XML response */
xml_partial_response(txn, root->doc, NULL /* end */, 0, &xmlbuf);
xmlBufferFree(xmlbuf);
return 0;
}
/* Callback to fetch DAV:getcontenttype */
static int propfind_getcontenttype(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_setcstr(&fctx->buf, "text/vcard; charset=utf-8");
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:resourcetype */
static int propfind_restype(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop __attribute__((unused)),
xmlNodePtr resp,
struct propstat propstat[],
void *rock __attribute__((unused)))
{
if (!propstat) {
/* Prescreen "property" request */
if (fctx->req_tgt->collection ||
(fctx->req_tgt->userid && fctx->depth >= 1) || fctx->depth >= 2) {
/* Add namespaces for possible resource types */
ensure_ns(fctx->ns, NS_CARDDAV, fctx->root, XML_NS_CARDDAV, "C");
}
return 0;
}
xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
&propstat[PROPSTAT_OK], name, ns, NULL, 0);
if (!fctx->record) {
xmlNewChild(node, NULL, BAD_CAST "collection", NULL);
if (fctx->req_tgt->collection) {
ensure_ns(fctx->ns, NS_CARDDAV,
resp ? resp->parent: node, XML_NS_CARDDAV, "C");
xmlNewChild(node, fctx->ns[NS_CARDDAV],
BAD_CAST "addressbook", NULL);
}
}
return 0;
}
static void prune_properties(struct vparse_card *vcard, strarray_t *partial)
{
struct vparse_entry **entryp = &vcard->properties;
while (*entryp) {
struct vparse_entry *entry = *entryp;
if (strarray_find_case(partial, entry->name, 0) < 0) {
*entryp = entry->next;
entry->next = NULL; /* so free doesn't walk the chain */
vparse_free_entry(entry);
}
else {
entryp = &((*entryp)->next);
}
}
}
/* Callback to prescreen/fetch CARDDAV:address-data */
static int propfind_addrdata(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
static struct mime_type_t *out_type = carddav_mime_types;
static strarray_t partial_addrdata = STRARRAY_INITIALIZER;
strarray_t *partial = &partial_addrdata;
const char *data = NULL;
size_t datalen = 0;
if (!fctx) {
/* Cleanup "property" request - free partial property array */
strarray_fini(partial);
return 0;
}
if (!propstat) {
/* Prescreen "property" request - read partial properties */
xmlNodePtr node;
/* Initialize partial property array to be empty */
strarray_init(partial);
/* Check for and parse child elements of CARDDAV:address-data */
for (node = xmlFirstElementChild(prop); node;
node = xmlNextElementSibling(node)) {
if (!xmlStrcmp(node->name, BAD_CAST "prop")) {
xmlChar *name = xmlGetProp(node, BAD_CAST "name");
if (name) {
strarray_add_case(partial, (const char *) name);
xmlFree(name);
}
}
}
}
else {
if (fctx->txn->meth != METH_REPORT) return HTTP_FORBIDDEN;
if (!out_type->content_type) return HTTP_BAD_MEDIATYPE;
if (!fctx->record) return HTTP_NOT_FOUND;
if (!fctx->msg_buf.len)
mailbox_map_record(fctx->mailbox, fctx->record, &fctx->msg_buf);
if (!fctx->msg_buf.len) return HTTP_SERVER_ERROR;
data = fctx->msg_buf.s + fctx->record->header_size;
datalen = fctx->record->size - fctx->record->header_size;
if (strarray_size(partial)) {
/* Limit returned properties */
struct vparse_card *vcard = fctx->obj;
if (!vcard) vcard = fctx->obj = vcard_parse_string(data, 1);
prune_properties(vcard->objects, partial);
/* Create vCard data from new vcard component */
buf_reset(&fctx->msg_buf);
vparse_tobuf(vcard, &fctx->msg_buf);
data = buf_cstring(&fctx->msg_buf);
datalen = buf_len(&fctx->msg_buf);
}
}
return propfind_getdata(name, ns, fctx, prop, propstat, carddav_mime_types,
&out_type, data, datalen);
}
/* Callback to fetch CARDDAV:addressbook-home-set */
int propfind_abookhome(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
xmlNodePtr node;
// XXX - should we be using httpd_userid here?
const char *userid = fctx->req_tgt->userid;
if (!(namespace_addressbook.enabled && 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);
if (strchr(userid, '@') || !httpd_extradomain) {
buf_printf(&fctx->buf, "%s/%s/%s/", namespace_addressbook.prefix,
USER_COLLECTION_PREFIX, userid);
}
else {
buf_printf(&fctx->buf, "%s/%s/%s@%s/", namespace_addressbook.prefix,
USER_COLLECTION_PREFIX, userid, httpd_extradomain);
}
if ((fctx->mode == PROPFIND_EXPAND) && xmlFirstElementChild(prop)) {
/* Return properties for this URL */
expand_property(prop, fctx, &namespace_addressbook, buf_cstring(&fctx->buf),
&carddav_parse_path, carddav_props, node, 0);
}
else {
/* Return just the URL */
xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
}
return 0;
}
/* Callback to fetch CARDDAV:supported-address-data */
static int propfind_suppaddrdata(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;
struct mime_type_t *mime;
if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;
node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
for (mime = carddav_mime_types; mime->content_type; mime++) {
xmlNodePtr type = xmlNewChild(node, fctx->ns[NS_CARDDAV],
BAD_CAST "address-data-type", NULL);
/* Trim any charset from content-type */
buf_reset(&fctx->buf);
buf_printf(&fctx->buf, "%.*s",
(int) strcspn(mime->content_type, ";"), mime->content_type);
xmlNewProp(type, BAD_CAST "content-type",
BAD_CAST buf_cstring(&fctx->buf));
if (mime->version)
xmlNewProp(type, BAD_CAST "version", BAD_CAST mime->version);
}
buf_reset(&fctx->buf);
return 0;
}
/* Callback to fetch CY:address-groups */
int propfind_addrgroups(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr prop __attribute__((unused)),
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
int r = 0;
struct carddav_db *davdb = NULL;
struct carddav_data *cdata = NULL;
strarray_t *groups;
xmlNodePtr node;
int i;
if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;
/* If we're here via report_sync_col then we don't have a db handle yet, so
* lets just manage this ourselves */
davdb = carddav_open_mailbox(fctx->mailbox);
if (davdb == NULL) {
r = HTTP_SERVER_ERROR;
goto done;
}
r = carddav_lookup_resource(davdb, fctx->req_tgt->mbentry->name,
fctx->req_tgt->resource, &cdata, 0);
if (r)
goto done;
node = xml_add_prop(HTTP_OK, fctx->ns[NS_CYRUS], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
groups = carddav_getuid_groups(davdb, cdata->vcard_uid);
if (groups == NULL)
goto done;
for (i = 0; i < strarray_size(groups); i++) {
const char *group_uid = strarray_nth(groups, i);
xmlNodePtr group = xmlNewChild(node, fctx->ns[NS_CYRUS],
BAD_CAST "address-group", NULL);
xmlAddChild(group,
xmlNewCDataBlock(fctx->root->doc,
BAD_CAST group_uid, strlen(group_uid)));
}
strarray_free(groups);
done:
carddav_close(davdb);
return r;
}
typedef enum vcardproperty_kind {
VCARD_ANY_PROPERTY = 0,
VCARD_FN_PROPERTY = 1,
VCARD_N_PROPERTY = 2,
VCARD_NICKNAME_PROPERTY = 3,
VCARD_UID_PROPERTY = 4,
VCARD_NO_PROPERTY = 1000
} vcardproperty_kind;
struct cardquery_filter {
unsigned allof : 1;
struct prop_filter *prop;
};
static unsigned vcardproperty_string_to_kind(const char *str)
{
if (!strcasecmp(str, "FN")) return VCARD_FN_PROPERTY;
else if (!strcasecmp(str, "N")) return VCARD_N_PROPERTY;
else if (!strcasecmp(str, "NICKNAME")) return VCARD_NICKNAME_PROPERTY;
else if (!strcasecmp(str, "UID")) return VCARD_UID_PROPERTY;
else return VCARD_ANY_PROPERTY;
}
static int parse_cardfilter(xmlNodePtr root, struct cardquery_filter *filter,
struct error_t *error)
{
xmlChar *attr;
xmlNodePtr node;
struct filter_profile_t profile =
{ 0 /* anyof */, COLLATION_UNICODE,
CARDDAV_SUPP_FILTER, CARDDAV_SUPP_COLLATION,
vcardproperty_string_to_kind, VCARD_NO_PROPERTY,
NULL /* param_string_to_kind */, 0 /* no_param_value */,
NULL /* parse_propfilter */ };
/* Parse elements of filter */
attr = xmlGetProp(root, BAD_CAST "test");
if (attr) {
if (!xmlStrcmp(attr, BAD_CAST "allof")) filter->allof = 1;
else if (xmlStrcmp(attr, BAD_CAST "anyof")) {
error->precond = CARDDAV_SUPP_FILTER;
error->desc = "Unsupported test";
error->node = xmlCopyNode(root, 2);
}
xmlFree(attr);
}
for (node = xmlFirstElementChild(root); node && !error->precond;
node = xmlNextElementSibling(node)) {
if (!xmlStrcmp(node->name, BAD_CAST "prop-filter")) {
struct prop_filter *prop = NULL;
dav_parse_propfilter(node, &prop, &profile, error);
if (prop) {
if (filter->prop) prop->next = filter->prop;
filter->prop = prop;
}
}
else {
error->precond = CARDDAV_SUPP_FILTER;
error->desc = "Unsupported element in filter";
error->node = xmlCopyNode(root, 1);
}
}
return error->precond ? HTTP_FORBIDDEN : 0;
}
static int apply_paramfilter(struct param_filter *paramfilter,
struct vparse_entry *prop)
{
struct vparse_param *param =
vparse_get_param(prop, (char *) paramfilter->name);
if (!param) return paramfilter->not_defined;
if (paramfilter->not_defined) return 0;
if (!paramfilter->match) return 1;
return dav_apply_textmatch(BAD_CAST param->value, paramfilter->match);
}
static int apply_propfilter(struct prop_filter *propfilter,
struct carddav_data *cdata,
struct propfind_ctx *fctx)
{
int pass = 1;
struct vparse_card *vcard = fctx->obj;
struct vparse_entry myprop, *prop = NULL;
memset(&myprop, 0, sizeof(struct vparse_entry));
if (!propfilter->param) {
switch (propfilter->kind) {
case VCARD_FN_PROPERTY:
if (cdata->fullname) myprop.v.value = (char *) cdata->fullname;
break;
case VCARD_N_PROPERTY:
if (cdata->name) myprop.v.value = (char *) cdata->name;
break;
case VCARD_NICKNAME_PROPERTY:
if (cdata->nickname) {
if (propfilter->match) {
myprop.multivaluesep = ',';
myprop.v.values =
strarray_split(cdata->nickname, ",", STRARRAY_TRIM);
}
else myprop.v.value = (char *) cdata->nickname;
}
break;
case VCARD_UID_PROPERTY:
if (cdata->vcard_uid) myprop.v.value = (char *) cdata->vcard_uid;
break;
default:
break;
}
if (myprop.v.value) prop = &myprop;
}
if (propfilter->param || (propfilter->kind == VCARD_ANY_PROPERTY)) {
/* Load message containing the resource and parse vcard data */
if (!vcard) {
if (!fctx->msg_buf.len) {
mailbox_map_record(fctx->mailbox, fctx->record, &fctx->msg_buf);
}
if (fctx->msg_buf.len) {
vcard = fctx->obj =
vcard_parse_string(buf_cstring(&fctx->msg_buf) +
fctx->record->header_size, 1);
}
if (!vcard) return 0;
}
prop = vparse_get_entry(vcard->objects, NULL, (char *) propfilter->name);
}
if (!prop) return propfilter->not_defined;
if (propfilter->not_defined) return 0;
if (!(propfilter->match || propfilter->param)) return 1;
/* Test each instance of this property (logical OR) */
do {
struct text_match_t *match;
struct param_filter *paramfilter;
if (!pass && strcasecmpsafe(prop->name, (char *) propfilter->name)) {
/* Skip property if name doesn't match */
continue;
}
pass = propfilter->allof;
/* Apply each text-match, breaking if allof fails or anyof succeeds */
for (match = propfilter->match;
match && (pass == propfilter->allof);
match = match->next) {
int n = 0;
const char *text = prop->multivaluesep ?
strarray_nth(prop->v.values, n) : prop->v.value;
/* Test each value of this property (logical OR) */
do {
pass = dav_apply_textmatch(BAD_CAST text, match);
} while (!pass && prop->multivaluesep &&
(text = strarray_nth(prop->v.values, ++n)));
}
/* Apply each param-filter, breaking if allof fails or anyof succeeds */
for (paramfilter = propfilter->param;
paramfilter && (pass == propfilter->allof);
paramfilter = paramfilter->next) {
pass = apply_paramfilter(paramfilter, prop);
}
} while (!pass && (prop = prop->next)); /* XXX No API to fetch next prop */
if (myprop.multivaluesep) strarray_free(myprop.v.values);
return pass;
}
/* See if the current resource matches the specified filter.
* Returns 1 if match, 0 otherwise.
*/
static int apply_cardfilter(struct propfind_ctx *fctx, void *data)
{
struct cardquery_filter *cardfilter =
(struct cardquery_filter *) fctx->filter_crit;
struct carddav_data *cdata = (struct carddav_data *) data;
struct prop_filter *propfilter;
int pass = 1;
for (propfilter = cardfilter->prop; propfilter;
propfilter = propfilter->next) {
pass = apply_propfilter(propfilter, cdata, fctx);
/* If allof fails or anyof succeeds, we're done */
if (pass != cardfilter->allof) break;
}
return pass;
}
static void free_cardfilter(struct cardquery_filter *cardfilter)
{
struct prop_filter *prop, *next;
for (prop = cardfilter->prop; prop; prop = next) {
next = prop->next;
dav_free_propfilter(prop);
}
}
static int report_card_query(struct transaction_t *txn,
struct meth_params *rparams __attribute__((unused)),
xmlNodePtr inroot, struct propfind_ctx *fctx)
{
int ret = 0;
xmlNodePtr node;
struct cardquery_filter cardfilter;
memset(&cardfilter, 0, sizeof(struct cardquery_filter));
fctx->filter_crit = &cardfilter;
fctx->open_db = (db_open_proc_t) &carddav_open_mailbox;
fctx->close_db = (db_close_proc_t) &carddav_close;
fctx->lookup_resource = (db_lookup_proc_t) &carddav_lookup_resource;
fctx->foreach_resource = (db_foreach_proc_t) &carddav_foreach;
fctx->proc_by_resource = &propfind_by_resource;
fctx->davdb = NULL;
/* Parse children element of report */
for (node = inroot->children; node; node = node->next) {
if (node->type == XML_ELEMENT_NODE) {
if (!xmlStrcmp(node->name, BAD_CAST "filter")) {
ret = parse_cardfilter(node, &cardfilter, &txn->error);
if (ret) goto done;
else fctx->filter = apply_cardfilter;
}
}
}
/* Setup for chunked response */
txn->flags.te |= TE_CHUNKED;
/* Begin XML response */
xml_response(HTTP_MULTI_STATUS, txn, fctx->root->doc);
if (fctx->depth++ > 0) {
/* Addressbook collection(s) */
if (txn->req_tgt.collection) {
/* Add response for target addressbook collection */
propfind_by_collection(txn->req_tgt.mbentry, fctx);
}
else {
/* Add responses for all contained addressbook collections */
mboxlist_mboxtree(txn->req_tgt.mbentry->name,
propfind_by_collection, fctx,
MBOXTREE_SKIP_ROOT);
/* Add responses for all shared addressbook collections */
mboxlist_usersubs(txn->req_tgt.userid,
propfind_by_collection, fctx,
MBOXTREE_SKIP_PERSONAL);
}
}
/* End XML response */
xml_partial_response(txn, fctx->root->doc, NULL /* end */, 0, &fctx->xmlbuf);
xmlBufferFree(fctx->xmlbuf);
/* End of output */
write_body(0, txn, NULL, 0);
done:
/* Free filter structure */
free_cardfilter(&cardfilter);
if (fctx->davdb) {
fctx->close_db(fctx->davdb);
fctx->davdb = NULL;
}
return ret;
}