/* http_applepush.c -- Apple push service endpoints for DAV push
 *
 * Copyright (c) 1994-2016 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.
 *
 */

#include "config.h"

#include "acl.h"
#include "httpd.h"
#include "http_dav.h"
#include "util.h"
#include <syslog.h>

/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"

static void applepush_init(struct buf *serverinfo);

static int meth_get_applepush(struct transaction_t *txn, void *params);
static int meth_post_applepush(struct transaction_t *txn, void *params);

struct namespace_t namespace_applepush = {
    URL_NS_APPLEPUSH, /*enabled*/0, "applepush", "/applepush/subscribe", NULL,
    http_allow_noauth_get, /*authschemes*/0,
    /*mbtype*/0,
    ALLOW_READ|ALLOW_POST,
    &applepush_init, NULL, NULL, NULL, NULL,
    {
        { NULL,                 NULL },                 /* ACL          */
        { NULL,                 NULL },                 /* BIND         */
        { NULL,                 NULL },                 /* CONNECT      */
        { NULL,                 NULL },                 /* COPY         */
        { NULL,                 NULL },                 /* DELETE       */
        { &meth_get_applepush,  NULL },                 /* GET          */
        { NULL,                 NULL },                 /* HEAD         */
        { NULL,                 NULL },                 /* LOCK         */
        { NULL,                 NULL },                 /* MKCALENDAR   */
        { NULL,                 NULL },                 /* MKCOL        */
        { NULL,                 NULL },                 /* MOVE         */
        { NULL,                 NULL },                 /* OPTIONS      */
        { NULL,                 NULL },                 /* PATCH        */
        { &meth_post_applepush, NULL },                 /* POST         */
        { NULL,                 NULL },                 /* PROPFIND     */
        { NULL,                 NULL },                 /* PROPPATCH    */
        { NULL,                 NULL },                 /* PUT          */
        { NULL,                 NULL },                 /* REPORT       */
        { NULL,                 NULL },                 /* SEARCH       */
        { NULL,                 NULL },                 /* TRACE        */
        { NULL,                 NULL },                 /* UNBIND       */
        { NULL,                 NULL }                  /* UNLOCK       */
    }
};

static void applepush_init(struct buf *serverinfo __attribute__((unused)))
{
    namespace_applepush.enabled = apns_enabled &&
        ((namespace_calendar.enabled && config_getstring(IMAPOPT_APS_TOPIC_CALDAV)) ||
         (namespace_addressbook.enabled && config_getstring(IMAPOPT_APS_TOPIC_CARDDAV)));
}

static int meth_get_applepush(struct transaction_t *txn,
                              void *params __attribute__((unused)))
{
    int rc = HTTP_BAD_REQUEST, r = 0;
    struct strlist *vals = NULL;
    const char *token = NULL, *key = NULL, *aps_topic = NULL;
    const char *mailbox_userid = NULL, *mailbox_uniqueid = NULL;
    strarray_t *keyparts = NULL;
    char *mboxname = NULL;
    struct mboxlist_entry *mbentry = NULL;
    int mbtype = 0;

    /* unpack query params */
    vals = hash_lookup("token", &txn->req_qparams);
    if (!vals) goto done;
    token = vals->s;

    vals = hash_lookup("key", &txn->req_qparams);
    if (!vals) goto done;
    key = vals->s;

    /* decompose key to userid + mailbox uniqueid */
    keyparts = strarray_split(key, "/", 0);
    if (strarray_size(keyparts) != 2)
        goto done;
    mailbox_userid = strarray_nth(keyparts, 0);
    mailbox_uniqueid = strarray_nth(keyparts, 1);

    /* lookup mailbox */
    mboxname = mboxlist_find_uniqueid(mailbox_uniqueid, mailbox_userid,
                                      httpd_authstate);
    if (!mboxname) {
        syslog(LOG_ERR,
               "meth_get_applepush: mboxlist_find_uniqueid(%s, %s) not found",
               mailbox_uniqueid, mailbox_userid);
        goto done;
    }

    r = mboxlist_lookup(mboxname, &mbentry, NULL);
    if (r || !mbentry) {
        syslog(LOG_ERR, "meth_get_applepush: mboxlist_lookup(%s): %s",
               mboxname, error_message(r));
        goto done;
    }

    /* mailbox must be calendar or addressbook */
    mbtype = mbtype_isa(mbentry->mbtype);
    if (mbtype != MBTYPE_CALENDAR && mbtype != MBTYPE_ADDRESSBOOK)
        goto done;

    /* check if auth user has access to mailbox */
    int myrights = httpd_myrights(httpd_authstate, mbentry);
    if (!(myrights & ACL_READ)) {
        syslog(LOG_ERR, "meth_get_applepush: no read access to %s for %s (%s)",
               mboxname, httpd_userid, mbentry->acl);
        goto done;
    }

    aps_topic = config_getstring(mbtype_isa(mbtype) == MBTYPE_CALENDAR ?
                                 IMAPOPT_APS_TOPIC_CALDAV :
                                 IMAPOPT_APS_TOPIC_CARDDAV);
    if (!aps_topic) {
        syslog(LOG_ERR, "aps_topic_%s not configured, can't subscribe",
               mbtype_isa(mbtype) == MBTYPE_CALENDAR ? "caldav" : "carddav");
        goto done;
    }

    mboxlist_entry_free(&mbentry);

    /* notify! */
    struct mboxevent *mboxevent = mboxevent_new(EVENT_APPLEPUSHSERVICE_DAV);
    mboxevent_set_applepushservice_dav(mboxevent, aps_topic, token, httpd_userid,
                                       mailbox_userid, mailbox_uniqueid, mbtype,
                                       config_getduration(IMAPOPT_APS_EXPIRY, 'd'));
    mboxevent_notify(&mboxevent);
    mboxevent_free(&mboxevent);

    rc = HTTP_OK;

done:
    mboxlist_entry_free(&mbentry);
    if (mboxname) free(mboxname);
    if (keyparts) strarray_free(keyparts);

    return rc;
}

static int meth_post_applepush(struct transaction_t *txn, void *params)
{
    /* Check Content-Type */
    const char **hdr = spool_getheader(txn->req_hdrs, "Content-Type");

    if (hdr && is_mediatype("application/x-www-form-urlencoded", hdr[0])) {
        /* Read body */
        txn->req_body.flags |= BODY_DECODE;

        int r = http_read_req_body(txn);
        if (r) {
            txn->flags.conn = CONN_CLOSE;
            return r;
        }

        /* x-www-form-urlencoded is essentially a URI query component
           with SP replaced with '+' */
        buf_replace_char(&txn->req_body.payload, '+', ' ');
        parse_query_params(txn, buf_cstring(&txn->req_body.payload));
    }
    else {
        /* POST has been seen with URL params,
           so for now we just call the GET handler */
    }

    return meth_get_applepush(txn, params);
}


/* 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;

    if (!propstat) {
        /* Prescreen "property" request - add namespace for environment */
        ensure_ns(fctx->ns, NS_MOBME, fctx->root, XML_NS_MOBME, "MM");
        return 0;
    }

    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, mailbox_uniqueid(fctx->mailbox));
    xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                 name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);

    return 0;
}
