/* ====================================================================
 * Copyright (c) 1995-1998 The Apache Group.  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. All advertising materials mentioning features or use of this
 *    software must display the following acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * 4. The names "Apache Server" and "Apache Group" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission.
 *
 * 5. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * THIS SOFTWARE IS PROVIDED BY THE APACHE GROUP ``AS IS'' AND ANY
 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE APACHE GROUP OR
 * IT'S CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Group and was originally based
 * on public domain software written at the National Center for
 * Supercomputing Applications, University of Illinois, Urbana-Champaign.
 * For more information on the Apache Group and the Apache HTTP server
 * project, please see <http://www.apache.org/>.
 *
 */

/*
 * URL Counting
 *
 * Track all incoming requests for each URL.  Maintains a database containing
 * each URL accessed, count of times accessed, and last time counter has been
 * reset.  If the URL is a directory, then the file that it is redirected to
 * is counted.
 *
 * The values for count of times accessed, and last time counter was reset are
 * made available to the to the document via variables called URL_COUNT and
 * URL_COUNT_RESET, respectively.  Another variable URL_COUNT_DB is set to the
 * database file used.
 *
 * Config file directives:
 *
 *     CounterType           `txt' or `dbm'
 *     CounterAutoAdd        `on' or `off': Automatically add missing URL's
 *     CounterFile           Path to ASCII or DBM file.
 *
 * The following pertain to a per server (or virtual server) configuration:
 *
 *     ServerCounterType     `txt' or `dbm'
 *     ServerCounterAutoAdd  `on' or `off': Automatically add missing URL's
 *     ServerCounterFile     path to ASCII or DBM file.
 *
 * ASCII files have the format: URL ### date
 *
 * Note: The counter files are open/closed for each request to allow
 * URLS to be managed by external programs without shutting down the
 * web server. 
 *
 * Originally written 1-14-95
 * by  Brian Kolaci
 *     bk@galaxy.net
 *
 * Ported to Apache 1.2b8 16-04-1997,
 *        to Apache 1.3b4-dev 02-01-1998,
 *        to Apache 1.3.1 02-Sep-1998
 * by  Ralf S. Engelschall
 *     rse@engelschall.com
 *     www.engelschall.com
 */

/*
 * Module definition information - the part between the -START and -END
 * lines below is used by Configure. This could be stored in a separate
 * instead.
 *
 * MODULE-DEFINITION-START
 * Name: urlcount_module
 * ConfigStart
    . ./helpers/find-dbm-lib
 * ConfigEnd
 * MODULE-DEFINITION-END
 */

#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_protocol.h"

#if defined(__GLIBC__) && defined(__GLIBC_MINOR__) \
    && __GLIBC__ >= 2 && __GLIBC_MINOR__ >= 1
#include <db1/ndbm.h>
#else
#include <ndbm.h>
#endif

module urlcount_module;

/*
 *  Data structions
 */
typedef enum {
    CntrTxt, 
    CntrDbm
} CounterType;

typedef struct {
    unsigned long count;
    char         *date;
} urlcount_results;

typedef struct {
    int         urlcount_default;
    CounterType urlcount_type;
    int         urlcount_auto_add;
    char       *urlcount_file;
} urlcount_config_rec;

#define DEF_CNTRTYPE    1
#define DEF_CNTRAA      2
#define DEF_CNTRFILE    4
#define DEF_ALL         (DEF_CNTRTYPE|DEF_CNTRAA|DEF_CNTRFILE)

/*
 *  Defaults
 */
#define DEFAULT_CNTR_TYPE       CntrDbm
#define DEFAULT_TIME_FORMAT     "%A, %d-%b-%y %T %Z"

/*
 * File locking support
 */
#if defined(USE_FCNTL_SERIALIZED_ACCEPT)
#define USE_FCNTL 1
#include <fcntl.h>
#endif
#if defined(USE_FLOCK_SERIALIZED_ACCEPT)
#define USE_FLOCK 1
#include <sys/file.h>
#endif
#if !defined(USE_FCNTL) && !defined(USE_FLOCK)
#define USE_FLOCK 1
#ifndef MPE
#include <sys/file.h>
#endif
#ifndef LOCK_UN
#undef USE_FLOCK
#define USE_FCNTL 1
#include <fcntl.h>
#endif
#endif
#ifdef AIX
#undef USE_FLOCK
#define USE_FCNTL 1
#include <fcntl.h>
#endif

/*
 * Create config data structures
 */
static void *urlcount_create_dir_config(pool *p, char *d)
{
    urlcount_config_rec *rec = 
        (urlcount_config_rec *)ap_pcalloc(p, sizeof(urlcount_config_rec));

    rec->urlcount_type       = DEFAULT_CNTR_TYPE;
    rec->urlcount_auto_add   = 0;
    rec->urlcount_file       = NULL;
    rec->urlcount_default    = DEF_ALL;
    return (rec);
}

static void *urlcount_create_srv_config(pool *p, server_rec *d)
{
    urlcount_config_rec *rec = 
        (urlcount_config_rec *) ap_pcalloc(p, sizeof(urlcount_config_rec));

    rec->urlcount_type       = DEFAULT_CNTR_TYPE;
    rec->urlcount_auto_add   = 0;
    rec->urlcount_file       = NULL;
    rec->urlcount_default    = DEF_ALL;
    return (rec);
}

static void *urlcount_merge_config(pool *p, void *parent, void *sub)
{
    urlcount_config_rec *par  = (urlcount_config_rec *)parent;
    urlcount_config_rec *chld = (urlcount_config_rec *)sub;
    urlcount_config_rec *mrg  = (urlcount_config_rec *)ap_palloc(p, sizeof(*mrg));

    if (chld->urlcount_default & DEF_CNTRTYPE)
        mrg->urlcount_type = par->urlcount_type;
    else
        mrg->urlcount_type = chld->urlcount_type;
    if (chld->urlcount_default & DEF_CNTRAA)
        mrg->urlcount_auto_add = par->urlcount_auto_add;
    else
        mrg->urlcount_auto_add = chld->urlcount_auto_add;
    if (chld->urlcount_default & DEF_CNTRFILE)
        mrg->urlcount_file = par->urlcount_file;
    else
        mrg->urlcount_file = chld->urlcount_file;
    mrg->urlcount_default = 0;
    return (mrg);
}

static const char *set_urlcount_type(cmd_parms *cmd, void *ct, char *arg)
{
    urlcount_config_rec *conf = (urlcount_config_rec *)ct;

    if (strcasecmp(arg, "txt") == 0)
        conf->urlcount_type = CntrTxt;
    else if (strcasecmp(arg, "dbm") == 0)
        conf->urlcount_type = CntrDbm;
    else
        return "CounterType must be `file' or `dbm'";
    conf->urlcount_default &= ~DEF_CNTRTYPE;
    return NULL;
}

static const char *set_urlcount_autoadd(cmd_parms *cmd, void *ct, int arg)
{
    urlcount_config_rec *conf = (urlcount_config_rec *)ct;

    conf->urlcount_auto_add = arg;
    conf->urlcount_default &= ~DEF_CNTRAA;
    return NULL;
}

static const char *set_urlcount_file(cmd_parms *cmd, void *ct, char *arg)
{
    urlcount_config_rec *conf = (urlcount_config_rec *)ct;

    if (strcmp(arg, "/dev/null") == 0)
        conf->urlcount_file = NULL;
    else
        conf->urlcount_file = ap_server_root_relative(cmd->pool, arg);
    conf->urlcount_default &= ~DEF_CNTRFILE;
    return NULL;
}

static const char *set_svr_urlcount_type(cmd_parms *cmd, void *ct, char *arg)
{
    urlcount_config_rec *conf =
        ap_get_module_config(cmd->server->module_config, &urlcount_module);
    
    return (set_urlcount_type(cmd, conf, arg));
}

static const char *set_svr_urlcount_autoadd(cmd_parms *cmd, void *ct, int arg)
{
    urlcount_config_rec *conf =
        ap_get_module_config(cmd->server->module_config, &urlcount_module);
    return (set_urlcount_autoadd(cmd, conf, arg));
}

static const char *set_svr_urlcount_file(cmd_parms *cmd, void *ct, char *arg)
{
    urlcount_config_rec *conf =
        ap_get_module_config(cmd->server->module_config, &urlcount_module);
    return (set_urlcount_file(cmd, conf, arg));
}

#ifdef USE_FCNTL
static struct flock lock_it;
static struct flock unlock_it;
#endif

static int fd_lock(int fd)
{
    int rc;

#ifdef USE_FCNTL
    lock_it.l_whence = SEEK_SET;
    lock_it.l_start  = 0;
    lock_it.l_len    = 0;
    lock_it.l_type   = F_WRLCK;
    lock_it.l_pid    = 0;

    while (((rc = fcntl(fd, F_SETLKW, &lock_it)) < 0)
           && (errno == EINTR))
        continue;
#endif
#ifdef USE_FLOCK
    while (((rc = flock(fd, LOCK_EX)) < 0)
           && (errno == EINTR))
        continue;
#endif
    return rc;
}

static int fd_unlock(int fd)
{
    int rc;

#ifdef USE_FCNTL
    unlock_it.l_whence = SEEK_SET;
    unlock_it.l_start  = 0;
    unlock_it.l_len    = 0;
    unlock_it.l_type   = F_UNLCK;
    unlock_it.l_pid    = 0;

    rc = fcntl(fd, F_SETLKW, &unlock_it);
#endif
#ifdef USE_FLOCK
    rc = flock(fd, LOCK_UN);
#endif
    return rc;
}

static char *urlcount_inc_txt(pool *p, urlcount_results *results,
                              urlcount_config_rec *r, char *uri)
{
    char buf[MAX_STRING_LEN];
    char buf2[MAX_STRING_LEN];
    int found; 
    int len = strlen(uri);
    int buflen = 0;
    int buflen2 = 0;
    FILE *fp;
    long roffset;
    long woffset;
    long fsize;

    /*
     * Open counter file
     */
    if ((fp = fopen(r->urlcount_file, "r+")) == NULL)
        if ((fp = fopen(r->urlcount_file, "w+")) == NULL)
            return (ap_pstrcat(p, "Failed to open counter TXT file: ", 
                               r->urlcount_file, NULL));
    if (fd_lock(fileno(fp))) {
        fclose(fp);
        return (ap_pstrcat(p, "Failed to lock counter TXT file: ",
                           r->urlcount_file, NULL));
    }

    /* 
     * Find end of file 
     */
    fseek(fp, 0, SEEK_END);
    fsize = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    /*
     */
    roffset = 0;
    woffset = 0;
    found   = 0;
    while (roffset < fsize) {

        roffset = ftell(fp);
        fgets(buf, sizeof(buf), fp);
        buflen = strlen(buf);

        /*
         *  If we already found the URI, manage buffers
         *  to effectively "push" the strings lower in the file.
         */
        if (found) {
            roffset = ftell(fp);
            /* Go back and write old buffer */
            fseek(fp, woffset, SEEK_SET);
            fwrite(buf2, buflen2, 1, fp);
            woffset = ftell(fp);
            /* Are we done ? */
            if (roffset >= fsize) {
                fwrite(buf, buflen, 1, fp);
                break;
            }
            /* Save copy of last buffer */
            strcpy(buf2, buf);
            buflen2 = buflen;
            /* Move offset back to read next line */
            fseek(fp, roffset, SEEK_SET);
            continue;
        }

        /*
         *  See if we found the matching URI
         */
        if (strncmp(uri, buf, len) == 0 && isspace(buf[len])) {
            char *ptr = &buf[len];
            char *q = strchr(ptr, '\n');
            if (q)
                *q = '\0';
            /* Skip spaces to number */
            while (*ptr && isspace(*ptr))
                ptr++;
            results->count = atol(ptr) + 1;
            /* Skip over number */
            while (*ptr && !isspace(*ptr))
                ptr++;
            /* Skip spaces to date */
            while (*ptr && isspace(*ptr))
                ptr++;
            results->date = ap_pstrdup(p, ptr);
            found++;
            ap_snprintf(buf2, sizeof(buf2), "%s\t%010lu\t%s\n",
                        uri, results->count, results->date);
            /*
             *  If we don't increase the size of the buffer,
             *  we can write it out now and get out of the loop.
             *  If not, we must then "push" all of the items
             *  down in the file.
             */
            buflen2 = strlen(buf2);
            woffset = roffset;
            if (buflen2 <= buflen) {
                buflen2--;
                while (buflen2 < buflen - 1)
                    buf2[buflen2++] = ' ';
                buf2[buflen2++] = '\n';
                buf2[buflen2++] = '\0';
                fseek(fp, -buflen, SEEK_CUR);
                fwrite(buf2, buflen, 1, fp);
                break;
            }
            continue;
        }
    }

    /*
     *  See if we need to append to end of file
     */
    if (!found && r->urlcount_auto_add) {
        results->count = 1;
        results->date  = ap_ht_time(p, time(0L), DEFAULT_TIME_FORMAT, 0);

        /* We should already be at the bottom of the file */
        fprintf(fp, "%s\t%010lu\t%s\n", uri, results->count, results->date);
    }

    /*
     * Close TXT file 
     */
    fd_unlock(fileno(fp));
    fclose(fp);

    /*
     * And signal that there was no error...
     */
    return NULL;
}

static char *urlcount_inc_dbm(pool *p, urlcount_results *results,
                              urlcount_config_rec *r, char *uri)
{
    DBM *dbm;
    datum d, q;
    int len;
    char *ptr;

    results->count = 0;
    results->date  = NULL;

    q.dptr  = uri;
    q.dsize = strlen(q.dptr);

    /* 
     * Try to fetch the count under URL from DBM file
     */
    if ((dbm = dbm_open(r->urlcount_file, O_RDWR, 0664)) == NULL)
        if ((dbm = dbm_open(r->urlcount_file, O_RDWR|O_CREAT, 0664)) == NULL)
            return (ap_pstrcat(p, "Failed to open counter DBM file: ", r->urlcount_file, NULL));
    if (fd_lock(dbm_dirfno(dbm))) {
        dbm_close(dbm);
        return (ap_pstrcat(p, "Failed to lock counter DBM file: ", r->urlcount_file, NULL));
    }
    d = dbm_fetch(dbm, q);

    /*
     *  See if found, if not check whether we have 
     *  to automatically add it
     */
    if (d.dptr != NULL) {
        ptr = d.dptr;
        results->count = atol(ptr);
        /* Skip to time stamp */
        while (*ptr && !isspace(*ptr))
            ptr++;
        while (*ptr && isspace(*ptr))
            ptr++;
        len = d.dsize - (ptr - d.dptr);
        results->date = ap_pcalloc(p, len + 1);
        ap_cpystrn(results->date, ptr, len);
        results->date[len] = '\0';
    }

    /*
     *  Increment count, set default date
     */
    results->count++;
    if (results->date == NULL)
        results->date = ap_ht_time(p, time(0L), DEFAULT_TIME_FORMAT, 0);

    /*
     * Add or update the record
     */
    if (d.dptr != NULL || r->urlcount_auto_add) {
        d.dptr = ap_psprintf(p, "%lu\t%s", results->count, results->date);
        d.dsize = strlen(d.dptr);
        dbm_store(dbm, q, d, DBM_REPLACE);
    }

    /*
     * Close DBM file 
     */
    fd_unlock(dbm_dirfno(dbm));
    dbm_close(dbm);

    /*
     * And signal that there was no error...
     */
    return NULL;
}

static char *urlcount_inc(pool *p, urlcount_results *results,
                          urlcount_config_rec *r, char *uri)
{
    char *puri;
    char *ptr;
    char *q;

    /* 
     * Normalize the URI stripping out double "//"
     */
    puri = ap_pstrdup(p, uri);
    ptr = puri;
    while (*ptr != '\0') {
        if (*ptr == '/' && *(ptr+1) == '/')
            for (q = ++ptr; *q != '\0'; )
                *q = *(q+1);
        else
            ptr++;
    }

    /*
     * Now dispatch to the type-dependent routines...
     */
    if (r->urlcount_type == CntrTxt)
        return urlcount_inc_txt(p, results, r, puri);
    else if (r->urlcount_type == CntrDbm)
        return urlcount_inc_dbm(p, results, r, puri);
    else
        return NULL;
}

static int urlcount_update(request_rec *r)
{
    urlcount_config_rec *svr, *dir;
    urlcount_results *sres, *dres;
    urlcount_results *res;
    char *dbfile;
    char *cp;

    /*
     *  Get to last request, if locally redirected.
     */
    while (r->next != NULL)
        r = r->next;

    /*
     * Get each of the counter files
     */
    svr = ap_get_module_config(r->server->module_config, &urlcount_module);
    dir = ap_get_module_config(r->per_dir_config,        &urlcount_module);

    /*
     * Decline operation if missing URI, 
     * this is an included request, or this
     * is not a regular file, or no counter
     * file is configured.
     */
    if (   r->uri == NULL 
        || strcmp(r->protocol, "INCLUDED") == 0
        || !S_ISREG(r->finfo.st_mode)          
        || (   svr->urlcount_file == NULL
            && dir->urlcount_file == NULL)    )
        return (DECLINED);

    /*
     * Decline for all images
     */
    if (r->content_type != NULL)
        if (strlen(r->content_type) > 6 && strncmp(r->content_type, "image/", 6) == 0)
            return (DECLINED);

    /*
     * Allocate result structures
     */
    sres = (urlcount_results *)ap_pcalloc(r->pool, sizeof(urlcount_results));
    dres = (urlcount_results *)ap_pcalloc(r->pool, sizeof(urlcount_results));

    /*
     * Increment the server configured counter
     */
    if (svr->urlcount_file != NULL)
        if ((cp = urlcount_inc(r->pool, sres, svr, r->uri)) != NULL)
            ap_log_error(APLOG_MARK, APLOG_ERR, r->server, "mod_urlcount: %s", cp);

    /*
     * Increment the directory configured counter
     * (using the full filename instead of the URL)
     */
    if (dir->urlcount_file != NULL)
        if ((cp = urlcount_inc(r->pool, dres, dir, r->filename)) != NULL)
            ap_log_error(APLOG_MARK, APLOG_ERR, r->server, "mod_urlcount: %s", cp);

    /*
     * Now set the environment variables while taking the server config as
     * preference over the directory config.
     */
    res = (sres->count) ? sres : dres;
    dbfile = (sres->count) ? svr->urlcount_file : dir->urlcount_file;

    cp = ap_psprintf(r->pool, "%lu", res->count);
    ap_table_set(r->subprocess_env, "URL_COUNT", cp);
    ap_table_set(r->subprocess_env, "URL_COUNT_RESET", res->date);
    ap_table_set(r->subprocess_env, "URL_COUNT_DB", dbfile);

    /*
     * And signal the API that all is ok...
     */
    return OK;
}

static command_rec urlcount_cmds[] = {
    { "CounterType", set_urlcount_type, NULL, ACCESS_CONF, TAKE1, 
      "Type of per-directory counter (`file' or `dbm')" },
    { "CounterAutoAdd", set_urlcount_autoadd, NULL, ACCESS_CONF, FLAG, 
      "Whether new URLs are added automatically to the per-directory counter file (`on' or `off')" },
    { "CounterFile", set_urlcount_file, NULL, ACCESS_CONF, TAKE1,
      "Filename of the per-directory counter file (`/path/to/file')" },
    { "ServerCounterType", set_svr_urlcount_type, NULL, RSRC_CONF, TAKE1, 
      "Type of per-server counter (`file' or `dbm')" },
    { "ServerCounterAutoAdd", set_svr_urlcount_autoadd, NULL, RSRC_CONF, FLAG, 
      "Whether new URLs are added automatically to the per-server counter file (`on' or `off')" },
    { "ServerCounterFile", set_svr_urlcount_file, NULL, RSRC_CONF, TAKE1,
      "Name of per-server counter file or database" },
    { NULL }
};

module MODULE_VAR_EXPORT urlcount_module = {
    STANDARD_MODULE_STUFF,
    NULL,                       /* initializer */
    urlcount_create_dir_config, /* dir config creater */
    urlcount_merge_config,      /* dir merger */
    urlcount_create_srv_config, /* server config */
    urlcount_merge_config,      /* merge server config */
    urlcount_cmds,              /* command table */
    NULL,                       /* handlers */
    NULL,                       /* filename translation */
    NULL,                       /* check_user_id */
    NULL,                       /* check auth */
    NULL,                       /* check access */
    NULL,                       /* type_checker */
    urlcount_update,            /* fixups */
    NULL,                       /* logger */
    NULL,                       /* header parser */
    NULL,                       /* child_init */
    NULL,                       /* child_exit */
    NULL                        /* post read-request */
};

