/*
 * Copyright (c) 2005, 2008 Sun Microsystems, Inc. All Rights Reserved.
 * Use is subject to license terms.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *  http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "httpd.h"
#include "http_config.h"
#include "http_log.h"
#include "apr_strings.h"
#include "apr_general.h"
#include "util_filter.h"
#include "apr_buckets.h"
#include "http_request.h"
#include "libsed.h"

static const char *sed_filter_name = "Sed";
#define MODSED_OUTBUF_SIZE 8000
#define MAX_TRANSIENT_BUCKETS 50

typedef struct sed_expr_config
{
    sed_commands_t *sed_cmds;
    const char *last_error;
} sed_expr_config;

typedef struct sed_config
{
    sed_expr_config output;
    sed_expr_config input;
} sed_config;

/* Context for filter invocation for single HTTP request */
typedef struct sed_filter_ctxt
{
    sed_eval_t eval;
    ap_filter_t *f;
    request_rec *r;
    apr_bucket_brigade *bb;
    apr_bucket_brigade *bbinp;
    char *outbuf;
    char *curoutbuf;
    int bufsize;
    apr_pool_t *tpool;
    int numbuckets;
} sed_filter_ctxt;

module AP_MODULE_DECLARE_DATA sed_module;

/* This function will be call back from libsed functions if there is any error
 * happend during execution of sed scripts
 */
static apr_status_t log_sed_errf(void *data, const char *error)
{
    request_rec *r = (request_rec *) data;
    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "%s", error);
    return APR_SUCCESS;
}

/* This function will be call back from libsed functions if there is any
 * compilation error.
 */
static apr_status_t sed_compile_errf(void *data, const char *error)
{
    sed_expr_config *sed_cfg = (sed_expr_config *) data;
    sed_cfg->last_error = error;
    return APR_SUCCESS;
}

/* clear the temporary pool (used for transient buckets)
 */
static void clear_ctxpool(sed_filter_ctxt* ctx)
{
    apr_pool_clear(ctx->tpool);
    ctx->outbuf = NULL;
    ctx->curoutbuf = NULL;
    ctx->numbuckets = 0;
}

/* alloc_outbuf
 * allocate output buffer
 */
static void alloc_outbuf(sed_filter_ctxt* ctx)
{
    ctx->outbuf = apr_palloc(ctx->tpool, ctx->bufsize + 1);
    ctx->curoutbuf = ctx->outbuf;
}

/* append_bucket
 * Allocate a new bucket from buf and sz and append to ctx->bb
 */
static apr_status_t append_bucket(sed_filter_ctxt* ctx, char* buf, int sz)
{
    apr_status_t status = APR_SUCCESS;
    apr_bucket *b;
    if (ctx->tpool == ctx->r->pool) {
        /* We are not using transient bucket */
        b = apr_bucket_pool_create(buf, sz, ctx->r->pool,
                                   ctx->r->connection->bucket_alloc);
        APR_BRIGADE_INSERT_TAIL(ctx->bb, b);
    }
    else {
        /* We are using transient bucket */
        b = apr_bucket_transient_create(buf, sz,
                                        ctx->r->connection->bucket_alloc);
        APR_BRIGADE_INSERT_TAIL(ctx->bb, b);
        ctx->numbuckets++;
        if (ctx->numbuckets >= MAX_TRANSIENT_BUCKETS) {
            b = apr_bucket_flush_create(ctx->r->connection->bucket_alloc);
            APR_BRIGADE_INSERT_TAIL(ctx->bb, b);
            status = ap_pass_brigade(ctx->f->next, ctx->bb);
            apr_brigade_cleanup(ctx->bb);
            clear_ctxpool(ctx);
        }
    }
    return status;
}

/*
 * flush_output_buffer
 * Flush the  output data (stored in ctx->outbuf)
 */
static apr_status_t flush_output_buffer(sed_filter_ctxt *ctx)
{
    int size = ctx->curoutbuf - ctx->outbuf;
    char *out;
    apr_status_t status = APR_SUCCESS;
    if ((ctx->outbuf == NULL) || (size <=0))
        return status;
    out = apr_pmemdup(ctx->tpool, ctx->outbuf, size);
    status = append_bucket(ctx, out, size);
    ctx->curoutbuf = ctx->outbuf;
    return status;
}

/* This is a call back function. When libsed wants to generate the output,
 * this function will be invoked.
 */
static apr_status_t sed_write_output(void *dummy, char *buf, int sz)
{
    /* dummy is basically filter context. Context is passed during invocation
     * of sed_eval_buffer
     */
    int remainbytes = 0;
    apr_status_t status = APR_SUCCESS;
    sed_filter_ctxt *ctx = (sed_filter_ctxt *) dummy;
    if (ctx->outbuf == NULL) {
        alloc_outbuf(ctx);
    }
    remainbytes = ctx->bufsize - (ctx->curoutbuf - ctx->outbuf);
    if (sz >= remainbytes) {
        if (remainbytes > 0) {
            memcpy(ctx->curoutbuf, buf, remainbytes);
            buf += remainbytes;
            sz -= remainbytes;
            ctx->curoutbuf += remainbytes;
        }
        /* buffer is now full */
        status = append_bucket(ctx, ctx->outbuf, ctx->bufsize);
        /* old buffer is now used so allocate new buffer */
        alloc_outbuf(ctx);
        /* if size is bigger than the allocated buffer directly add to output
         * brigade */
        if ((status == APR_SUCCESS) && (sz >= ctx->bufsize)) {
            char* newbuf = apr_pmemdup(ctx->tpool, buf, sz);
            status = append_bucket(ctx, newbuf, sz);
            /* pool might get clear after append_bucket */
            if (ctx->outbuf == NULL) {
                alloc_outbuf(ctx);
            }
        }
        else {
            memcpy(ctx->curoutbuf, buf, sz);
            ctx->curoutbuf += sz;
        }
    }
    else {
        memcpy(ctx->curoutbuf, buf, sz);
        ctx->curoutbuf += sz;
    }
    return status;
}

/* Compile a sed expression. Compiled context is saved in sed_cfg->sed_cmds.
 * Memory required for compilation context is allocated from cmd->pool.
 */
static apr_status_t compile_sed_expr(sed_expr_config *sed_cfg,
                                     cmd_parms *cmd,
                                     const char *expr)
{
    apr_status_t status = APR_SUCCESS;

    if (!sed_cfg->sed_cmds) {
        sed_commands_t *sed_cmds;
        sed_cmds = apr_pcalloc(cmd->pool, sizeof(sed_commands_t));
        status = sed_init_commands(sed_cmds, sed_compile_errf, sed_cfg,
                                   cmd->pool);
        if (status != APR_SUCCESS) {
            sed_destroy_commands(sed_cmds);
            return status;
        }
        sed_cfg->sed_cmds = sed_cmds;
    }
    status = sed_compile_string(sed_cfg->sed_cmds, expr);
    if (status != APR_SUCCESS) {
        sed_destroy_commands(sed_cfg->sed_cmds);
        sed_cfg->sed_cmds = NULL;
    }
    return status;
}

/* sed eval cleanup function */
static apr_status_t sed_eval_cleanup(void *data)
{
    sed_eval_t *eval = (sed_eval_t *) data;
    sed_destroy_eval(eval);
    return APR_SUCCESS;
}

/* Initialize sed filter context. If successful then context is set in f->ctx
 */
static apr_status_t init_context(ap_filter_t *f, sed_expr_config *sed_cfg, int usetpool)
{
    apr_status_t status;
    sed_filter_ctxt* ctx;
    request_rec *r = f->r;
    /* Create the context. Call sed_init_eval. libsed will generated
     * output by calling sed_write_output and generates any error by
     * invoking log_sed_errf.
     */
    ctx = apr_pcalloc(r->pool, sizeof(sed_filter_ctxt));
    ctx->r = r;
    ctx->bb = NULL;
    ctx->numbuckets = 0;
    ctx->f = f;
    status = sed_init_eval(&ctx->eval, sed_cfg->sed_cmds, log_sed_errf,
                           r, &sed_write_output, r->pool);
    if (status != APR_SUCCESS) {
        return status;
    }
    apr_pool_cleanup_register(r->pool, &ctx->eval, sed_eval_cleanup,
                              apr_pool_cleanup_null);
    ctx->bufsize = MODSED_OUTBUF_SIZE;
    if (usetpool) {
        apr_pool_create(&(ctx->tpool), r->pool);
    }
    else {
        ctx->tpool = r->pool;
    }
    alloc_outbuf(ctx);
    f->ctx = ctx;
    return APR_SUCCESS;
}

/* Entry function for Sed output filter */
static apr_status_t sed_response_filter(ap_filter_t *f,
                                        apr_bucket_brigade *bb)
{
    apr_bucket *b;
    apr_status_t status;
    sed_config *cfg = ap_get_module_config(f->r->per_dir_config,
                                           &sed_module);
    sed_filter_ctxt *ctx = f->ctx;
    sed_expr_config *sed_cfg = &cfg->output;

    if ((sed_cfg == NULL) || (sed_cfg->sed_cmds == NULL)) {
        /* No sed expressions */
        ap_remove_output_filter(f);
        return ap_pass_brigade(f->next, bb);
    }

    if (ctx == NULL) {

        if (APR_BUCKET_IS_EOS(APR_BRIGADE_FIRST(bb))) {
            /* no need to run sed filter for Head requests */
            ap_remove_output_filter(f);
            return ap_pass_brigade(f->next, bb);
        }

        status = init_context(f, sed_cfg, 1);
        if (status != APR_SUCCESS)
             return status;
        ctx = f->ctx;
        apr_table_unset(f->r->headers_out, "Content-Length");
    }

    ctx->bb = apr_brigade_create(f->r->pool, f->c->bucket_alloc);

    /* Here is the main logic. Iterate through all the buckets, read the
     * content of the bucket, call sed_eval_buffer on the data.
     * sed_eval_buffer will read the data line by line, run filters on each
     * line. sed_eval_buffer will generates the output by calling
     * sed_write_output which will add the output to ctx->bb. At the end of
     * the loop, ctx->bb is passed to the next filter in chain. At the end of
     * the data, if new line is not found then sed_eval_buffer will store the
     * data in its own buffer.
     *
     * Once eos bucket is found then sed_finalize_eval will flush the rest of
     * the data. If there is no new line in last line of data, new line is
     * appended (that is a solaris sed behavior). libsed's internal memory for
     * evaluation is allocated on request's pool so it will be cleared once
     * request is over.
     *
     * If flush bucket is found then append the the flush bucket to ctx->bb
     * and pass it to next filter. There may be some data which will still be
     * in sed's internal buffer which can't be flushed until new line
     * character is arrived.
     */
    for (b = APR_BRIGADE_FIRST(bb); b != APR_BRIGADE_SENTINEL(bb);) {
        const char *buf = NULL;
        apr_size_t bytes = 0;
        if (APR_BUCKET_IS_EOS(b)) {
            apr_bucket *b1 = APR_BUCKET_NEXT(b);
            /* Now clean up the internal sed buffer */
            sed_finalize_eval(&ctx->eval, ctx);
            status = flush_output_buffer(ctx);
            if (status != APR_SUCCESS) {
                clear_ctxpool(ctx);
                return status;
            }
            APR_BUCKET_REMOVE(b);
            /* Insert the eos bucket to ctx->bb brigade */
            APR_BRIGADE_INSERT_TAIL(ctx->bb, b);
            b = b1;
        }
        else if (APR_BUCKET_IS_FLUSH(b)) {
            apr_bucket *b1 = APR_BUCKET_NEXT(b);
            APR_BUCKET_REMOVE(b);
            status = flush_output_buffer(ctx);
            if (status != APR_SUCCESS) {
                clear_ctxpool(ctx);
                return status;
            }
            APR_BRIGADE_INSERT_TAIL(ctx->bb, b);
            b = b1;
        }
        else if (APR_BUCKET_IS_METADATA(b)) {
            b = APR_BUCKET_NEXT(b);
        }
        else if (apr_bucket_read(b, &buf, &bytes, APR_BLOCK_READ)
                 == APR_SUCCESS) {
            apr_bucket *b1 = APR_BUCKET_NEXT(b);
            status = sed_eval_buffer(&ctx->eval, buf, bytes, ctx);
            if (status != APR_SUCCESS) {
                clear_ctxpool(ctx);
                return status;
            }
            APR_BUCKET_REMOVE(b);
            apr_bucket_delete(b);
            b = b1;
        }
        else {
            apr_bucket *b1 = APR_BUCKET_NEXT(b);
            APR_BUCKET_REMOVE(b);
            b = b1;
        }
    }
    apr_brigade_cleanup(bb);
    status = flush_output_buffer(ctx);
    if (status != APR_SUCCESS) {
        clear_ctxpool(ctx);
        return status;
    }
    if (!APR_BRIGADE_EMPTY(ctx->bb)) {
        status = ap_pass_brigade(f->next, ctx->bb);
        apr_brigade_cleanup(ctx->bb);
    }
    clear_ctxpool(ctx);
    return status;
}

/* Entry function for Sed input filter */
static apr_status_t sed_request_filter(ap_filter_t *f,
                                       apr_bucket_brigade *bb,
                                       ap_input_mode_t mode,
                                       apr_read_type_e block,
                                       apr_off_t readbytes)
{
    sed_config *cfg = ap_get_module_config(f->r->per_dir_config,
                                           &sed_module);
    sed_filter_ctxt *ctx = f->ctx;
    apr_status_t status;
    apr_bucket_brigade *bbinp;
    sed_expr_config *sed_cfg = &cfg->input;

    if (mode != AP_MODE_READBYTES) {
        return ap_get_brigade(f->next, bb, mode, block, readbytes);
    }

    if ((sed_cfg == NULL) || (sed_cfg->sed_cmds == NULL)) {
        /* No sed expression */
        return ap_get_brigade(f->next, bb, mode, block, readbytes);
    }

    if (!ctx) {
        if (!ap_is_initial_req(f->r)) {
            ap_remove_input_filter(f);
            /* XXX : Should we filter the sub requests too */
            return ap_get_brigade(f->next, bb, mode, block, readbytes);
        }
        status = init_context(f, sed_cfg, 0);
        if (status != APR_SUCCESS)
             return status;
        ctx = f->ctx;
        ctx->bb    = apr_brigade_create(f->r->pool, f->c->bucket_alloc);
        ctx->bbinp = apr_brigade_create(f->r->pool, f->c->bucket_alloc);
    }

    bbinp = ctx->bbinp;

    /* Here is the logic :
     * Read the readbytes data from next level fiter into bbinp. Loop through
     * the buckets in bbinp and read the data from buckets and invoke
     * sed_eval_buffer on the data. libsed will generate its output using
     * sed_write_output which will add data in ctx->bb. Do it until it have
     * atleast one bucket bucket in ctx->bb. At the end of data eos bucket
     * should be there.
     *
     * Once eos bucket is seen, then invoke sed_finalize_eval to clear the
     * output. If the last byte of data is not a new line character then sed
     * will add a new line to the data that is default sed behaviour. Note
     * that using this filter with POST data, caller may not expect this
     * behaviour.
     *
     * If next level fiter generate the flush bucket, we can't do much about
     * it. If we want to return the flush bucket in brigade bb (to the caller)
     * the question is where to add it?
     */
    while (APR_BRIGADE_EMPTY(ctx->bb)) {
        apr_bucket *b;

        /* read the bytes from next level filter */
        apr_brigade_cleanup(bbinp);
        status = ap_get_brigade(f->next, bbinp, mode, block, readbytes);
        if (status != APR_SUCCESS) {
            return status;
        }
        for (b = APR_BRIGADE_FIRST(bbinp); b != APR_BRIGADE_SENTINEL(bbinp);
             b = APR_BUCKET_NEXT(b)) {
            const char *buf = NULL;
            apr_size_t bytes;

            if (APR_BUCKET_IS_EOS(b)) {
                /* eos bucket. Clear the internal sed buffers */
                sed_finalize_eval(&ctx->eval, ctx);
                flush_output_buffer(ctx);
                APR_BUCKET_REMOVE(b);
                APR_BRIGADE_INSERT_TAIL(ctx->bb, b);
                break;
            }
            else if (APR_BUCKET_IS_FLUSH(b)) {
                /* What should we do with flush bucket */
                continue;
            }
            if (apr_bucket_read(b, &buf, &bytes, APR_BLOCK_READ)
                     == APR_SUCCESS) {
                status = sed_eval_buffer(&ctx->eval, buf, bytes, ctx);
                if (status != APR_SUCCESS)
                    return status;
                flush_output_buffer(ctx);
            }
        }
    }

    if (!APR_BRIGADE_EMPTY(ctx->bb)) {
        apr_bucket *b = NULL;

        if (apr_brigade_partition(ctx->bb, readbytes, &b) == APR_INCOMPLETE) {
            APR_BRIGADE_CONCAT(bb, ctx->bb);
        }
        else {
            APR_BRIGADE_CONCAT(bb, ctx->bb);
            apr_brigade_split_ex(bb, b, ctx->bb);
        }
    }
    return APR_SUCCESS;
}

static const char *sed_add_expr(cmd_parms *cmd, void *cfg, const char *arg)
{
    int offset = (int) (long) cmd->info;
    sed_expr_config *sed_cfg =
                (sed_expr_config *) (((char *) cfg) + offset);
    if (compile_sed_expr(sed_cfg, cmd, arg) != APR_SUCCESS) {
        return apr_psprintf(cmd->temp_pool,
                            "Failed to compile sed expression. %s",
                            sed_cfg->last_error);
    }
    return NULL;
}

static void *create_sed_dir_config(apr_pool_t *p, char *s)
{
    sed_config *cfg = apr_pcalloc(p, sizeof(sed_config));
    return cfg;
}

static const command_rec sed_filter_cmds[] = {
    AP_INIT_TAKE1("OutputSed", sed_add_expr,
                  (void *) APR_OFFSETOF(sed_config, output),
                  ACCESS_CONF,
                  "Sed regular expression for Response"),
    AP_INIT_TAKE1("InputSed", sed_add_expr,
                  (void *) APR_OFFSETOF(sed_config, input),
                  ACCESS_CONF,
                  "Sed regular expression for Request"),
    {NULL}
};

static void register_hooks(apr_pool_t *p)
{
    ap_register_output_filter(sed_filter_name, sed_response_filter, NULL,
                              AP_FTYPE_RESOURCE);
    ap_register_input_filter(sed_filter_name, sed_request_filter, NULL,
                             AP_FTYPE_RESOURCE);
}

AP_DECLARE_MODULE(sed) = {
    STANDARD20_MODULE_STUFF,
    create_sed_dir_config,      /* dir config creater */
    NULL,                       /* dir merger --- default is to override */
    NULL,                       /* server config */
    NULL,                       /* merge server config */
    sed_filter_cmds,            /* command table */
    register_hooks              /* register hooks */
};
