/* dav_db.c -- implementation of per-user DAV database
 *
 * Copyright (c) 1994-2012 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 <stdlib.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>

#include "assert.h"
#include "caldav_alarm.h"
#include "cyrusdb.h"
#include "dav_db.h"
#include "global.h"
#include "util.h"
#include "xmalloc.h"

#define CMD_CREATE_CAL                                                  \
    "CREATE TABLE IF NOT EXISTS ical_objs ("                            \
    " rowid INTEGER PRIMARY KEY,"                                       \
    " creationdate INTEGER,"                                            \
    " mailbox TEXT NOT NULL,"                                           \
    " resource TEXT NOT NULL,"                                          \
    " imap_uid INTEGER,"                                                \
    " modseq INTEGER,"                                                  \
    " createdmodseq INTEGER,"                                           \
    " lock_token TEXT,"                                                 \
    " lock_owner TEXT,"                                                 \
    " lock_ownerid TEXT,"                                               \
    " lock_expire INTEGER,"                                             \
    " comp_type INTEGER,"                                               \
    " ical_uid TEXT,"                                                   \
    " organizer TEXT,"                                                  \
    " dtstart TEXT,"                                                    \
    " dtend TEXT,"                                                      \
    " comp_flags INTEGER,"                                              \
    " sched_tag TEXT,"                                                  \
    " alive INTEGER,"                                                   \
    " UNIQUE( mailbox, resource ) );"                                   \
    "CREATE INDEX IF NOT EXISTS idx_ical_uid ON ical_objs ( ical_uid );"

#define CMD_CREATE_CARD                                                 \
    "CREATE TABLE IF NOT EXISTS vcard_objs ("                           \
    " rowid INTEGER PRIMARY KEY,"                                       \
    " creationdate INTEGER,"                                            \
    " mailbox TEXT NOT NULL,"                                           \
    " resource TEXT NOT NULL,"                                          \
    " imap_uid INTEGER,"                                                \
    " modseq INTEGER,"                                                  \
    " createdmodseq INTEGER,"                                           \
    " lock_token TEXT,"                                                 \
    " lock_owner TEXT,"                                                 \
    " lock_ownerid TEXT,"                                               \
    " lock_expire INTEGER,"                                             \
    " version INTEGER,"                                                 \
    " vcard_uid TEXT,"                                                  \
    " kind INTEGER,"                                                    \
    " fullname TEXT,"                                                   \
    " name TEXT,"                                                       \
    " nickname TEXT,"                                                   \
    " alive INTEGER,"                                                   \
    " UNIQUE( mailbox, resource ) );"                                   \
    "CREATE INDEX IF NOT EXISTS idx_vcard_fn ON vcard_objs ( fullname );" \
    "CREATE INDEX IF NOT EXISTS idx_vcard_uid ON vcard_objs ( vcard_uid );"

#define CMD_CREATE_EM                                                   \
    "CREATE TABLE IF NOT EXISTS vcard_emails ("                         \
    " rowid INTEGER PRIMARY KEY,"                                       \
    " objid INTEGER,"                                                   \
    " pos INTEGER NOT NULL," /* for sorting */                          \
    " email TEXT NOT NULL COLLATE NOCASE,"                              \
    " ispref INTEGER NOT NULL DEFAULT 0,"                               \
    " ispinned INTEGER NOT NULL DEFAULT 0,"                             \
    " FOREIGN KEY (objid) REFERENCES vcard_objs (rowid) ON DELETE CASCADE );" \
    "CREATE INDEX IF NOT EXISTS idx_vcard_email ON vcard_emails ( email COLLATE NOCASE );"

#define CMD_CREATE_GR                                                   \
    "CREATE TABLE IF NOT EXISTS vcard_groups ("                         \
    " rowid INTEGER PRIMARY KEY,"                                       \
    " objid INTEGER,"                                                   \
    " pos INTEGER NOT NULL," /* for sorting */                          \
    " member_uid TEXT NOT NULL,"                                        \
    " otheruser TEXT NOT NULL DEFAULT \"\","                            \
    " FOREIGN KEY (objid) REFERENCES vcard_objs (rowid) ON DELETE CASCADE );"

#define CMD_CREATE_OBJS                                                 \
    "CREATE TABLE IF NOT EXISTS dav_objs ("                             \
    " rowid INTEGER PRIMARY KEY,"                                       \
    " creationdate INTEGER,"                                            \
    " mailbox TEXT NOT NULL,"                                           \
    " resource TEXT NOT NULL,"                                          \
    " imap_uid INTEGER,"                                                \
    " modseq INTEGER,"                                                  \
    " createdmodseq INTEGER,"                                           \
    " lock_token TEXT,"                                                 \
    " lock_owner TEXT,"                                                 \
    " lock_ownerid TEXT,"                                               \
    " lock_expire INTEGER,"                                             \
    " filename TEXT,"                                                   \
    " type TEXT,"                                                       \
    " subtype TEXT,"                                                    \
    " res_uid TEXT,"                                                    \
    " ref_count INTEGER,"                                               \
    " alive INTEGER,"                                                   \
    " UNIQUE( mailbox, resource ) );"                                   \
    "CREATE INDEX IF NOT EXISTS idx_res_uid ON dav_objs ( res_uid );"

#define CMD_CREATE_CALCACHE                                             \
    "CREATE TABLE IF NOT EXISTS ical_jmapcache ("                       \
    " rowid INTEGER NOT NULL,"                                          \
    " userid TEXT NOT NULL,"                                            \
    " jmapversion INTEGER NOT NULL,"                                    \
    " jmapdata TEXT NOT NULL,"                                          \
    " PRIMARY KEY (rowid, userid)"                                      \
    " FOREIGN KEY (rowid) REFERENCES ical_objs (rowid) ON DELETE CASCADE );"

#define CMD_CREATE_CARDCACHE                                            \
    "CREATE TABLE IF NOT EXISTS vcard_jmapcache ("                      \
    " rowid INTEGER NOT NULL PRIMARY KEY,"                              \
    " jmapversion INTEGER NOT NULL,"                                    \
    " jmapdata TEXT NOT NULL,"                                          \
    " FOREIGN KEY (rowid) REFERENCES vcard_objs (rowid) ON DELETE CASCADE );"


#define CMD_CREATE CMD_CREATE_CAL CMD_CREATE_CARD CMD_CREATE_EM CMD_CREATE_GR \
                   CMD_CREATE_OBJS CMD_CREATE_CALCACHE CMD_CREATE_CARDCACHE

/* leaves these unused columns around, but that's life.  A dav_reconstruct
 * will fix them */
#define CMD_DBUPGRADEv2                                         \
    "ALTER TABLE ical_objs ADD COLUMN comp_flags INTEGER;"      \
    "UPDATE ical_objs SET comp_flags = recurring + 2 * transp;"

#define CMD_DBUPGRADEv3                                         \
    "ALTER TABLE ical_objs ADD COLUMN modseq INTEGER;"          \
    "UPDATE ical_objs SET modseq = 1;"                          \
    "ALTER TABLE vcard_objs ADD COLUMN modseq INTEGER;"         \
    "UPDATE vcard_objs SET modseq = 1;"

#define CMD_DBUPGRADEv4                                         \
    "ALTER TABLE ical_objs ADD COLUMN alive INTEGER;"           \
    "UPDATE ical_objs SET alive = 1;"                           \
    "ALTER TABLE vcard_objs ADD COLUMN alive INTEGER;"          \
    "UPDATE vcard_objs SET alive = 1;"

#define CMD_DBUPGRADEv5                                         \
    "ALTER TABLE vcard_emails ADD COLUMN ispref INTEGER NOT NULL DEFAULT 0;"    \
    "ALTER TABLE vcard_groups ADD COLUMN otheruser TEXT NOT NULL DEFAULT \"\";"

#define CMD_DBUPGRADEv6 CMD_CREATE_OBJS

#define CMD_DBUPGRADEv7                                         \
    "ALTER TABLE ical_objs ADD COLUMN createdmodseq INTEGER;"   \
    "UPDATE ical_objs SET createdmodseq = 0;"                   \
    "ALTER TABLE vcard_objs ADD COLUMN createdmodseq INTEGER;"  \
    "UPDATE vcard_objs SET createdmodseq = 0;"                  \
    "ALTER TABLE dav_objs ADD COLUMN createdmodseq INTEGER;"    \
    "UPDATE dav_objs SET createdmodseq = 0;"

#define CMD_DBUPGRADEv8                                         \
    "ALTER TABLE vcard_emails ADD COLUMN ispinned INTEGER NOT NULL DEFAULT 0;"

#define CMD_DBUPGRADEv9 CMD_CREATE_CALCACHE CMD_CREATE_CARDCACHE


struct sqldb_upgrade davdb_upgrade[] = {
  { 2, CMD_DBUPGRADEv2, NULL },
  { 3, CMD_DBUPGRADEv3, NULL },
  { 4, CMD_DBUPGRADEv4, NULL },
  { 5, CMD_DBUPGRADEv5, NULL },
  { 6, CMD_DBUPGRADEv6, NULL },
  { 7, CMD_DBUPGRADEv7, NULL },
  { 8, CMD_DBUPGRADEv8, NULL },
  { 9, CMD_DBUPGRADEv9, NULL },
  { 0, NULL, NULL }
};

#define DB_VERSION 9

static int in_reconstruct = 0;

EXPORTED sqldb_t *dav_open_userid(const char *userid)
{
    sqldb_t *db = NULL;
    struct buf fname = BUF_INITIALIZER;
    dav_getpath_byuserid(&fname, userid);
    if (in_reconstruct) buf_printf(&fname, ".NEW");
    db = sqldb_open(buf_cstring(&fname), CMD_CREATE, DB_VERSION, davdb_upgrade,
                    config_getduration(IMAPOPT_DAV_LOCK_TIMEOUT, 's') * 1000);
    buf_free(&fname);
    return db;
}

EXPORTED sqldb_t *dav_open_mailbox(struct mailbox *mailbox)
{
    sqldb_t *db = NULL;
    struct buf fname = BUF_INITIALIZER;
    dav_getpath(&fname, mailbox);
    if (in_reconstruct) buf_printf(&fname, ".NEW");
    db = sqldb_open(buf_cstring(&fname), CMD_CREATE, DB_VERSION, davdb_upgrade,
                    config_getduration(IMAPOPT_DAV_LOCK_TIMEOUT, 's') * 1000);
    buf_free(&fname);
    return db;
}

EXPORTED int dav_attach_userid(sqldb_t *db, const char *userid)
{
    struct buf fname = BUF_INITIALIZER;
    dav_getpath_byuserid(&fname, userid);
    int r = sqldb_attach(db, buf_cstring(&fname));
    buf_free(&fname);
    return r;
}

EXPORTED int dav_attach_mailbox(sqldb_t *db, struct mailbox *mailbox)
{
    struct buf fname = BUF_INITIALIZER;
    dav_getpath(&fname, mailbox);
    int r = sqldb_attach(db, buf_cstring(&fname));
    buf_free(&fname);
    return r;
}


/*
 * mboxlist_usermboxtree() callback function to create DAV DB entries for a mailbox
 */
static int _dav_reconstruct_mb(const mbentry_t *mbentry,
                               void *rock
#ifndef WITH_JMAP
                                          __attribute__((unused))
#endif
                              )
{
#ifdef WITH_JMAP
    const char *userid = (const char *) rock;
    struct buf attrib = BUF_INITIALIZER;
#endif
    int (*addproc)(struct mailbox *) = NULL;
    int r = 0;

    signals_poll();

    switch (mbentry->mbtype) {
#ifdef WITH_DAV
    case MBTYPE_CALENDAR:
    case MBTYPE_COLLECTION:
    case MBTYPE_ADDRESSBOOK:
        addproc = &mailbox_add_dav;
        break;
#endif
#ifdef WITH_JMAP
    case MBTYPE_SUBMISSION:
        addproc = &mailbox_add_email_alarms;
        break;

    case MBTYPE_EMAIL:
        r = annotatemore_lookup(mbentry->name, "/specialuse", userid, &attrib);
        if (!r && buf_len(&attrib)) {
            strarray_t *specialuse =
                strarray_split(buf_cstring(&attrib), NULL, 0);

            if (strarray_find(specialuse, "\\Snoozed", 0) >= 0) {
                addproc = &mailbox_add_email_alarms;
            }
            strarray_free(specialuse);
        }
        buf_free(&attrib);
        break;
#endif
    }

    if (addproc) {
        struct mailbox *mailbox = NULL;
        /* Open/lock header */
        r = mailbox_open_iwl(mbentry->name, &mailbox);
        // needs to be writable to remove bogus lastalarm data
        if (!r) r = addproc(mailbox);
        mailbox_close(&mailbox);
    }

    return r;
}

static void run_audit_tool(const char *tool, const char *srcdb, const char *dstdb)
{
    pid_t pid = fork();
    if (pid < 0)
        return;

    if (pid == 0) {
        /* child */
        execl(tool, tool, srcdb, dstdb, (void *)NULL);
        exit(-1);
    }

    int status;
    while (waitpid(pid, &status, 0) < 0);
}

EXPORTED int dav_reconstruct_user(const char *userid, const char *audit_tool)
{
    syslog(LOG_NOTICE, "dav_reconstruct_user: %s", userid);

    struct buf fname = BUF_INITIALIZER;
    dav_getpath_byuserid(&fname, userid);

    struct buf newfname = BUF_INITIALIZER;
    dav_getpath_byuserid(&newfname, userid);
    buf_printf(&newfname, ".NEW");

    /* XXX - this still means that alarms can go missing if this
     * task is interrupted, but we can't afford to keep the
     * alarm database locked for the entire time, it's a single
     * blocking database over the entire server */
    caldav_alarm_delete_user(userid);

    in_reconstruct = 1;

    sqldb_t *userdb = dav_open_userid(userid);
    sqldb_begin(userdb, "reconstruct");
    int r = mboxlist_usermboxtree(userid, NULL,
                                  _dav_reconstruct_mb, (void *) userid, 0);
    if (r)
        sqldb_rollback(userdb, "reconstruct");
    else
        sqldb_commit(userdb, "reconstruct");
    sqldb_close(&userdb);

    in_reconstruct = 0;

    /* this actually works before close according to the internets */
    if (r) {
        syslog(LOG_ERR, "dav_reconstruct_user: %s FAILED %s", userid, error_message(r));
        if (audit_tool) {
            printf("Not auditing %s, reconstruct failed %s\n", userid, error_message(r));
        }
        unlink(buf_cstring(&newfname));
    }
    else {
        syslog(LOG_NOTICE, "dav_reconstruct_user: %s SUCCEEDED", userid);
        if (audit_tool) {
            run_audit_tool(audit_tool, buf_cstring(&fname), buf_cstring(&newfname));
            unlink(buf_cstring(&newfname));
        }
        else {
            rename(buf_cstring(&newfname), buf_cstring(&fname));
        }
    }

    buf_free(&newfname);
    buf_free(&fname);

    return 0;
}
