/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 "apr.h"
#include "apr_strings.h"
#include "apr_thread_proc.h"
#include "apr_hash.h"
#include "apr_user.h"
#include "apr_lib.h"
#include "apr_optional.h"

#define APR_WANT_STRFUNC
#define APR_WANT_MEMFUNC
#include "apr_want.h"

#include "ap_config.h"
#include "util_filter.h"
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_request.h"
#include "http_core.h"
#include "http_protocol.h"
#include "http_log.h"
#include "http_main.h"
#include "util_script.h"
#include "http_core.h"
#include "mod_include.h"
#include "ap_expr.h"

/* helper for Latin1 <-> entity encoding */
#if APR_CHARSET_EBCDIC
#include "util_ebcdic.h"
#define RAW_ASCII_CHAR(ch)  apr_xlate_conv_byte(ap_hdrs_from_ascii, \
                                                (unsigned char)ch)
#else /* APR_CHARSET_EBCDIC */
#define RAW_ASCII_CHAR(ch)  (ch)
#endif /* !APR_CHARSET_EBCDIC */


/*
 * +-------------------------------------------------------+
 * |                                                       |
 * |                 Types and Structures
 * |                                                       |
 * +-------------------------------------------------------+
 */

/* sll used for string expansion */
typedef struct result_item {
    struct result_item *next;
    apr_size_t len;
    const char *string;
} result_item_t;

/* conditional expression parser stuff */
typedef enum {
    TOKEN_STRING,
    TOKEN_RE,
    TOKEN_AND,
    TOKEN_OR,
    TOKEN_NOT,
    TOKEN_EQ,
    TOKEN_NE,
    TOKEN_RBRACE,
    TOKEN_LBRACE,
    TOKEN_GROUP,
    TOKEN_GE,
    TOKEN_LE,
    TOKEN_GT,
    TOKEN_LT,
    TOKEN_ACCESS
} token_type_t;

typedef struct {
    token_type_t  type;
    const char   *value;
#ifdef DEBUG_INCLUDE
    const char   *s;
#endif
} token_t;

typedef struct parse_node {
    struct parse_node *parent;
    struct parse_node *left;
    struct parse_node *right;
    token_t token;
    int value;
    int done;
#ifdef DEBUG_INCLUDE
    int dump_done;
#endif
} parse_node_t;

typedef enum {
    XBITHACK_OFF,
    XBITHACK_ON,
    XBITHACK_FULL,
    XBITHACK_UNSET
} xbithack_t;

typedef struct {
    const char *default_error_msg;
    const char *default_time_fmt;
    const char *undefined_echo;
    xbithack_t  xbithack;
    signed char lastmodified;
    signed char etag;
    signed char legacy_expr;
} include_dir_config;

typedef struct {
    const char *default_start_tag;
    const char *default_end_tag;
} include_server_config;

/* main parser states */
typedef enum {
    PARSE_PRE_HEAD,
    PARSE_HEAD,
    PARSE_DIRECTIVE,
    PARSE_DIRECTIVE_POSTNAME,
    PARSE_DIRECTIVE_TAIL,
    PARSE_DIRECTIVE_POSTTAIL,
    PARSE_PRE_ARG,
    PARSE_ARG,
    PARSE_ARG_NAME,
    PARSE_ARG_POSTNAME,
    PARSE_ARG_EQ,
    PARSE_ARG_PREVAL,
    PARSE_ARG_VAL,
    PARSE_ARG_VAL_ESC,
    PARSE_ARG_POSTVAL,
    PARSE_TAIL,
    PARSE_TAIL_SEQ,
    PARSE_EXECUTE
} parse_state_t;

typedef struct arg_item {
    struct arg_item  *next;
    char             *name;
    apr_size_t        name_len;
    char             *value;
    apr_size_t        value_len;
} arg_item_t;

typedef struct {
    const char *source;
    const char *rexp;
    apr_size_t  nsub;
    ap_regmatch_t match[AP_MAX_REG_MATCH];
    int have_match;
} backref_t;

typedef struct {
    unsigned int T[256];
    unsigned int x;
    apr_size_t pattern_len;
} bndm_t;

struct ssi_internal_ctx {
    parse_state_t state;
    int           seen_eos;
    int           error;
    char          quote;         /* quote character value (or \0) */
    apr_size_t    parse_pos;     /* parse position of partial matches */
    apr_size_t    bytes_read;

    apr_bucket_brigade *tmp_bb;

    const char   *start_seq;
    bndm_t       *start_seq_pat;
    const char   *end_seq;
    apr_size_t    end_seq_len;
    char         *directive;     /* name of the current directive */
    apr_size_t    directive_len; /* length of the current directive name */

    arg_item_t   *current_arg;   /* currently parsed argument */
    arg_item_t   *argv;          /* all arguments */

    backref_t    *re;            /* NULL if there wasn't a regex yet */

    const char   *undefined_echo;
    apr_size_t    undefined_echo_len;

    char         legacy_expr;     /* use ap_expr or legacy mod_include
                                    expression parser? */

    ap_expr_eval_ctx_t *expr_eval_ctx;  /* NULL if there wasn't an ap_expr yet */
    const char         *expr_vary_this; /* for use by ap_expr_eval_ctx */
    const char         *expr_err;       /* for use by ap_expr_eval_ctx */
#ifdef DEBUG_INCLUDE
    struct {
        ap_filter_t *f;
        apr_bucket_brigade *bb;
    } debug;
#endif
};


/*
 * +-------------------------------------------------------+
 * |                                                       |
 * |                  Debugging Utilities
 * |                                                       |
 * +-------------------------------------------------------+
 */

#ifdef DEBUG_INCLUDE

#define TYPE_TOKEN(token, ttype) do { \
    (token)->type = ttype;            \
    (token)->s = #ttype;              \
} while(0)

#define CREATE_NODE(ctx, name) do {                       \
    (name) = apr_palloc((ctx)->dpool, sizeof(*(name)));   \
    (name)->parent = (name)->left = (name)->right = NULL; \
    (name)->done = 0;                                     \
    (name)->dump_done = 0;                                \
} while(0)

static void debug_printf(include_ctx_t *ctx, const char *fmt, ...)
{
    va_list ap;
    char *debug__str;

    va_start(ap, fmt);
    debug__str = apr_pvsprintf(ctx->pool, fmt, ap);
    va_end(ap);

    APR_BRIGADE_INSERT_TAIL(ctx->intern->debug.bb, apr_bucket_pool_create(
                            debug__str, strlen(debug__str), ctx->pool,
                            ctx->intern->debug.f->c->bucket_alloc));
}

#define DUMP__CHILD(ctx, is, node, child) if (1) {                           \
    parse_node_t *d__c = node->child;                                        \
    if (d__c) {                                                              \
        if (!d__c->dump_done) {                                              \
            if (d__c->parent != node) {                                      \
                debug_printf(ctx, "!!! Parse tree is not consistent !!!\n"); \
                if (!d__c->parent) {                                         \
                    debug_printf(ctx, "Parent of " #child " child node is "  \
                                 "NULL.\n");                                 \
                }                                                            \
                else {                                                       \
                    debug_printf(ctx, "Parent of " #child " child node "     \
                                 "points to another node (of type %s)!\n",   \
                                 d__c->parent->token.s);                     \
                }                                                            \
                return;                                                      \
            }                                                                \
            node = d__c;                                                     \
            continue;                                                        \
        }                                                                    \
    }                                                                        \
    else {                                                                   \
        debug_printf(ctx, "%s(missing)\n", is);                              \
    }                                                                        \
}

static void debug_dump_tree(include_ctx_t *ctx, parse_node_t *root)
{
    parse_node_t *current;
    char *is;

    if (!root) {
        debug_printf(ctx, "     -- Parse Tree empty --\n\n");
        return;
    }

    debug_printf(ctx, "     ----- Parse Tree -----\n");
    current = root;
    is = "     ";

    while (current) {
        switch (current->token.type) {
        case TOKEN_STRING:
        case TOKEN_RE:
            debug_printf(ctx, "%s%s (%s)\n", is, current->token.s,
                         current->token.value);
            current->dump_done = 1;
            current = current->parent;
            continue;

        case TOKEN_NOT:
        case TOKEN_GROUP:
        case TOKEN_RBRACE:
        case TOKEN_LBRACE:
            if (!current->dump_done) {
                debug_printf(ctx, "%s%s\n", is, current->token.s);
                is = apr_pstrcat(ctx->dpool, is, "    ", NULL);
                current->dump_done = 1;
            }

            DUMP__CHILD(ctx, is, current, right)

            if (!current->right || current->right->dump_done) {
                is = apr_pstrmemdup(ctx->dpool, is, strlen(is) - 4);
                if (current->right) current->right->dump_done = 0;
                current = current->parent;
            }
            continue;

        default:
            if (!current->dump_done) {
                debug_printf(ctx, "%s%s\n", is, current->token.s);
                is = apr_pstrcat(ctx->dpool, is, "    ", NULL);
                current->dump_done = 1;
            }

            DUMP__CHILD(ctx, is, current, left)
            DUMP__CHILD(ctx, is, current, right)

            if ((!current->left || current->left->dump_done) &&
                (!current->right || current->right->dump_done)) {

                is = apr_pstrmemdup(ctx->dpool, is, strlen(is) - 4);
                if (current->left) current->left->dump_done = 0;
                if (current->right) current->right->dump_done = 0;
                current = current->parent;
            }
            continue;
        }
    }

    /* it is possible to call this function within the parser loop, to see
     * how the tree is built. That way, we must cleanup after us to dump
     * always the whole tree
     */
    root->dump_done = 0;
    if (root->left) root->left->dump_done = 0;
    if (root->right) root->right->dump_done = 0;

    debug_printf(ctx, "     --- End Parse Tree ---\n\n");
}

#define DEBUG_INIT(ctx, filter, brigade) do { \
    (ctx)->intern->debug.f = filter;          \
    (ctx)->intern->debug.bb = brigade;        \
} while(0)

#define DEBUG_PRINTF(arg) debug_printf arg

#define DEBUG_DUMP_TOKEN(ctx, token) do {                                     \
    token_t *d__t = (token);                                                  \
                                                                              \
    if (d__t->type == TOKEN_STRING || d__t->type == TOKEN_RE) {               \
        DEBUG_PRINTF(((ctx), "     Found: %s (%s)\n", d__t->s, d__t->value)); \
    }                                                                         \
    else {                                                                    \
        DEBUG_PRINTF((ctx, "     Found: %s\n", d__t->s));                     \
    }                                                                         \
} while(0)

#define DEBUG_DUMP_EVAL(ctx, node) do {                                       \
    char c = '"';                                                             \
    switch ((node)->token.type) {                                             \
    case TOKEN_STRING:                                                        \
        debug_printf((ctx), "     Evaluate: %s (%s) -> %c\n", (node)->token.s,\
                     (node)->token.value, ((node)->value) ? '1':'0');         \
        break;                                                                \
    case TOKEN_AND:                                                           \
    case TOKEN_OR:                                                            \
        debug_printf((ctx), "     Evaluate: %s (Left: %s; Right: %s) -> %c\n",\
                     (node)->token.s,                                         \
                     (((node)->left->done) ? ((node)->left->value ?"1":"0")   \
                                          : "short circuited"),               \
                     (((node)->right->done) ? ((node)->right->value?"1":"0")  \
                                          : "short circuited"),               \
                     (node)->value ? '1' : '0');                              \
        break;                                                                \
    case TOKEN_EQ:                                                            \
    case TOKEN_NE:                                                            \
    case TOKEN_GT:                                                            \
    case TOKEN_GE:                                                            \
    case TOKEN_LT:                                                            \
    case TOKEN_LE:                                                            \
        if ((node)->right->token.type == TOKEN_RE) c = '/';                   \
        debug_printf((ctx), "     Compare:  %s (\"%s\" with %c%s%c) -> %c\n", \
                     (node)->token.s,                                         \
                     (node)->left->token.value,                               \
                     c, (node)->right->token.value, c,                        \
                     (node)->value ? '1' : '0');                              \
        break;                                                                \
    default:                                                                  \
        debug_printf((ctx), "     Evaluate: %s -> %c\n", (node)->token.s,     \
                     (node)->value ? '1' : '0');                              \
        break;                                                                \
    }                                                                         \
} while(0)

#define DEBUG_DUMP_UNMATCHED(ctx, unmatched) do {                        \
    if (unmatched) {                                                     \
        DEBUG_PRINTF(((ctx), "     Unmatched %c\n", (char)(unmatched))); \
    }                                                                    \
} while(0)

#define DEBUG_DUMP_COND(ctx, text)                                 \
    DEBUG_PRINTF(((ctx), "**** %s cond status=\"%c\"\n", (text),   \
                  ((ctx)->flags & SSI_FLAG_COND_TRUE) ? '1' : '0'))

#define DEBUG_DUMP_TREE(ctx, root) debug_dump_tree(ctx, root)

#else /* DEBUG_INCLUDE */

#define TYPE_TOKEN(token, ttype) (token)->type = ttype

#define CREATE_NODE(ctx, name) do {                       \
    (name) = apr_palloc((ctx)->dpool, sizeof(*(name)));   \
    (name)->parent = (name)->left = (name)->right = NULL; \
    (name)->done = 0;                                     \
} while(0)

#define DEBUG_INIT(ctx, f, bb)
#define DEBUG_PRINTF(arg)
#define DEBUG_DUMP_TOKEN(ctx, token)
#define DEBUG_DUMP_EVAL(ctx, node)
#define DEBUG_DUMP_UNMATCHED(ctx, unmatched)
#define DEBUG_DUMP_COND(ctx, text)
#define DEBUG_DUMP_TREE(ctx, root)

#endif /* !DEBUG_INCLUDE */


/*
 * +-------------------------------------------------------+
 * |                                                       |
 * |                 Static Module Data
 * |                                                       |
 * +-------------------------------------------------------+
 */

/* global module structure */
module AP_MODULE_DECLARE_DATA include_module;

/* function handlers for include directives */
static apr_hash_t *include_handlers;

/* forward declaration of handler registry */
static APR_OPTIONAL_FN_TYPE(ap_register_include_handler) *ssi_pfn_register;

/* Sentinel value to store in subprocess_env for items that
 * shouldn't be evaluated until/unless they're actually used
 */
static const char lazy_eval_sentinel = '\0';
#define LAZY_VALUE (&lazy_eval_sentinel)

/* default values */
#define DEFAULT_START_SEQUENCE "<!--#"
#define DEFAULT_END_SEQUENCE "-->"
#define DEFAULT_ERROR_MSG "[an error occurred while processing this directive]"
#define DEFAULT_TIME_FORMAT "%A, %d-%b-%Y %H:%M:%S %Z"
#define DEFAULT_UNDEFINED_ECHO "(none)"

#define UNSET -1

#ifdef XBITHACK
#define DEFAULT_XBITHACK XBITHACK_FULL
#else
#define DEFAULT_XBITHACK XBITHACK_OFF
#endif


/*
 * +-------------------------------------------------------+
 * |                                                       |
 * |            Environment/Expansion Functions
 * |                                                       |
 * +-------------------------------------------------------+
 */

/*
 * decodes a string containing html entities or numeric character references.
 * 's' is overwritten with the decoded string.
 * If 's' is syntatically incorrect, then the followed fixups will be made:
 *   unknown entities will be left undecoded;
 *   references to unused numeric characters will be deleted.
 *   In particular, &#00; will not be decoded, but will be deleted.
 */

/* maximum length of any ISO-LATIN-1 HTML entity name. */
#define MAXENTLEN (6)

/* The following is a shrinking transformation, therefore safe. */

static void decodehtml(char *s)
{
    int val, i, j;
    char *p;
    const char *ents;
    static const char * const entlist[MAXENTLEN + 1] =
    {
        NULL,                     /* 0 */
        NULL,                     /* 1 */
        "lt\074gt\076",           /* 2 */
        "amp\046ETH\320eth\360",  /* 3 */
        "quot\042Auml\304Euml\313Iuml\317Ouml\326Uuml\334auml\344euml"
        "\353iuml\357ouml\366uuml\374yuml\377",                         /* 4 */

        "Acirc\302Aring\305AElig\306Ecirc\312Icirc\316Ocirc\324Ucirc"
        "\333THORN\336szlig\337acirc\342aring\345aelig\346ecirc\352"
        "icirc\356ocirc\364ucirc\373thorn\376",                         /* 5 */

        "Agrave\300Aacute\301Atilde\303Ccedil\307Egrave\310Eacute\311"
        "Igrave\314Iacute\315Ntilde\321Ograve\322Oacute\323Otilde"
        "\325Oslash\330Ugrave\331Uacute\332Yacute\335agrave\340"
        "aacute\341atilde\343ccedil\347egrave\350eacute\351igrave"
        "\354iacute\355ntilde\361ograve\362oacute\363otilde\365"
        "oslash\370ugrave\371uacute\372yacute\375"                      /* 6 */
    };

    /* Do a fast scan through the string until we find anything
     * that needs more complicated handling
     */
    for (; *s != '&'; s++) {
        if (*s == '\0') {
            return;
        }
    }

    for (p = s; *s != '\0'; s++, p++) {
        if (*s != '&') {
            *p = *s;
            continue;
        }
        /* find end of entity */
        for (i = 1; s[i] != ';' && s[i] != '\0'; i++) {
            continue;
        }

        if (s[i] == '\0') {     /* treat as normal data */
            *p = *s;
            continue;
        }

        /* is it numeric ? */
        if (s[1] == '#') {
            for (j = 2, val = 0; j < i && apr_isdigit(s[j]); j++) {
                val = val * 10 + s[j] - '0';
            }
            s += i;
            if (j < i || val <= 8 || (val >= 11 && val <= 31) ||
                (val >= 127 && val <= 160) || val >= 256) {
                p--;            /* no data to output */
            }
            else {
                *p = RAW_ASCII_CHAR(val);
            }
        }
        else {
            j = i - 1;
            if (j > MAXENTLEN || entlist[j] == NULL) {
                /* wrong length */
                *p = '&';
                continue;       /* skip it */
            }
            for (ents = entlist[j]; *ents != '\0'; ents += i) {
                if (strncmp(s + 1, ents, j) == 0) {
                    break;
                }
            }

            if (*ents == '\0') {
                *p = '&';       /* unknown */
            }
            else {
                *p = RAW_ASCII_CHAR(((const unsigned char *) ents)[j]);
                s += i;
            }
        }
    }

    *p = '\0';
}

static void add_include_vars(request_rec *r)
{
    apr_table_t *e = r->subprocess_env;
    char *t;

    apr_table_setn(e, "DATE_LOCAL", LAZY_VALUE);
    apr_table_setn(e, "DATE_GMT", LAZY_VALUE);
    apr_table_setn(e, "LAST_MODIFIED", LAZY_VALUE);
    apr_table_setn(e, "DOCUMENT_URI", r->uri);
    apr_table_setn(e, "DOCUMENT_ARGS", r->args ? r->args : "");
    if (r->path_info && *r->path_info) {
        apr_table_setn(e, "DOCUMENT_PATH_INFO", r->path_info);
    }
    apr_table_setn(e, "USER_NAME", LAZY_VALUE);
    if (r->filename && (t = strrchr(r->filename, '/'))) {
        apr_table_setn(e, "DOCUMENT_NAME", ++t);
    }
    else {
        apr_table_setn(e, "DOCUMENT_NAME", r->uri);
    }
    if (r->args) {
        char *arg_copy = apr_pstrdup(r->pool, r->args);

        ap_unescape_url(arg_copy);
        apr_table_setn(e, "QUERY_STRING_UNESCAPED",
                  ap_escape_shell_cmd(r->pool, arg_copy));
    }
}

static const char *add_include_vars_lazy(request_rec *r, const char *var, const char *timefmt)
{
    char *val;
    if (!strcasecmp(var, "DATE_LOCAL")) {
        val = ap_ht_time(r->pool, r->request_time, timefmt, 0);
    }
    else if (!strcasecmp(var, "DATE_GMT")) {
        val = ap_ht_time(r->pool, r->request_time, timefmt, 1);
    }
    else if (!strcasecmp(var, "LAST_MODIFIED")) {
        val = ap_ht_time(r->pool, r->finfo.mtime, timefmt, 0);
    }
    else if (!strcasecmp(var, "USER_NAME")) {
        if (apr_uid_name_get(&val, r->finfo.user, r->pool) != APR_SUCCESS) {
            val = "<unknown>";
        }
    }
    else {
        val = NULL;
    }

    if (val) {
        apr_table_setn(r->subprocess_env, var, val);
    }
    return val;
}

static const char *get_include_var(const char *var, include_ctx_t *ctx)
{
    const char *val;
    request_rec *r = ctx->r;

    if (apr_isdigit(*var) && !var[1]) {
        apr_size_t idx = *var - '0';
        backref_t *re = ctx->intern->re;

        /* Handle $0 .. $9 from the last regex evaluated.
         * The choice of returning NULL strings on not-found,
         * v.s. empty strings on an empty match is deliberate.
         */
        if (!re || !re->have_match) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01329)
                "regex capture $%" APR_SIZE_T_FMT " refers to no regex in %s",
                idx, r->filename);
            return NULL;
        }
        else if (re->nsub < idx || idx >= AP_MAX_REG_MATCH) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01330)
                          "regex capture $%" APR_SIZE_T_FMT
                          " is out of range (last regex was: '%s') in %s",
                          idx, re->rexp, r->filename);
            return NULL;
        }
        else if (re->match[idx].rm_so < 0 || re->match[idx].rm_eo < 0) {
            /* This particular subpattern was not used by the regex */
            return NULL;
        }
        else {
            val = apr_pstrmemdup(ctx->dpool, re->source + re->match[idx].rm_so,
                                 re->match[idx].rm_eo - re->match[idx].rm_so);
        }
    }
    else {
        val = apr_table_get(r->subprocess_env, var);

        if (val == LAZY_VALUE) {
            val = add_include_vars_lazy(r, var, ctx->time_str);
        }
    }

    return val;
}

static const char *include_expr_var_fn(ap_expr_eval_ctx_t *eval_ctx,
                                       const void *data,
                                       const char *arg)
{
    const char *res, *name = data;
    include_ctx_t *ctx = eval_ctx->data;
    if ((name[0] == 'e') || (name[0] == 'E')) {
        /* keep legacy "env" semantics */
        if ((res = apr_table_get(ctx->r->notes, arg)) != NULL)
            return res;
        else if ((res = get_include_var(arg, ctx)) != NULL)
            return res;
        else
            return getenv(arg);
    }
    else {
        return get_include_var(arg, ctx);
    }
}

static int include_expr_lookup(ap_expr_lookup_parms *parms)
{
    switch (parms->type) {
    case AP_EXPR_FUNC_STRING:
        if (strcasecmp(parms->name, "v") == 0 ||
            strcasecmp(parms->name, "reqenv") == 0 ||
            strcasecmp(parms->name, "env") == 0) {
            *parms->func = include_expr_var_fn;
            *parms->data = parms->name;
            return OK;
        }
        break;
    /*
     * We could also make the SSI vars available as %{...} style variables
     * (AP_EXPR_FUNC_VAR), but this would create problems if we ever want
     * to cache parsed expressions for performance reasons.
     */
    }
    return ap_run_expr_lookup(parms);
}


/*
 * Do variable substitution on strings
 *
 * (Note: If out==NULL, this function allocs a buffer for the resulting
 * string from ctx->pool. The return value is always the parsed string)
 */
static char *ap_ssi_parse_string(include_ctx_t *ctx, const char *in, char *out,
                                 apr_size_t length, int leave_name)
{
    request_rec *r = ctx->r;
    result_item_t *result = NULL, *current = NULL;
    apr_size_t outlen = 0, inlen, span;
    char *ret = NULL, *eout = NULL;
    const char *p;

    if (out) {
        /* sanity check, out && !length is not supported */
        ap_assert(out && length);

        ret = out;
        eout = out + length - 1;
    }

    span = strcspn(in, "\\$");
    inlen = strlen(in);

    /* fast exit */
    if (inlen == span) {
        if (out) {
            apr_cpystrn(out, in, length);
        }
        else {
            ret = apr_pstrmemdup(ctx->pool, in, (length && length <= inlen)
                                                ? length - 1 : inlen);
        }

        return ret;
    }

    /* well, actually something to do */
    p = in + span;

    if (out) {
        if (span) {
            memcpy(out, in, (out+span <= eout) ? span : (eout-out));
            out += span;
        }
    }
    else {
        current = result = apr_palloc(ctx->dpool, sizeof(*result));
        current->next = NULL;
        current->string = in;
        current->len = span;
        outlen = span;
    }

    /* loop for specials */
    do {
        if ((out && out >= eout) || (length && outlen >= length)) {
            break;
        }

        /* prepare next entry */
        if (!out && current->len) {
            current->next = apr_palloc(ctx->dpool, sizeof(*current->next));
            current = current->next;
            current->next = NULL;
            current->len = 0;
        }

        /*
         * escaped character
         */
        if (*p == '\\') {
            if (out) {
                *out++ = (p[1] == '$') ? *++p : *p;
                ++p;
            }
            else {
                current->len = 1;
                current->string = (p[1] == '$') ? ++p : p;
                ++p;
                ++outlen;
            }
        }

        /*
         * variable expansion
         */
        else {       /* *p == '$' */
            const char *newp = NULL, *ep, *key = NULL;

            if (*++p == '{') {
                ep = ap_strchr_c(++p, '}');
                if (!ep) {
                    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01331) "Missing '}' on "
                                  "variable \"%s\" in %s", p, r->filename);
                    break;
                }

                if (p < ep) {
                    key = apr_pstrmemdup(ctx->dpool, p, ep - p);
                    newp = ep + 1;
                }
                p -= 2;
            }
            else {
                ep = p;
                while (*ep == '_' || apr_isalnum(*ep)) {
                    ++ep;
                }

                if (p < ep) {
                    key = apr_pstrmemdup(ctx->dpool, p, ep - p);
                    newp = ep;
                }
                --p;
            }

            /* empty name results in a copy of '$' in the output string */
            if (!key) {
                if (out) {
                    *out++ = *p++;
                }
                else {
                    current->len = 1;
                    current->string = p++;
                    ++outlen;
                }
            }
            else {
                const char *val = get_include_var(key, ctx);
                apr_size_t len = 0;

                if (val) {
                    len = strlen(val);
                }
                else if (leave_name) {
                    val = p;
                    len = ep - p;
                }

                if (val && len) {
                    if (out) {
                        memcpy(out, val, (out+len <= eout) ? len : (eout-out));
                        out += len;
                    }
                    else {
                        current->len = len;
                        current->string = val;
                        outlen += len;
                    }
                }

                p = newp;
            }
        }

        if ((out && out >= eout) || (length && outlen >= length)) {
            break;
        }

        /* check the remainder */
        if (*p && (span = strcspn(p, "\\$")) > 0) {
            if (!out && current->len) {
                current->next = apr_palloc(ctx->dpool, sizeof(*current->next));
                current = current->next;
                current->next = NULL;
            }

            if (out) {
                memcpy(out, p, (out+span <= eout) ? span : (eout-out));
                out += span;
            }
            else {
                current->len = span;
                current->string = p;
                outlen += span;
            }

            p += span;
        }
    } while (p < in+inlen);

    /* assemble result */
    if (out) {
        if (out > eout) {
            *eout = '\0';
        }
        else {
            *out = '\0';
        }
    }
    else {
        const char *ep;

        if (length && outlen > length) {
            outlen = length - 1;
        }

        ret = out = apr_palloc(ctx->pool, outlen + 1);
        ep = ret + outlen;

        do {
            if (result->len) {
                memcpy(out, result->string, (out+result->len <= ep)
                                            ? result->len : (ep-out));
                out += result->len;
            }
            result = result->next;
        } while (result && out < ep);

        ret[outlen] = '\0';
    }

    return ret;
}


/*
 * +-------------------------------------------------------+
 * |                                                       |
 * |              Conditional Expression Parser
 * |                                                       |
 * +-------------------------------------------------------+
 */

static APR_INLINE int re_check(include_ctx_t *ctx, const char *string,
                               const char *rexp)
{
    ap_regex_t *compiled;
    backref_t *re = ctx->intern->re;

    compiled = ap_pregcomp(ctx->dpool, rexp, AP_REG_EXTENDED);
    if (!compiled) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, ctx->r, APLOGNO(02667)
                      "unable to compile pattern \"%s\"", rexp);
        return -1;
    }

    if (!re) {
        re = ctx->intern->re = apr_palloc(ctx->pool, sizeof(*re));
    }

    re->source = apr_pstrdup(ctx->pool, string);
    re->rexp = apr_pstrdup(ctx->pool, rexp);
    re->nsub = compiled->re_nsub;
    re->have_match = !ap_regexec(compiled, string, AP_MAX_REG_MATCH,
                                 re->match, 0);

    ap_pregfree(ctx->dpool, compiled);
    return re->have_match;
}

static int get_ptoken(include_ctx_t *ctx, const char **parse, token_t *token, token_t *previous)
{
    const char *p;
    apr_size_t shift;
    int unmatched;

    token->value = NULL;

    if (!*parse) {
        return 0;
    }

    /* Skip leading white space */
    while (apr_isspace(**parse)) {
        ++*parse;
    }

    if (!**parse) {
        *parse = NULL;
        return 0;
    }

    TYPE_TOKEN(token, TOKEN_STRING); /* the default type */
    p = *parse;
    unmatched = 0;

    switch (*(*parse)++) {
    case '(':
        TYPE_TOKEN(token, TOKEN_LBRACE);
        return 0;
    case ')':
        TYPE_TOKEN(token, TOKEN_RBRACE);
        return 0;
    case '=':
        if (**parse == '=') ++*parse;
        TYPE_TOKEN(token, TOKEN_EQ);
        return 0;
    case '!':
        if (**parse == '=') {
            TYPE_TOKEN(token, TOKEN_NE);
            ++*parse;
            return 0;
        }
        TYPE_TOKEN(token, TOKEN_NOT);
        return 0;
    case '\'':
        unmatched = '\'';
        break;
    case '/':
        /* if last token was ACCESS, this token is STRING */
        if (previous != NULL && TOKEN_ACCESS == previous->type) {
            break;
        }
        TYPE_TOKEN(token, TOKEN_RE);
        unmatched = '/';
        break;
    case '|':
        if (**parse == '|') {
            TYPE_TOKEN(token, TOKEN_OR);
            ++*parse;
            return 0;
        }
        break;
    case '&':
        if (**parse == '&') {
            TYPE_TOKEN(token, TOKEN_AND);
            ++*parse;
            return 0;
        }
        break;
    case '>':
        if (**parse == '=') {
            TYPE_TOKEN(token, TOKEN_GE);
            ++*parse;
            return 0;
        }
        TYPE_TOKEN(token, TOKEN_GT);
        return 0;
    case '<':
        if (**parse == '=') {
            TYPE_TOKEN(token, TOKEN_LE);
            ++*parse;
            return 0;
        }
        TYPE_TOKEN(token, TOKEN_LT);
        return 0;
    case '-':
        if (**parse == 'A') {
            TYPE_TOKEN(token, TOKEN_ACCESS);
            ++*parse;
            return 0;
        }
        break;
    }

    /* It's a string or regex token
     * Now search for the next token, which finishes this string
     */
    shift = 0;
    p = *parse = token->value = unmatched ? *parse : p;

    for (; **parse; p = ++*parse) {
        if (**parse == '\\') {
            if (!*(++*parse)) {
                p = *parse;
                break;
            }

            ++shift;
        }
        else {
            if (unmatched) {
                if (**parse == unmatched) {
                    unmatched = 0;
                    ++*parse;
                    break;
                }
            } else if (apr_isspace(**parse)) {
                break;
            }
            else {
                int found = 0;

                switch (**parse) {
                case '(':
                case ')':
                case '=':
                case '!':
                case '<':
                case '>':
                    ++found;
                    break;

                case '|':
                case '&':
                    if ((*parse)[1] == **parse) {
                        ++found;
                    }
                    break;
                }

                if (found) {
                    break;
                }
            }
        }
    }

    if (unmatched) {
        token->value = apr_pstrdup(ctx->dpool, "");
    }
    else {
        apr_size_t len = p - token->value - shift;
        char *c = apr_palloc(ctx->dpool, len + 1);

        p = token->value;
        token->value = c;

        while (shift--) {
            const char *e = ap_strchr_c(p, '\\');

            memcpy(c, p, e-p);
            c   += e-p;
            *c++ = *++e;
            len -= e-p;
            p    = e+1;
        }

        if (len) {
            memcpy(c, p, len);
        }
        c[len] = '\0';
    }

    return unmatched;
}

static int parse_expr(include_ctx_t *ctx, const char *expr, int *was_error)
{
    parse_node_t *new, *root = NULL, *current = NULL;
    request_rec *r = ctx->r;
    request_rec *rr = NULL;
    const char *error = APLOGNO(03188) "Invalid expression \"%s\" in file %s";
    const char *parse = expr;
    unsigned regex = 0;

    *was_error = 0;

    if (!parse) {
        return 0;
    }

    /* Create Parse Tree */
    while (1) {
        /* uncomment this to see how the tree a built:
         *
         * DEBUG_DUMP_TREE(ctx, root);
         */
        CREATE_NODE(ctx, new);

        {
#ifdef DEBUG_INCLUDE
            int was_unmatched =
#endif
            get_ptoken(ctx, &parse, &new->token,
                       (current != NULL ? &current->token : NULL));
            if (!parse)
                break;

            DEBUG_DUMP_UNMATCHED(ctx, was_unmatched);
            DEBUG_DUMP_TOKEN(ctx, &new->token);
        }

        if (!current) {
            switch (new->token.type) {
            case TOKEN_STRING:
            case TOKEN_NOT:
            case TOKEN_ACCESS:
            case TOKEN_LBRACE:
                root = current = new;
                continue;

            default:
                /* Intentional no APLOGNO */
                /* error text provides APLOGNO */
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, error, expr,
                              r->filename);
                *was_error = 1;
                return 0;
            }
        }

        switch (new->token.type) {
        case TOKEN_STRING:
            switch (current->token.type) {
            case TOKEN_STRING:
                current->token.value =
                    apr_pstrcat(ctx->dpool, current->token.value,
                                *current->token.value ? " " : "",
                                new->token.value, NULL);
                continue;

            case TOKEN_RE:
            case TOKEN_RBRACE:
            case TOKEN_GROUP:
                break;

            default:
                new->parent = current;
                current = current->right = new;
                continue;
            }
            break;

        case TOKEN_RE:
            switch (current->token.type) {
            case TOKEN_EQ:
            case TOKEN_NE:
                new->parent = current;
                current = current->right = new;
                ++regex;
                continue;

            default:
                break;
            }
            break;

        case TOKEN_AND:
        case TOKEN_OR:
            switch (current->token.type) {
            case TOKEN_STRING:
            case TOKEN_RE:
            case TOKEN_GROUP:
                current = current->parent;

                while (current) {
                    switch (current->token.type) {
                    case TOKEN_AND:
                    case TOKEN_OR:
                    case TOKEN_LBRACE:
                        break;

                    default:
                        current = current->parent;
                        continue;
                    }
                    break;
                }

                if (!current) {
                    new->left = root;
                    root->parent = new;
                    current = root = new;
                    continue;
                }

                new->left = current->right;
                new->left->parent = new;
                new->parent = current;
                current = current->right = new;
                continue;

            default:
                break;
            }
            break;

        case TOKEN_EQ:
        case TOKEN_NE:
        case TOKEN_GE:
        case TOKEN_GT:
        case TOKEN_LE:
        case TOKEN_LT:
            if (current->token.type == TOKEN_STRING) {
                current = current->parent;

                if (!current) {
                    new->left = root;
                    root->parent = new;
                    current = root = new;
                    continue;
                }

                switch (current->token.type) {
                case TOKEN_LBRACE:
                case TOKEN_AND:
                case TOKEN_OR:
                    new->left = current->right;
                    new->left->parent = new;
                    new->parent = current;
                    current = current->right = new;
                    continue;

                default:
                    break;
                }
            }
            break;

        case TOKEN_RBRACE:
            while (current && current->token.type != TOKEN_LBRACE) {
                current = current->parent;
            }

            if (current) {
                TYPE_TOKEN(&current->token, TOKEN_GROUP);
                continue;
            }

            error = APLOGNO(03189) "Unmatched ')' in \"%s\" in file %s";
            break;

        case TOKEN_NOT:
        case TOKEN_ACCESS:
        case TOKEN_LBRACE:
            switch (current->token.type) {
            case TOKEN_STRING:
            case TOKEN_RE:
            case TOKEN_RBRACE:
            case TOKEN_GROUP:
                break;

            default:
                current->right = new;
                new->parent = current;
                current = new;
                continue;
            }
            break;

        default:
            break;
        }

        /* Intentional no APLOGNO */
        /* error text provides APLOGNO */
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, error, expr, r->filename);
        *was_error = 1;
        return 0;
    }

    DEBUG_DUMP_TREE(ctx, root);

    /* Evaluate Parse Tree */
    current = root;
    error = NULL;
    while (current) {
        switch (current->token.type) {
        case TOKEN_STRING:
            current->token.value =
                ap_ssi_parse_string(ctx, current->token.value, NULL, 0,
                                    SSI_EXPAND_DROP_NAME);
            current->value = !!*current->token.value;
            break;

        case TOKEN_AND:
        case TOKEN_OR:
            if (!current->left || !current->right) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01332)
                              "Invalid expression \"%s\" in file %s",
                              expr, r->filename);
                *was_error = 1;
                return 0;
            }

            if (!current->left->done) {
                switch (current->left->token.type) {
                case TOKEN_STRING:
                    current->left->token.value =
                        ap_ssi_parse_string(ctx, current->left->token.value,
                                            NULL, 0, SSI_EXPAND_DROP_NAME);
                    current->left->value = !!*current->left->token.value;
                    DEBUG_DUMP_EVAL(ctx, current->left);
                    current->left->done = 1;
                    break;

                default:
                    current = current->left;
                    continue;
                }
            }

            /* short circuit evaluation */
            if (!current->right->done && !regex &&
                ((current->token.type == TOKEN_AND && !current->left->value) ||
                (current->token.type == TOKEN_OR && current->left->value))) {
                current->value = current->left->value;
            }
            else {
                if (!current->right->done) {
                    switch (current->right->token.type) {
                    case TOKEN_STRING:
                        current->right->token.value =
                            ap_ssi_parse_string(ctx,current->right->token.value,
                                                NULL, 0, SSI_EXPAND_DROP_NAME);
                        current->right->value = !!*current->right->token.value;
                        DEBUG_DUMP_EVAL(ctx, current->right);
                        current->right->done = 1;
                        break;

                    default:
                        current = current->right;
                        continue;
                    }
                }

                if (current->token.type == TOKEN_AND) {
                    current->value = current->left->value &&
                                     current->right->value;
                }
                else {
                    current->value = current->left->value ||
                                     current->right->value;
                }
            }
            break;

        case TOKEN_EQ:
        case TOKEN_NE:
            if (!current->left || !current->right ||
                current->left->token.type != TOKEN_STRING ||
                (current->right->token.type != TOKEN_STRING &&
                 current->right->token.type != TOKEN_RE)) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01333)
                            "Invalid expression \"%s\" in file %s",
                            expr, r->filename);
                *was_error = 1;
                return 0;
            }
            current->left->token.value =
                ap_ssi_parse_string(ctx, current->left->token.value, NULL, 0,
                                    SSI_EXPAND_DROP_NAME);
            current->right->token.value =
                ap_ssi_parse_string(ctx, current->right->token.value, NULL, 0,
                                    SSI_EXPAND_DROP_NAME);

            if (current->right->token.type == TOKEN_RE) {
                current->value = re_check(ctx, current->left->token.value,
                                          current->right->token.value);
                --regex;
            }
            else {
                current->value = !strcmp(current->left->token.value,
                                         current->right->token.value);
            }

            if (current->token.type == TOKEN_NE) {
                current->value = !current->value;
            }
            break;

        case TOKEN_GE:
        case TOKEN_GT:
        case TOKEN_LE:
        case TOKEN_LT:
            if (!current->left || !current->right ||
                current->left->token.type != TOKEN_STRING ||
                current->right->token.type != TOKEN_STRING) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01334)
                              "Invalid expression \"%s\" in file %s",
                              expr, r->filename);
                *was_error = 1;
                return 0;
            }

            current->left->token.value =
                ap_ssi_parse_string(ctx, current->left->token.value, NULL, 0,
                                    SSI_EXPAND_DROP_NAME);
            current->right->token.value =
                ap_ssi_parse_string(ctx, current->right->token.value, NULL, 0,
                                    SSI_EXPAND_DROP_NAME);

            current->value = strcmp(current->left->token.value,
                                    current->right->token.value);

            switch (current->token.type) {
            case TOKEN_GE: current->value = current->value >= 0; break;
            case TOKEN_GT: current->value = current->value >  0; break;
            case TOKEN_LE: current->value = current->value <= 0; break;
            case TOKEN_LT: current->value = current->value <  0; break;
            default: current->value = 0; break; /* should not happen */
            }
            break;

        case TOKEN_NOT:
        case TOKEN_GROUP:
            if (current->right) {
                if (!current->right->done) {
                    current = current->right;
                    continue;
                }
                current->value = current->right->value;
            }
            else {
                current->value = 1;
            }

            if (current->token.type == TOKEN_NOT) {
                current->value = !current->value;
            }
            break;

        case TOKEN_ACCESS:
            if (current->left || !current->right ||
                (current->right->token.type != TOKEN_STRING &&
                 current->right->token.type != TOKEN_RE)) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01335)
                            "Invalid expression \"%s\" in file %s: Token '-A' must be followed by a URI string.",
                            expr, r->filename);
                *was_error = 1;
                return 0;
            }
            current->right->token.value =
                ap_ssi_parse_string(ctx, current->right->token.value, NULL, 0,
                                    SSI_EXPAND_DROP_NAME);
            rr = ap_sub_req_lookup_uri(current->right->token.value, r, NULL);
            /* 400 and higher are considered access denied */
            if (rr->status < HTTP_BAD_REQUEST) {
                current->value = 1;
            }
            else {
                current->value = 0;
                ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rr->status, r, APLOGNO(01336)
                              "mod_include: The tested "
                              "subrequest -A \"%s\" returned an error code.",
                              current->right->token.value);
            }
            ap_destroy_sub_req(rr);
            break;

        case TOKEN_RE:
            if (!error) {
                error = APLOGNO(03190) "No operator before regex in expr \"%s\" in file %s";
            }
        case TOKEN_LBRACE:
            if (!error) {
                error = APLOGNO(03191) "Unmatched '(' in \"%s\" in file %s";
            }
        default:
            if (!error) {
                error = APLOGNO(03192) "internal parser error in \"%s\" in file %s";
            }

            /* Intentional no APLOGNO */
            /* error text provides APLOGNO */
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, error, expr, r->filename);
            *was_error = 1;
            return 0;
        }

        DEBUG_DUMP_EVAL(ctx, current);
        current->done = 1;
        current = current->parent;
    }

    return (root ? root->value : 0);
}

/* same as above, but use common ap_expr syntax / API */
static int parse_ap_expr(include_ctx_t *ctx, const char *expr, int *was_error)
{
    ap_expr_info_t expr_info;
    const char *err;
    int ret;
    backref_t *re = ctx->intern->re;
    ap_expr_eval_ctx_t *eval_ctx = ctx->intern->expr_eval_ctx;

    expr_info.filename = ctx->r->filename;
    expr_info.line_number = 0;
    expr_info.module_index = APLOG_MODULE_INDEX;
    expr_info.flags = AP_EXPR_FLAG_RESTRICTED;
    err = ap_expr_parse(ctx->r->pool, ctx->r->pool, &expr_info, expr,
                        include_expr_lookup);
    if (err) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, ctx->r, APLOGNO(01337)
                      "Could not parse expr \"%s\" in %s: %s", expr,
                      ctx->r->filename, err);
        *was_error = 1;
        return 0;
    }

    if (!re) {
        ctx->intern->re = re = apr_pcalloc(ctx->pool, sizeof(*re));
    }
    else {
        /* ap_expr_exec_ctx() does not care about re->have_match but only about
         * re->source
         */
        if (!re->have_match)
            re->source = NULL;
    }

    if (!eval_ctx) {
        eval_ctx = apr_pcalloc(ctx->pool, sizeof(*eval_ctx));
        ctx->intern->expr_eval_ctx = eval_ctx;
        eval_ctx->r         = ctx->r;
        eval_ctx->c         = ctx->r->connection;
        eval_ctx->s         = ctx->r->server;
        eval_ctx->p         = ctx->r->pool;
        eval_ctx->data      = ctx;
        eval_ctx->err       = &ctx->intern->expr_err;
        eval_ctx->vary_this = &ctx->intern->expr_vary_this;
        eval_ctx->re_nmatch = AP_MAX_REG_MATCH;
        eval_ctx->re_pmatch = re->match;
        eval_ctx->re_source = &re->source;
    }

    eval_ctx->info = &expr_info;
    ret = ap_expr_exec_ctx(eval_ctx);
    if (ret < 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, ctx->r, APLOGNO(01338)
                      "Could not evaluate expr \"%s\" in %s: %s", expr,
                      ctx->r->filename, ctx->intern->expr_err);
        *was_error = 1;
        return 0;
    }
    *was_error = 0;
    if (re->source)
        re->have_match = 1;
    return ret;
}

/*
 * +-------------------------------------------------------+
 * |                                                       |
 * |                    Action Handlers
 * |                                                       |
 * +-------------------------------------------------------+
 */

/*
 * Extract the next tag name and value.
 * If there are no more tags, set the tag name to NULL.
 * The tag value is html decoded if dodecode is non-zero.
 * The tag value may be NULL if there is no tag value..
 */
static void ap_ssi_get_tag_and_value(include_ctx_t *ctx, char **tag,
                                     char **tag_val, int dodecode)
{
    if (!ctx->intern->argv) {
        *tag = NULL;
        *tag_val = NULL;

        return;
    }

    *tag_val = ctx->intern->argv->value;
    *tag = ctx->intern->argv->name;

    ctx->intern->argv = ctx->intern->argv->next;

    if (dodecode && *tag_val) {
        decodehtml(*tag_val);
    }
}

static int find_file(request_rec *r, const char *directive, const char *tag,
                     char *tag_val, apr_finfo_t *finfo)
{
    char *to_send = tag_val;
    request_rec *rr = NULL;
    int ret=0;
    char *error_fmt = NULL;
    apr_status_t rv = APR_SUCCESS;

    if (!strcmp(tag, "file")) {
        char *newpath;

        /* be safe; only files in this directory or below allowed */
        rv = apr_filepath_merge(&newpath, NULL, tag_val,
                                APR_FILEPATH_SECUREROOTTEST |
                                APR_FILEPATH_NOTABSOLUTE, r->pool);

        if (rv != APR_SUCCESS) {
            error_fmt = APLOGNO(02668) "unable to access file \"%s\" "
                        "in parsed file %s";
        }
        else {
            /* note: it is okay to pass NULL for the "next filter" since
               we never attempt to "run" this sub request. */
            rr = ap_sub_req_lookup_file(newpath, r, NULL);

            if (rr->status == HTTP_OK && rr->finfo.filetype != APR_NOFILE) {
                to_send = rr->filename;
                if ((rv = apr_stat(finfo, to_send,
                    APR_FINFO_GPROT | APR_FINFO_MIN, rr->pool)) != APR_SUCCESS
                    && rv != APR_INCOMPLETE) {
                    error_fmt = APLOGNO(02669) "unable to get information "
                                "about \"%s\" in parsed file %s";
                }
            }
            else {
                error_fmt = APLOGNO(02670) "unable to lookup information "
                            "about \"%s\" in parsed file %s";
            }
        }

        if (error_fmt) {
            ret = -1;
            /* Intentional no APLOGNO */
            /* error_fmt provides APLOGNO */
            ap_log_rerror(APLOG_MARK, APLOG_ERR,
                          rv, r, error_fmt, to_send, r->filename);
        }

        if (rr) ap_destroy_sub_req(rr);

        return ret;
    }
    else if (!strcmp(tag, "virtual")) {
        /* note: it is okay to pass NULL for the "next filter" since
           we never attempt to "run" this sub request. */
        rr = ap_sub_req_lookup_uri(tag_val, r, NULL);

        if (rr->status == HTTP_OK && rr->finfo.filetype != APR_NOFILE) {
            memcpy((char *) finfo, (const char *) &rr->finfo,
                   sizeof(rr->finfo));
            ap_destroy_sub_req(rr);
            return 0;
        }
        else {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01339) "unable to get "
                          "information about \"%s\" in parsed file %s",
                          tag_val, r->filename);
            ap_destroy_sub_req(rr);
            return -1;
        }
    }
    else {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01340) "unknown parameter \"%s\" "
                      "to tag %s in %s", tag, directive, r->filename);
        return -1;
    }
}

/*
 * <!--#comment blah blah blah ... -->
 */
static apr_status_t handle_comment(include_ctx_t *ctx, ap_filter_t *f,
                                   apr_bucket_brigade *bb)
{
    return APR_SUCCESS;
}

/*
 * <!--#include virtual|file="..." [onerror|virtual|file="..."] ... -->
 *
 * Output each file/virtual in turn until one of them returns an error.
 * On error, ignore all further file/virtual attributes until we reach
 * an onerror attribute, where we make an attempt to serve the onerror
 * virtual url. If onerror fails, or no onerror is present, the default
 * error string is inserted into the stream.
 */
static apr_status_t handle_include(include_ctx_t *ctx, ap_filter_t *f,
                                   apr_bucket_brigade *bb)
{
    request_rec *r = f->r;
    char *last_error;

    if (!ctx->argc) {
        ap_log_rerror(APLOG_MARK,
                      (ctx->flags & SSI_FLAG_PRINTING)
                          ? APLOG_ERR : APLOG_WARNING,
                      0, r, APLOGNO(01341)
                      "missing argument for include element in %s",
                      r->filename);
    }

    if (!(ctx->flags & SSI_FLAG_PRINTING)) {
        return APR_SUCCESS;
    }

    if (!ctx->argc) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    last_error = NULL;
    while (1) {
        char *tag     = NULL;
        char *tag_val = NULL;
        request_rec *rr = NULL;
        char *error_fmt = NULL;
        char *parsed_string;
        apr_status_t rv = APR_SUCCESS;
        int status = 0;

        ap_ssi_get_tag_and_value(ctx, &tag, &tag_val, SSI_VALUE_DECODED);
        if (!tag || !tag_val) {
            break;
        }

        if (strcmp(tag, "virtual") && strcmp(tag, "file") && strcmp(tag,
                "onerror")) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01342) "unknown parameter "
                          "\"%s\" to tag include in %s", tag, r->filename);
            SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
            break;
        }

        parsed_string = ap_ssi_parse_string(ctx, tag_val, NULL, 0,
                                            SSI_EXPAND_DROP_NAME);
        if (tag[0] == 'f') {
            char *newpath;

            /* be safe; only files in this directory or below allowed */
            rv = apr_filepath_merge(&newpath, NULL, parsed_string,
                                    APR_FILEPATH_SECUREROOTTEST |
                                    APR_FILEPATH_NOTABSOLUTE, ctx->dpool);

            if (rv != APR_SUCCESS) {
                error_fmt = "unable to include file \"%s\" in parsed file %s";
            }
            else {
                rr = ap_sub_req_lookup_file(newpath, r, f->next);
            }
        }
        else if ((tag[0] == 'v' && !last_error)
                || (tag[0] == 'o' && last_error)) {
            if (r->kept_body) {
                rr = ap_sub_req_method_uri(r->method, parsed_string, r, f->next);
            }
            else {
                rr = ap_sub_req_lookup_uri(parsed_string, r, f->next);
            }
        }
        else {
            continue;
        }

        if (!error_fmt && rr->status != HTTP_OK) {
            error_fmt = "unable to include \"%s\" in parsed file %s, subrequest setup returned %d";
        }

        if (!error_fmt && (ctx->flags & SSI_FLAG_NO_EXEC) &&
            rr->content_type && strncmp(rr->content_type, "text/", 5)) {

            error_fmt = "unable to include potential exec \"%s\" in parsed "
                        "file %s, content type not text/*";
        }

        /* See the Kludge in includes_filter for why.
         * Basically, it puts a bread crumb in here, then looks
         * for the crumb later to see if its been here.
         */
        if (rr) {
            ap_set_module_config(rr->request_config, &include_module, r);
        }

        if (!error_fmt && ((status = ap_run_sub_req(rr)))) {
            error_fmt = "unable to include \"%s\" in parsed file %s, subrequest returned %d";
        }

        if (error_fmt) {
            /* Intentional no APLOGNO */
            /* error text is also sent to client */
            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, error_fmt, tag_val,
                    r->filename, status ? status : rr ? rr->status : 0);
            if (last_error) {
                /* onerror threw an error, give up completely */
                break;
            }
            last_error = error_fmt;
        }
        else {
            last_error = NULL;
        }

        /* Do *not* destroy the subrequest here; it may have allocated
         * variables in this r->subprocess_env in the subrequest's
         * r->pool, so that pool must survive as long as this request.
         * Yes, this is a memory leak. */

    }

    if (last_error) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
    }

    return APR_SUCCESS;
}

/*
 * <!--#echo [decoding="..."] [encoding="..."] var="..." [decoding="..."]
 *  [encoding="..."] var="..." ... -->
 */
static apr_status_t handle_echo(include_ctx_t *ctx, ap_filter_t *f,
                                apr_bucket_brigade *bb)
{
    const char *encoding = "entity", *decoding = "none";
    request_rec *r = f->r;
    int error = 0;

    if (!ctx->argc) {
        ap_log_rerror(APLOG_MARK,
                      (ctx->flags & SSI_FLAG_PRINTING)
                          ? APLOG_ERR : APLOG_WARNING,
                      0, r, APLOGNO(01343)
                      "missing argument for echo element in %s",
                      r->filename);
    }

    if (!(ctx->flags & SSI_FLAG_PRINTING)) {
        return APR_SUCCESS;
    }

    if (!ctx->argc) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    while (1) {
        char *tag = NULL;
        char *tag_val = NULL;

        ap_ssi_get_tag_and_value(ctx, &tag, &tag_val, SSI_VALUE_DECODED);
        if (!tag || !tag_val) {
            break;
        }

        if (!strcmp(tag, "var")) {
            const char *val;
            const char *echo_text = NULL;
            apr_size_t e_len;

            val = get_include_var(ap_ssi_parse_string(ctx, tag_val, NULL,
                                                      0, SSI_EXPAND_DROP_NAME),
                                  ctx);

            if (val) {
                char *last = NULL;
                char *e, *d, *token;

                echo_text = val;

                d = apr_pstrdup(ctx->pool, decoding);
                token = apr_strtok(d, ", \t", &last);

                while (token) {
                    if (!strcasecmp(token, "none")) {
                        /* do nothing */
                    }
                    else if (!strcasecmp(token, "url")) {
                        char *buf = apr_pstrdup(ctx->pool, echo_text);
                        ap_unescape_url(buf);
                        echo_text = buf;
                    }
                    else if (!strcasecmp(token, "urlencoded")) {
                        char *buf = apr_pstrdup(ctx->pool, echo_text);
                        ap_unescape_urlencoded(buf);
                        echo_text = buf;
                    }
                    else if (!strcasecmp(token, "entity")) {
                        char *buf = apr_pstrdup(ctx->pool, echo_text);
                        decodehtml(buf);
                        echo_text = buf;
                    }
                    else if (!strcasecmp(token, "base64")) {
                        echo_text = ap_pbase64decode(ctx->dpool, echo_text);
                    }
                    else {
                        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01344) "unknown value "
                                      "\"%s\" to parameter \"decoding\" of tag echo in "
                                      "%s", token, r->filename);
                        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
                        error = 1;
                        break;
                    }
                    token = apr_strtok(NULL, ", \t", &last);
                }

                e = apr_pstrdup(ctx->pool, encoding);
                token = apr_strtok(e, ", \t", &last);

                while (token) {
                    if (!strcasecmp(token, "none")) {
                        /* do nothing */
                    }
                    else if (!strcasecmp(token, "url")) {
                        echo_text = ap_escape_uri(ctx->dpool, echo_text);
                    }
                    else if (!strcasecmp(token, "urlencoded")) {
                        echo_text = ap_escape_urlencoded(ctx->dpool, echo_text);
                    }
                    else if (!strcasecmp(token, "entity")) {
                        echo_text = ap_escape_html2(ctx->dpool, echo_text, 0);
                    }
                    else if (!strcasecmp(token, "base64")) {
                        char *buf;
                        buf = ap_pbase64encode(ctx->dpool, (char *)echo_text);
                        echo_text = buf;
                    }
                    else {
                        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01345) "unknown value "
                                      "\"%s\" to parameter \"encoding\" of tag echo in "
                                      "%s", token, r->filename);
                        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
                        error = 1;
                        break;
                    }
                    token = apr_strtok(NULL, ", \t", &last);
                }

                e_len = strlen(echo_text);
            }
            else {
                echo_text = ctx->intern->undefined_echo;
                e_len = ctx->intern->undefined_echo_len;
            }

            if (error) {
                break;
            }

            APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_pool_create(
                                    apr_pmemdup(ctx->pool, echo_text, e_len),
                                    e_len, ctx->pool, f->c->bucket_alloc));
        }
        else if (!strcmp(tag, "decoding")) {
            decoding = tag_val;
        }
        else if (!strcmp(tag, "encoding")) {
            encoding = tag_val;
        }
        else {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01346) "unknown parameter "
                          "\"%s\" in tag echo of %s", tag, r->filename);
            SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
            break;
        }
    }

    return APR_SUCCESS;
}

/*
 * <!--#config [timefmt="..."] [sizefmt="..."] [errmsg="..."]
 *             [echomsg="..."] -->
 */
static apr_status_t handle_config(include_ctx_t *ctx, ap_filter_t *f,
                                  apr_bucket_brigade *bb)
{
    request_rec *r = f->r;
    apr_table_t *env = r->subprocess_env;

    if (!ctx->argc) {
        ap_log_rerror(APLOG_MARK,
                      (ctx->flags & SSI_FLAG_PRINTING)
                          ? APLOG_ERR : APLOG_WARNING,
                      0, r, APLOGNO(01347)
                      "missing argument for config element in %s",
                      r->filename);
    }

    if (!(ctx->flags & SSI_FLAG_PRINTING)) {
        return APR_SUCCESS;
    }

    if (!ctx->argc) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    while (1) {
        char *tag     = NULL;
        char *tag_val = NULL;

        ap_ssi_get_tag_and_value(ctx, &tag, &tag_val, SSI_VALUE_RAW);
        if (!tag || !tag_val) {
            break;
        }

        if (!strcmp(tag, "errmsg")) {
            ctx->error_str = ap_ssi_parse_string(ctx, tag_val, NULL, 0,
                                                 SSI_EXPAND_DROP_NAME);
        }
        else if (!strcmp(tag, "echomsg")) {
            ctx->intern->undefined_echo =
                ap_ssi_parse_string(ctx, tag_val, NULL, 0,SSI_EXPAND_DROP_NAME);
            ctx->intern->undefined_echo_len=strlen(ctx->intern->undefined_echo);
        }
        else if (!strcmp(tag, "timefmt")) {
            apr_time_t date = r->request_time;

            ctx->time_str = ap_ssi_parse_string(ctx, tag_val, NULL, 0,
                                                SSI_EXPAND_DROP_NAME);

            apr_table_setn(env, "DATE_LOCAL", ap_ht_time(r->pool, date,
                           ctx->time_str, 0));
            apr_table_setn(env, "DATE_GMT", ap_ht_time(r->pool, date,
                           ctx->time_str, 1));
            apr_table_setn(env, "LAST_MODIFIED",
                           ap_ht_time(r->pool, r->finfo.mtime,
                           ctx->time_str, 0));
        }
        else if (!strcmp(tag, "sizefmt")) {
            char *parsed_string;

            parsed_string = ap_ssi_parse_string(ctx, tag_val, NULL, 0,
                                                SSI_EXPAND_DROP_NAME);
            if (!strcmp(parsed_string, "bytes")) {
                ctx->flags |= SSI_FLAG_SIZE_IN_BYTES;
            }
            else if (!strcmp(parsed_string, "abbrev")) {
                ctx->flags &= SSI_FLAG_SIZE_ABBREV;
            }
            else {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01348) "unknown value "
                              "\"%s\" to parameter \"sizefmt\" of tag config "
                              "in %s", parsed_string, r->filename);
                SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
                break;
            }
        }
        else {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01349) "unknown parameter "
                          "\"%s\" to tag config in %s", tag, r->filename);
            SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
            break;
        }
    }

    return APR_SUCCESS;
}

/*
 * <!--#fsize virtual|file="..." [virtual|file="..."] ... -->
 */
static apr_status_t handle_fsize(include_ctx_t *ctx, ap_filter_t *f,
                                 apr_bucket_brigade *bb)
{
    request_rec *r = f->r;

    if (!ctx->argc) {
        ap_log_rerror(APLOG_MARK,
                      (ctx->flags & SSI_FLAG_PRINTING)
                          ? APLOG_ERR : APLOG_WARNING,
                      0, r, APLOGNO(01350)
                      "missing argument for fsize element in %s",
                      r->filename);
    }

    if (!(ctx->flags & SSI_FLAG_PRINTING)) {
        return APR_SUCCESS;
    }

    if (!ctx->argc) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    while (1) {
        char *tag     = NULL;
        char *tag_val = NULL;
        apr_finfo_t finfo;
        char *parsed_string;

        ap_ssi_get_tag_and_value(ctx, &tag, &tag_val, SSI_VALUE_DECODED);
        if (!tag || !tag_val) {
            break;
        }

        parsed_string = ap_ssi_parse_string(ctx, tag_val, NULL, 0,
                                            SSI_EXPAND_DROP_NAME);

        if (!find_file(r, "fsize", tag, parsed_string, &finfo)) {
            char *buf;
            apr_size_t len;

            if (!(ctx->flags & SSI_FLAG_SIZE_IN_BYTES)) {
                buf = apr_strfsize(finfo.size, apr_palloc(ctx->pool, 5));
                len = 4; /* omit the \0 terminator */
            }
            else {
                apr_size_t l, x, pos;
                char *tmp;

                tmp = apr_psprintf(ctx->dpool, "%" APR_OFF_T_FMT, finfo.size);
                len = l = strlen(tmp);

                for (x = 0; x < l; ++x) {
                    if (x && !((l - x) % 3)) {
                        ++len;
                    }
                }

                if (len == l) {
                    buf = apr_pstrmemdup(ctx->pool, tmp, len);
                }
                else {
                    buf = apr_palloc(ctx->pool, len);

                    for (pos = x = 0; x < l; ++x) {
                        if (x && !((l - x) % 3)) {
                            buf[pos++] = ',';
                        }
                        buf[pos++] = tmp[x];
                    }
                }
            }

            APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_pool_create(buf, len,
                                    ctx->pool, f->c->bucket_alloc));
        }
        else {
            SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
            break;
        }
    }

    return APR_SUCCESS;
}

/*
 * <!--#flastmod virtual|file="..." [virtual|file="..."] ... -->
 */
static apr_status_t handle_flastmod(include_ctx_t *ctx, ap_filter_t *f,
                                    apr_bucket_brigade *bb)
{
    request_rec *r = f->r;

    if (!ctx->argc) {
        ap_log_rerror(APLOG_MARK,
                      (ctx->flags & SSI_FLAG_PRINTING)
                          ? APLOG_ERR : APLOG_WARNING,
                      0, r, APLOGNO(01351)
                      "missing argument for flastmod element in %s",
                      r->filename);
    }

    if (!(ctx->flags & SSI_FLAG_PRINTING)) {
        return APR_SUCCESS;
    }

    if (!ctx->argc) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    while (1) {
        char *tag     = NULL;
        char *tag_val = NULL;
        apr_finfo_t  finfo;
        char *parsed_string;

        ap_ssi_get_tag_and_value(ctx, &tag, &tag_val, SSI_VALUE_DECODED);
        if (!tag || !tag_val) {
            break;
        }

        parsed_string = ap_ssi_parse_string(ctx, tag_val, NULL, 0,
                                            SSI_EXPAND_DROP_NAME);

        if (!find_file(r, "flastmod", tag, parsed_string, &finfo)) {
            char *t_val;
            apr_size_t t_len;

            t_val = ap_ht_time(ctx->pool, finfo.mtime, ctx->time_str, 0);
            t_len = strlen(t_val);

            APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_pool_create(t_val, t_len,
                                    ctx->pool, f->c->bucket_alloc));
        }
        else {
            SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
            break;
        }
    }

    return APR_SUCCESS;
}

/*
 * <!--#if expr="..." -->
 */
static apr_status_t handle_if(include_ctx_t *ctx, ap_filter_t *f,
                              apr_bucket_brigade *bb)
{
    char *tag = NULL;
    char *expr = NULL;
    request_rec *r = f->r;
    int expr_ret, was_error;

    if (ctx->argc != 1) {
        ap_log_rerror(APLOG_MARK,
                      (ctx->flags & SSI_FLAG_PRINTING)
                          ? APLOG_ERR : APLOG_WARNING,
                      0, r,
                      (ctx->argc)
                      ? APLOGNO(01352) "too many arguments for if element in %s"
                      : APLOGNO(01353) "missing expr argument for if element in %s",
                      r->filename);
    }

    if (!(ctx->flags & SSI_FLAG_PRINTING)) {
        ++(ctx->if_nesting_level);
        return APR_SUCCESS;
    }

    if (ctx->argc != 1) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    ap_ssi_get_tag_and_value(ctx, &tag, &expr, SSI_VALUE_RAW);

    if (strcmp(tag, "expr")) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01354) "unknown parameter \"%s\" "
                      "to tag if in %s", tag, r->filename);
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    if (!expr) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01355) "missing expr value for if "
                      "element in %s", r->filename);
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    DEBUG_PRINTF((ctx, "****    if expr=\"%s\"\n", expr));

    if (ctx->intern->legacy_expr)
        expr_ret = parse_expr(ctx, expr, &was_error);
    else
        expr_ret = parse_ap_expr(ctx, expr, &was_error);

    if (was_error) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    if (expr_ret) {
        ctx->flags |= (SSI_FLAG_PRINTING | SSI_FLAG_COND_TRUE);
    }
    else {
        ctx->flags &= SSI_FLAG_CLEAR_PRINT_COND;
    }

    DEBUG_DUMP_COND(ctx, "   if");

    ctx->if_nesting_level = 0;

    return APR_SUCCESS;
}

/*
 * <!--#elif expr="..." -->
 */
static apr_status_t handle_elif(include_ctx_t *ctx, ap_filter_t *f,
                                apr_bucket_brigade *bb)
{
    char *tag = NULL;
    char *expr = NULL;
    request_rec *r = f->r;
    int expr_ret, was_error;

    if (ctx->argc != 1) {
        ap_log_rerror(APLOG_MARK,
                      (!(ctx->if_nesting_level)) ? APLOG_ERR : APLOG_WARNING,
                      0, r,
                      (ctx->argc)
                      ? APLOGNO(01356) "too many arguments for if element in %s"
                      : APLOGNO(01357) "missing expr argument for if element in %s",
                      r->filename);
    }

    if (ctx->if_nesting_level) {
        return APR_SUCCESS;
    }

    if (ctx->argc != 1) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    ap_ssi_get_tag_and_value(ctx, &tag, &expr, SSI_VALUE_RAW);

    if (strcmp(tag, "expr")) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01358) "unknown parameter \"%s\" "
                      "to tag if in %s", tag, r->filename);
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    if (!expr) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01359) "missing expr in elif "
                      "statement: %s", r->filename);
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    DEBUG_PRINTF((ctx, "****  elif expr=\"%s\"\n", expr));
    DEBUG_DUMP_COND(ctx, " elif");

    if (ctx->flags & SSI_FLAG_COND_TRUE) {
        ctx->flags &= SSI_FLAG_CLEAR_PRINTING;
        return APR_SUCCESS;
    }

    if (ctx->intern->legacy_expr)
        expr_ret = parse_expr(ctx, expr, &was_error);
    else
        expr_ret = parse_ap_expr(ctx, expr, &was_error);

    if (was_error) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    if (expr_ret) {
        ctx->flags |= (SSI_FLAG_PRINTING | SSI_FLAG_COND_TRUE);
    }
    else {
        ctx->flags &= SSI_FLAG_CLEAR_PRINT_COND;
    }

    DEBUG_DUMP_COND(ctx, " elif");

    return APR_SUCCESS;
}

/*
 * <!--#else -->
 */
static apr_status_t handle_else(include_ctx_t *ctx, ap_filter_t *f,
                                apr_bucket_brigade *bb)
{
    request_rec *r = f->r;

    if (ctx->argc) {
        ap_log_rerror(APLOG_MARK,
                      (!(ctx->if_nesting_level)) ? APLOG_ERR : APLOG_WARNING,
                      0, r, APLOGNO(01360)
                      "else directive does not take tags in %s",
                      r->filename);
    }

    if (ctx->if_nesting_level) {
        return APR_SUCCESS;
    }

    if (ctx->argc) {
        if (ctx->flags & SSI_FLAG_PRINTING) {
            SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        }

        return APR_SUCCESS;
    }

    DEBUG_DUMP_COND(ctx, " else");

    if (ctx->flags & SSI_FLAG_COND_TRUE) {
        ctx->flags &= SSI_FLAG_CLEAR_PRINTING;
    }
    else {
        ctx->flags |= (SSI_FLAG_PRINTING | SSI_FLAG_COND_TRUE);
    }

    return APR_SUCCESS;
}

/*
 * <!--#endif -->
 */
static apr_status_t handle_endif(include_ctx_t *ctx, ap_filter_t *f,
                                 apr_bucket_brigade *bb)
{
    request_rec *r = f->r;

    if (ctx->argc) {
        ap_log_rerror(APLOG_MARK,
                      (!(ctx->if_nesting_level)) ? APLOG_ERR : APLOG_WARNING,
                      0, r, APLOGNO(01361)
                      "endif directive does not take tags in %s",
                      r->filename);
    }

    if (ctx->if_nesting_level) {
        --(ctx->if_nesting_level);
        return APR_SUCCESS;
    }

    if (ctx->argc) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    DEBUG_DUMP_COND(ctx, "endif");

    ctx->flags |= (SSI_FLAG_PRINTING | SSI_FLAG_COND_TRUE);

    return APR_SUCCESS;
}

/*
 * <!--#set var="..." value="..." ... -->
 */
static apr_status_t handle_set(include_ctx_t *ctx, ap_filter_t *f,
                               apr_bucket_brigade *bb)
{
    const char *encoding = "none", *decoding = "none";
    char *var = NULL;
    request_rec *r = f->r;
    request_rec *sub = r->main;
    apr_pool_t *p = r->pool;
    int error = 0;

    if (ctx->argc < 2) {
        ap_log_rerror(APLOG_MARK,
                      (ctx->flags & SSI_FLAG_PRINTING)
                          ? APLOG_ERR : APLOG_WARNING,
                      0, r,
                      APLOGNO(01362) "missing argument for set element in %s",
                      r->filename);
    }

    if (!(ctx->flags & SSI_FLAG_PRINTING)) {
        return APR_SUCCESS;
    }

    if (ctx->argc < 2) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    /* we need to use the 'main' request pool to set notes as that is
     * a notes lifetime
     */
    while (sub) {
        p = sub->pool;
        sub = sub->main;
    }

    while (1) {
        char *tag = NULL;
        char *tag_val = NULL;

        ap_ssi_get_tag_and_value(ctx, &tag, &tag_val, SSI_VALUE_RAW);

        if (!tag || !tag_val) {
            break;
        }

        if (!strcmp(tag, "var")) {
            decodehtml(tag_val);
            var = ap_ssi_parse_string(ctx, tag_val, NULL, 0,
                                      SSI_EXPAND_DROP_NAME);
        }
        else if (!strcmp(tag, "decoding")) {
            decoding = tag_val;
        }
        else if (!strcmp(tag, "encoding")) {
            encoding = tag_val;
        }
        else if (!strcmp(tag, "value")) {
            char *parsed_string;

            if (!var) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01363) "variable must "
                              "precede value in set directive in %s",
                              r->filename);
                SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
                break;
            }

            parsed_string = ap_ssi_parse_string(ctx, tag_val, NULL, 0,
                                                SSI_EXPAND_DROP_NAME);

            if (parsed_string) {
                char *last = NULL;
                char *e, *d, *token;

                d = apr_pstrdup(ctx->pool, decoding);
                token = apr_strtok(d, ", \t", &last);

                while (token) {
                    if (!strcasecmp(token, "none")) {
                        /* do nothing */
                    }
                    else if (!strcasecmp(token, "url")) {
                        char *buf = apr_pstrdup(ctx->pool, parsed_string);
                        ap_unescape_url(buf);
                        parsed_string = buf;
                    }
                    else if (!strcasecmp(token, "urlencoded")) {
                        char *buf = apr_pstrdup(ctx->pool, parsed_string);
                        ap_unescape_urlencoded(buf);
                        parsed_string = buf;
                    }
                    else if (!strcasecmp(token, "entity")) {
                        char *buf = apr_pstrdup(ctx->pool, parsed_string);
                        decodehtml(buf);
                        parsed_string = buf;
                    }
                    else if (!strcasecmp(token, "base64")) {
                        parsed_string = ap_pbase64decode(ctx->dpool, parsed_string);
                    }
                    else {
                        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01364) "unknown value "
                                      "\"%s\" to parameter \"decoding\" of tag set in "
                                      "%s", token, r->filename);
                        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
                        error = 1;
                        break;
                    }
                    token = apr_strtok(NULL, ", \t", &last);
                }

                e = apr_pstrdup(ctx->pool, encoding);
                token = apr_strtok(e, ", \t", &last);

                while (token) {
                    if (!strcasecmp(token, "none")) {
                        /* do nothing */
                    }
                    else if (!strcasecmp(token, "url")) {
                        parsed_string = ap_escape_uri(ctx->dpool, parsed_string);
                    }
                    else if (!strcasecmp(token, "urlencoded")) {
                        parsed_string = ap_escape_urlencoded(ctx->dpool, parsed_string);
                    }
                    else if (!strcasecmp(token, "entity")) {
                        parsed_string = ap_escape_html2(ctx->dpool, parsed_string, 0);
                    }
                    else if (!strcasecmp(token, "base64")) {
                        char *buf;
                        buf = ap_pbase64encode(ctx->dpool, (char *)parsed_string);
                        parsed_string = buf;
                    }
                    else {
                        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01365) "unknown value "
                                      "\"%s\" to parameter \"encoding\" of tag set in "
                                      "%s", token, r->filename);
                        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
                        error = 1;
                        break;
                    }
                    token = apr_strtok(NULL, ", \t", &last);
                }

            }

            if (error) {
                break;
            }

            apr_table_setn(r->subprocess_env, apr_pstrdup(p, var),
                           apr_pstrdup(p, parsed_string));
        }
        else {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01366) "Invalid tag for set "
                          "directive in %s", r->filename);
            SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
            break;
        }
    }

    return APR_SUCCESS;
}

/*
 * <!--#printenv -->
 */
static apr_status_t handle_printenv(include_ctx_t *ctx, ap_filter_t *f,
                                    apr_bucket_brigade *bb)
{
    request_rec *r = f->r;
    const apr_array_header_t *arr;
    const apr_table_entry_t *elts;
    int i;

    if (ctx->argc) {
        ap_log_rerror(APLOG_MARK,
                      (ctx->flags & SSI_FLAG_PRINTING)
                          ? APLOG_ERR : APLOG_WARNING,
                      0, r,
                      APLOGNO(01367) "printenv directive does not take tags in %s",
                      r->filename);
    }

    if (!(ctx->flags & SSI_FLAG_PRINTING)) {
        return APR_SUCCESS;
    }

    if (ctx->argc) {
        SSI_CREATE_ERROR_BUCKET(ctx, f, bb);
        return APR_SUCCESS;
    }

    arr = apr_table_elts(r->subprocess_env);
    elts = (apr_table_entry_t *)arr->elts;

    for (i = 0; i < arr->nelts; ++i) {
        const char *key_text, *val_text;

        /* get key */
        key_text = ap_escape_html(ctx->dpool, elts[i].key);

        /* get value */
        val_text = elts[i].val;
        if (val_text == LAZY_VALUE)
            val_text = add_include_vars_lazy(r, elts[i].key, ctx->time_str);
        val_text = ap_escape_html(ctx->dpool, val_text);

        apr_brigade_putstrs(bb, NULL, NULL, key_text, "=", val_text, "\n",
                            NULL);
    }

    ctx->flush_now = 1;
    return APR_SUCCESS;
}


/*
 * +-------------------------------------------------------+
 * |                                                       |
 * |               Main Includes-Filter Engine
 * |                                                       |
 * +-------------------------------------------------------+
 */

/* This is an implementation of the BNDM search algorithm.
 *
 * Fast and Flexible String Matching by Combining Bit-parallelism and
 * Suffix Automata (2001)
 * Gonzalo Navarro, Mathieu Raffinot
 *
 * http://www-igm.univ-mlv.fr/~raffinot/ftp/jea2001.ps.gz
 *
 * Initial code submitted by Sascha Schumann.
 */

/* Precompile the bndm_t data structure. */
static bndm_t *bndm_compile(apr_pool_t *pool, const char *n, apr_size_t nl)
{
    unsigned int x;
    const char *ne = n + nl;
    bndm_t *t = apr_palloc(pool, sizeof(*t));

    memset(t->T, 0, sizeof(unsigned int) * 256);
    t->pattern_len = nl;

    for (x = 1; n < ne; x <<= 1) {
        t->T[(unsigned char) *n++] |= x;
    }

    t->x = x - 1;

    return t;
}

/* Implements the BNDM search algorithm (as described above).
 *
 * h  - the string to look in
 * hl - length of the string to look for
 * t  - precompiled bndm structure against the pattern
 *
 * Returns the count of character that is the first match or hl if no
 * match is found.
 */
static apr_size_t bndm(bndm_t *t, const char *h, apr_size_t hl)
{
    const char *skip;
    const char *he, *p, *pi;
    unsigned int *T, x, d;
    apr_size_t nl;

    he = h + hl;

    T = t->T;
    x = t->x;
    nl = t->pattern_len;

    pi = h - 1; /* pi: p initial */
    p = pi + nl; /* compare window right to left. point to the first char */

    while (p < he) {
        skip = p;
        d = x;
        do {
            d &= T[(unsigned char) *p--];
            if (!d) {
                break;
            }
            if ((d & 1)) {
                if (p != pi) {
                    skip = p;
                }
                else {
                    return p - h + 1;
                }
            }
            d >>= 1;
        } while (d);

        pi = skip;
        p = pi + nl;
    }

    return hl;
}

/*
 * returns the index position of the first byte of start_seq (or the len of
 * the buffer as non-match)
 */
static apr_size_t find_start_sequence(include_ctx_t *ctx, const char *data,
                                      apr_size_t len)
{
    struct ssi_internal_ctx *intern = ctx->intern;
    apr_size_t slen = intern->start_seq_pat->pattern_len;
    apr_size_t index;
    const char *p, *ep;

    if (len < slen) {
        p = data; /* try partial match at the end of the buffer (below) */
    }
    else {
        /* try fast bndm search over the buffer
         * (hopefully the whole start sequence can be found in this buffer)
         */
        index = bndm(intern->start_seq_pat, data, len);

        /* wow, found it. ready. */
        if (index < len) {
            intern->state = PARSE_DIRECTIVE;
            return index;
        }
        else {
            /* ok, the pattern can't be found as whole in the buffer,
             * check the end for a partial match
             */
            p = data + len - slen + 1;
        }
    }

    ep = data + len;
    do {
        while (p < ep && *p != *intern->start_seq) {
            ++p;
        }

        index = p - data;

        /* found a possible start_seq start */
        if (p < ep) {
            apr_size_t pos = 1;

            ++p;
            while (p < ep && *p == intern->start_seq[pos]) {
                ++p;
                ++pos;
            }

            /* partial match found. Store the info for the next round */
            if (p == ep) {
                intern->state = PARSE_HEAD;
                intern->parse_pos = pos;
                return index;
            }
        }

        /* we must try all combinations; consider (e.g.) SSIStartTag "--->"
         * and a string data of "--.-" and the end of the buffer
         */
        p = data + index + 1;
    } while (p < ep);

    /* no match */
    return len;
}

/*
 * returns the first byte *after* the partial (or final) match.
 *
 * If we had to trick with the start_seq start, 'release' returns the
 * number of chars of the start_seq which appeared not to be part of a
 * full tag and may have to be passed down the filter chain.
 */
static apr_size_t find_partial_start_sequence(include_ctx_t *ctx,
                                              const char *data,
                                              apr_size_t len,
                                              apr_size_t *release)
{
    struct ssi_internal_ctx *intern = ctx->intern;
    apr_size_t pos, spos = 0;
    apr_size_t slen = intern->start_seq_pat->pattern_len;
    const char *p, *ep;

    pos = intern->parse_pos;
    ep = data + len;
    *release = 0;

    do {
        p = data;

        while (p < ep && pos < slen && *p == intern->start_seq[pos]) {
            ++p;
            ++pos;
        }

        /* full match */
        if (pos == slen) {
            intern->state = PARSE_DIRECTIVE;
            return (p - data);
        }

        /* the whole buffer is a partial match */
        if (p == ep) {
            intern->parse_pos = pos;
            return (p - data);
        }

        /* No match so far, but again:
         * We must try all combinations, since the start_seq is a random
         * user supplied string
         *
         * So: look if the first char of start_seq appears somewhere within
         * the current partial match. If it does, try to start a match that
         * begins with this offset. (This can happen, if a strange
         * start_seq like "---->" spans buffers)
         */
        if (spos < intern->parse_pos) {
            do {
                ++spos;
                ++*release;
                p = intern->start_seq + spos;
                pos = intern->parse_pos - spos;

                while (pos && *p != *intern->start_seq) {
                    ++p;
                    ++spos;
                    ++*release;
                    --pos;
                }

                /* if a matching beginning char was found, try to match the
                 * remainder of the old buffer.
                 */
                if (pos > 1) {
                    apr_size_t t = 1;

                    ++p;
                    while (t < pos && *p == intern->start_seq[t]) {
                        ++p;
                        ++t;
                    }

                    if (t == pos) {
                        /* yeah, another partial match found in the *old*
                         * buffer, now test the *current* buffer for
                         * continuing match
                         */
                        break;
                    }
                }
            } while (pos > 1);

            if (pos) {
                continue;
            }
        }

        break;
    } while (1); /* work hard to find a match ;-) */

    /* no match at all, release all (wrongly) matched chars so far */
    *release = intern->parse_pos;
    intern->state = PARSE_PRE_HEAD;
    return 0;
}

/*
 * returns the position after the directive
 */
static apr_size_t find_directive(include_ctx_t *ctx, const char *data,
                                 apr_size_t len, char ***store,
                                 apr_size_t **store_len)
{
    struct ssi_internal_ctx *intern = ctx->intern;
    const char *p = data;
    const char *ep = data + len;
    apr_size_t pos;

    switch (intern->state) {
    case PARSE_DIRECTIVE:
        while (p < ep && !apr_isspace(*p)) {
            /* we have to consider the case of missing space between directive
             * and end_seq (be somewhat lenient), e.g. <!--#printenv-->
             */
            if (*p == *intern->end_seq) {
                intern->state = PARSE_DIRECTIVE_TAIL;
                intern->parse_pos = 1;
                ++p;
                return (p - data);
            }
            ++p;
        }

        if (p < ep) { /* found delimiter whitespace */
            intern->state = PARSE_DIRECTIVE_POSTNAME;
            *store = &intern->directive;
            *store_len = &intern->directive_len;
        }

        break;

    case PARSE_DIRECTIVE_TAIL:
        pos = intern->parse_pos;

        while (p < ep && pos < intern->end_seq_len &&
               *p == intern->end_seq[pos]) {
            ++p;
            ++pos;
        }

        /* full match, we're done */
        if (pos == intern->end_seq_len) {
            intern->state = PARSE_DIRECTIVE_POSTTAIL;
            *store = &intern->directive;
            *store_len = &intern->directive_len;
            break;
        }

        /* partial match, the buffer is too small to match fully */
        if (p == ep) {
            intern->parse_pos = pos;
            break;
        }

        /* no match. continue normal parsing */
        intern->state = PARSE_DIRECTIVE;
        return 0;

    case PARSE_DIRECTIVE_POSTTAIL:
        intern->state = PARSE_EXECUTE;
        intern->directive_len -= intern->end_seq_len;
        /* continue immediately with the next state */

    case PARSE_DIRECTIVE_POSTNAME:
        if (PARSE_DIRECTIVE_POSTNAME == intern->state) {
            intern->state = PARSE_PRE_ARG;
        }
        ctx->argc = 0;
        intern->argv = NULL;

        if (!intern->directive_len) {
            intern->error = 1;
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, ctx->r, APLOGNO(01368) "missing "
                          "directive name in parsed document %s",
                          ctx->r->filename);
        }
        else {
            char *sp = intern->directive;
            char *sep = intern->directive + intern->directive_len;

            /* normalize directive name */
            for (; sp < sep; ++sp) {
                *sp = apr_tolower(*sp);
            }
        }

        return 0;

    default:
        /* get a rid of a gcc warning about unhandled enumerations */
        break;
    }

    return (p - data);
}

/*
 * find out whether the next token is (a possible) end_seq or an argument
 */
static apr_size_t find_arg_or_tail(include_ctx_t *ctx, const char *data,
                                   apr_size_t len)
{
    struct ssi_internal_ctx *intern = ctx->intern;
    const char *p = data;
    const char *ep = data + len;

    /* skip leading WS */
    while (p < ep && apr_isspace(*p)) {
        ++p;
    }

    /* buffer doesn't consist of whitespaces only */
    if (p < ep) {
        intern->state = (*p == *intern->end_seq) ? PARSE_TAIL : PARSE_ARG;
    }

    return (p - data);
}

/*
 * test the stream for end_seq. If it doesn't match at all, it must be an
 * argument
 */
static apr_size_t find_tail(include_ctx_t *ctx, const char *data,
                            apr_size_t len)
{
    struct ssi_internal_ctx *intern = ctx->intern;
    const char *p = data;
    const char *ep = data + len;
    apr_size_t pos = intern->parse_pos;

    if (PARSE_TAIL == intern->state) {
        intern->state = PARSE_TAIL_SEQ;
        pos = intern->parse_pos = 0;
    }

    while (p < ep && pos < intern->end_seq_len && *p == intern->end_seq[pos]) {
        ++p;
        ++pos;
    }

    /* bingo, full match */
    if (pos == intern->end_seq_len) {
        intern->state = PARSE_EXECUTE;
        return (p - data);
    }

    /* partial match, the buffer is too small to match fully */
    if (p == ep) {
        intern->parse_pos = pos;
        return (p - data);
    }

    /* no match. It must be an argument string then
     * The caller should cleanup and rewind to the reparse point
     */
    intern->state = PARSE_ARG;
    return 0;
}

/*
 * extract name=value from the buffer
 * A pcre-pattern could look (similar to):
 * name\s*(?:=\s*(["'`]?)value\1(?>\s*))?
 */
static apr_size_t find_argument(include_ctx_t *ctx, const char *data,
                                apr_size_t len, char ***store,
                                apr_size_t **store_len)
{
    struct ssi_internal_ctx *intern = ctx->intern;
    const char *p = data;
    const char *ep = data + len;

    switch (intern->state) {
    case PARSE_ARG:
        /*
         * create argument structure and append it to the current list
         */
        intern->current_arg = apr_palloc(ctx->dpool,
                                         sizeof(*intern->current_arg));
        intern->current_arg->next = NULL;

        ++(ctx->argc);
        if (!intern->argv) {
            intern->argv = intern->current_arg;
        }
        else {
            arg_item_t *newarg = intern->argv;

            while (newarg->next) {
                newarg = newarg->next;
            }
            newarg->next = intern->current_arg;
        }

        /* check whether it's a valid one. If it begins with a quote, we
         * can safely assume, someone forgot the name of the argument
         */
        switch (*p) {
        case '"': case '\'': case '`':
            *store = NULL;

            intern->state = PARSE_ARG_VAL;
            intern->quote = *p++;
            intern->current_arg->name = NULL;
            intern->current_arg->name_len = 0;
            intern->error = 1;

            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, ctx->r, APLOGNO(01369) "missing "
                          "argument name for value to tag %s in %s",
                          apr_pstrmemdup(ctx->r->pool, intern->directive,
                                         intern->directive_len),
                                         ctx->r->filename);

            return (p - data);

        default:
            intern->state = PARSE_ARG_NAME;
        }
        /* continue immediately with next state */

    case PARSE_ARG_NAME:
        while (p < ep && !apr_isspace(*p) && *p != '=') {
            ++p;
        }

        if (p < ep) {
            intern->state = PARSE_ARG_POSTNAME;
            *store = &intern->current_arg->name;
            *store_len = &intern->current_arg->name_len;
            return (p - data);
        }
        break;

    case PARSE_ARG_POSTNAME:
        intern->current_arg->name = apr_pstrmemdup(ctx->dpool,
                                                 intern->current_arg->name,
                                                 intern->current_arg->name_len);
        if (!intern->current_arg->name_len) {
            intern->error = 1;
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, ctx->r, APLOGNO(01370) "missing "
                          "argument name for value to tag %s in %s",
                          apr_pstrmemdup(ctx->r->pool, intern->directive,
                                         intern->directive_len),
                                         ctx->r->filename);
        }
        else {
            ap_str_tolower(intern->current_arg->name);
        }

        intern->state = PARSE_ARG_EQ;
        /* continue with next state immediately */

    case PARSE_ARG_EQ:
        *store = NULL;

        while (p < ep && apr_isspace(*p)) {
            ++p;
        }

        if (p < ep) {
            if (*p == '=') {
                intern->state = PARSE_ARG_PREVAL;
                ++p;
            }
            else { /* no value */
                intern->current_arg->value = NULL;
                intern->state = PARSE_PRE_ARG;
            }

            return (p - data);
        }
        break;

    case PARSE_ARG_PREVAL:
        *store = NULL;

        while (p < ep && apr_isspace(*p)) {
            ++p;
        }

        /* buffer doesn't consist of whitespaces only */
        if (p < ep) {
            intern->state = PARSE_ARG_VAL;
            switch (*p) {
            case '"': case '\'': case '`':
                intern->quote = *p++;
                break;
            default:
                intern->quote = '\0';
                break;
            }

            return (p - data);
        }
        break;

    case PARSE_ARG_VAL_ESC:
        if (*p == intern->quote) {
            ++p;
        }
        intern->state = PARSE_ARG_VAL;
        /* continue with next state immediately */

    case PARSE_ARG_VAL:
        for (; p < ep; ++p) {
            if (intern->quote && *p == '\\') {
                ++p;
                if (p == ep) {
                    intern->state = PARSE_ARG_VAL_ESC;
                    break;
                }

                if (*p != intern->quote) {
                    --p;
                }
            }
            else if (intern->quote && *p == intern->quote) {
                ++p;
                *store = &intern->current_arg->value;
                *store_len = &intern->current_arg->value_len;
                intern->state = PARSE_ARG_POSTVAL;
                break;
            }
            else if (!intern->quote && apr_isspace(*p)) {
                ++p;
                *store = &intern->current_arg->value;
                *store_len = &intern->current_arg->value_len;
                intern->state = PARSE_ARG_POSTVAL;
                break;
            }
        }

        return (p - data);

    case PARSE_ARG_POSTVAL:
        /*
         * The value is still the raw input string. Finally clean it up.
         */
        --(intern->current_arg->value_len);

        /* strip quote escaping \ from the string */
        if (intern->quote) {
            apr_size_t shift = 0;
            char *sp;

            sp = intern->current_arg->value;
            ep = intern->current_arg->value + intern->current_arg->value_len;
            while (sp < ep && *sp != '\\') {
                ++sp;
            }
            for (; sp < ep; ++sp) {
                if (*sp == '\\' && sp[1] == intern->quote) {
                    ++sp;
                    ++shift;
                }
                if (shift) {
                    *(sp-shift) = *sp;
                }
            }

            intern->current_arg->value_len -= shift;
        }

        intern->current_arg->value[intern->current_arg->value_len] = '\0';
        intern->state = PARSE_PRE_ARG;

        return 0;

    default:
        /* get a rid of a gcc warning about unhandled enumerations */
        break;
    }

    return len; /* partial match of something */
}

/*
 * This is the main loop over the current bucket brigade.
 */
static apr_status_t send_parsed_content(ap_filter_t *f, apr_bucket_brigade *bb)
{
    include_ctx_t *ctx = f->ctx;
    struct ssi_internal_ctx *intern = ctx->intern;
    request_rec *r = f->r;
    apr_bucket *b = APR_BRIGADE_FIRST(bb);
    apr_bucket_brigade *pass_bb;
    apr_status_t rv = APR_SUCCESS;
    char *magic; /* magic pointer for sentinel use */

    /* fast exit */
    if (APR_BRIGADE_EMPTY(bb)) {
        return APR_SUCCESS;
    }

    /* we may crash, since already cleaned up; hand over the responsibility
     * to the next filter;-)
     */
    if (intern->seen_eos) {
        return ap_pass_brigade(f->next, bb);
    }

    /* All stuff passed along has to be put into that brigade */
    pass_bb = apr_brigade_create(ctx->pool, f->c->bucket_alloc);

    /* initialization for this loop */
    intern->bytes_read = 0;
    intern->error = 0;
    ctx->flush_now = 0;

    /* loop over the current bucket brigade */
    while (b != APR_BRIGADE_SENTINEL(bb)) {
        const char *data = NULL;
        apr_size_t len, index, release;
        apr_bucket *newb = NULL;
        char **store = &magic;
        apr_size_t *store_len = NULL;

        /* handle meta buckets before reading any data */
        if (APR_BUCKET_IS_METADATA(b)) {
            newb = APR_BUCKET_NEXT(b);

            APR_BUCKET_REMOVE(b);

            if (APR_BUCKET_IS_EOS(b)) {
                intern->seen_eos = 1;

                /* Hit end of stream, time for cleanup ... But wait!
                 * Perhaps we're not ready yet. We may have to loop one or
                 * two times again to finish our work. In that case, we
                 * just re-insert the EOS bucket to allow for an extra loop.
                 *
                 * PARSE_EXECUTE means, we've hit a directive just before the
                 *    EOS, which is now waiting for execution.
                 *
                 * PARSE_DIRECTIVE_POSTTAIL means, we've hit a directive with
                 *    no argument and no space between directive and end_seq
                 *    just before the EOS. (consider <!--#printenv--> as last
                 *    or only string within the stream). This state, however,
                 *    just cleans up and turns itself to PARSE_EXECUTE, which
                 *    will be passed through within the next (and actually
                 *    last) round.
                 */
                if (PARSE_EXECUTE            == intern->state ||
                    PARSE_DIRECTIVE_POSTTAIL == intern->state) {
                    APR_BUCKET_INSERT_BEFORE(newb, b);
                }
                else {
                    break; /* END OF STREAM */
                }
            }
            else {
                APR_BRIGADE_INSERT_TAIL(pass_bb, b);

                if (APR_BUCKET_IS_FLUSH(b)) {
                    ctx->flush_now = 1;
                }

                b = newb;
                continue;
            }
        }

        /* enough is enough ... */
        if (ctx->flush_now ||
            intern->bytes_read > AP_MIN_BYTES_TO_WRITE) {

            if (!APR_BRIGADE_EMPTY(pass_bb)) {
                rv = ap_pass_brigade(f->next, pass_bb);
                if (rv != APR_SUCCESS) {
                    apr_brigade_destroy(pass_bb);
                    return rv;
                }
            }

            ctx->flush_now = 0;
            intern->bytes_read = 0;
        }

        /* read the current bucket data */
        len = 0;
        if (!intern->seen_eos) {
            if (intern->bytes_read > 0) {
                rv = apr_bucket_read(b, &data, &len, APR_NONBLOCK_READ);
                if (APR_STATUS_IS_EAGAIN(rv)) {
                    ctx->flush_now = 1;
                    continue;
                }
            }

            if (!len || rv != APR_SUCCESS) {
                rv = apr_bucket_read(b, &data, &len, APR_BLOCK_READ);
            }

            if (rv != APR_SUCCESS) {
                apr_brigade_destroy(pass_bb);
                return rv;
            }

            intern->bytes_read += len;
        }

        /* zero length bucket, fetch next one */
        if (!len && !intern->seen_eos) {
            b = APR_BUCKET_NEXT(b);
            continue;
        }

        /*
         * it's actually a data containing bucket, start/continue parsing
         */

        switch (intern->state) {
        /* no current tag; search for start sequence */
        case PARSE_PRE_HEAD:
            index = find_start_sequence(ctx, data, len);

            if (index < len) {
                apr_bucket_split(b, index);
            }

            newb = APR_BUCKET_NEXT(b);
            if (ctx->flags & SSI_FLAG_PRINTING) {
                APR_BUCKET_REMOVE(b);
                APR_BRIGADE_INSERT_TAIL(pass_bb, b);
            }
            else {
                apr_bucket_delete(b);
            }

            if (index < len) {
                /* now delete the start_seq stuff from the remaining bucket */
                if (PARSE_DIRECTIVE == intern->state) { /* full match */
                    apr_bucket_split(newb, intern->start_seq_pat->pattern_len);
                    ctx->flush_now = 1; /* pass pre-tag stuff */
                }

                b = APR_BUCKET_NEXT(newb);
                apr_bucket_delete(newb);
            }
            else {
                b = newb;
            }

            break;

        /* we're currently looking for the end of the start sequence */
        case PARSE_HEAD:
            index = find_partial_start_sequence(ctx, data, len, &release);

            /* check if we mismatched earlier and have to release some chars */
            if (release && (ctx->flags & SSI_FLAG_PRINTING)) {
                char *to_release = apr_pmemdup(ctx->pool, intern->start_seq, release);

                newb = apr_bucket_pool_create(to_release, release, ctx->pool,
                                              f->c->bucket_alloc);
                APR_BRIGADE_INSERT_TAIL(pass_bb, newb);
            }

            if (index) { /* any match */
                /* now delete the start_seq stuff from the remaining bucket */
                if (PARSE_DIRECTIVE == intern->state) { /* final match */
                    apr_bucket_split(b, index);
                    ctx->flush_now = 1; /* pass pre-tag stuff */
                }
                newb = APR_BUCKET_NEXT(b);
                apr_bucket_delete(b);
                b = newb;
            }

            break;

        /* we're currently grabbing the directive name */
        case PARSE_DIRECTIVE:
        case PARSE_DIRECTIVE_POSTNAME:
        case PARSE_DIRECTIVE_TAIL:
        case PARSE_DIRECTIVE_POSTTAIL:
            index = find_directive(ctx, data, len, &store, &store_len);

            if (index) {
                apr_bucket_split(b, index);
                newb = APR_BUCKET_NEXT(b);
            }

            if (store) {
                if (index) {
                    APR_BUCKET_REMOVE(b);
                    apr_bucket_setaside(b, r->pool);
                    APR_BRIGADE_INSERT_TAIL(intern->tmp_bb, b);
                    b = newb;
                }

                /* time for cleanup? */
                if (store != &magic) {
                    apr_brigade_pflatten(intern->tmp_bb, store, store_len,
                                         ctx->dpool);
                    apr_brigade_cleanup(intern->tmp_bb);
                }
            }
            else if (index) {
                apr_bucket_delete(b);
                b = newb;
            }

            break;

        /* skip WS and find out what comes next (arg or end_seq) */
        case PARSE_PRE_ARG:
            index = find_arg_or_tail(ctx, data, len);

            if (index) { /* skipped whitespaces */
                if (index < len) {
                    apr_bucket_split(b, index);
                }
                newb = APR_BUCKET_NEXT(b);
                apr_bucket_delete(b);
                b = newb;
            }

            break;

        /* currently parsing name[=val] */
        case PARSE_ARG:
        case PARSE_ARG_NAME:
        case PARSE_ARG_POSTNAME:
        case PARSE_ARG_EQ:
        case PARSE_ARG_PREVAL:
        case PARSE_ARG_VAL:
        case PARSE_ARG_VAL_ESC:
        case PARSE_ARG_POSTVAL:
            index = find_argument(ctx, data, len, &store, &store_len);

            if (index) {
                apr_bucket_split(b, index);
                newb = APR_BUCKET_NEXT(b);
            }

            if (store) {
                if (index) {
                    APR_BUCKET_REMOVE(b);
                    apr_bucket_setaside(b, r->pool);
                    APR_BRIGADE_INSERT_TAIL(intern->tmp_bb, b);
                    b = newb;
                }

                /* time for cleanup? */
                if (store != &magic) {
                    apr_brigade_pflatten(intern->tmp_bb, store, store_len,
                                         ctx->dpool);
                    apr_brigade_cleanup(intern->tmp_bb);
                }
            }
            else if (index) {
                apr_bucket_delete(b);
                b = newb;
            }

            break;

        /* try to match end_seq at current pos. */
        case PARSE_TAIL:
        case PARSE_TAIL_SEQ:
            index = find_tail(ctx, data, len);

            switch (intern->state) {
            case PARSE_EXECUTE:  /* full match */
                apr_bucket_split(b, index);
                newb = APR_BUCKET_NEXT(b);
                apr_bucket_delete(b);
                b = newb;
                break;

            case PARSE_ARG:      /* no match */
                /* PARSE_ARG must reparse at the beginning */
                APR_BRIGADE_PREPEND(bb, intern->tmp_bb);
                b = APR_BRIGADE_FIRST(bb);
                break;

            default:             /* partial match */
                newb = APR_BUCKET_NEXT(b);
                APR_BUCKET_REMOVE(b);
                apr_bucket_setaside(b, r->pool);
                APR_BRIGADE_INSERT_TAIL(intern->tmp_bb, b);
                b = newb;
                break;
            }

            break;

        /* now execute the parsed directive, cleanup the space and
         * start again with PARSE_PRE_HEAD
         */
        case PARSE_EXECUTE:
            /* if there was an error, it was already logged; just stop here */
            if (intern->error) {
                if (ctx->flags & SSI_FLAG_PRINTING) {
                    SSI_CREATE_ERROR_BUCKET(ctx, f, pass_bb);
                    intern->error = 0;
                }
            }
            else {
                include_handler_fn_t *handle_func;

                handle_func =
                    (include_handler_fn_t *)apr_hash_get(include_handlers, intern->directive,
                                                         intern->directive_len);

                if (handle_func) {
                    DEBUG_INIT(ctx, f, pass_bb);
                    rv = handle_func(ctx, f, pass_bb);
                    if (rv != APR_SUCCESS) {
                        apr_brigade_destroy(pass_bb);
                        return rv;
                    }
                }
                else {
                    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01371)
                                  "unknown directive \"%s\" in parsed doc %s",
                                  apr_pstrmemdup(r->pool, intern->directive,
                                                 intern->directive_len),
                                                 r->filename);
                    if (ctx->flags & SSI_FLAG_PRINTING) {
                        SSI_CREATE_ERROR_BUCKET(ctx, f, pass_bb);
                    }
                }
            }

            /* cleanup */
            apr_pool_clear(ctx->dpool);
            apr_brigade_cleanup(intern->tmp_bb);

            /* Oooof. Done here, start next round */
            intern->state = PARSE_PRE_HEAD;
            break;

        } /* switch(ctx->state) */

    } /* while (brigade) */

    /* End of stream. Final cleanup */
    if (intern->seen_eos) {
        if (PARSE_HEAD == intern->state) {
            if (ctx->flags & SSI_FLAG_PRINTING) {
                char *to_release = apr_pmemdup(ctx->pool, intern->start_seq,
                                                          intern->parse_pos);

                APR_BRIGADE_INSERT_TAIL(pass_bb,
                                        apr_bucket_pool_create(to_release,
                                        intern->parse_pos, ctx->pool,
                                        f->c->bucket_alloc));
            }
        }
        else if (PARSE_PRE_HEAD != intern->state) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01372)
                          "SSI directive was not properly finished at the end "
                          "of parsed document %s", r->filename);
            if (ctx->flags & SSI_FLAG_PRINTING) {
                SSI_CREATE_ERROR_BUCKET(ctx, f, pass_bb);
            }
        }

        if (!(ctx->flags & SSI_FLAG_PRINTING)) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01373)
                          "missing closing endif directive in parsed document"
                          " %s", r->filename);
        }

        /* cleanup our temporary memory */
        apr_brigade_destroy(intern->tmp_bb);
        apr_pool_destroy(ctx->dpool);

        /* don't forget to finally insert the EOS bucket */
        APR_BRIGADE_INSERT_TAIL(pass_bb, b);
    }

    /* if something's left over, pass it along */
    if (!APR_BRIGADE_EMPTY(pass_bb)) {
        rv = ap_pass_brigade(f->next, pass_bb);
    }
    else {
        rv = APR_SUCCESS;
        apr_brigade_destroy(pass_bb);
    }
    return rv;
}


/*
 * +-------------------------------------------------------+
 * |                                                       |
 * |                     Runtime Hooks
 * |                                                       |
 * +-------------------------------------------------------+
 */

static int includes_setup(ap_filter_t *f)
{
    include_dir_config *conf = ap_get_module_config(f->r->per_dir_config,
                                                    &include_module);

    /* When our xbithack value isn't set to full or our platform isn't
     * providing group-level protection bits or our group-level bits do not
     * have group-execite on, we will set the no_local_copy value to 1 so
     * that we will not send 304s.
     */
    if ((conf->xbithack != XBITHACK_FULL)
        || !(f->r->finfo.valid & APR_FINFO_GPROT)
        || !(f->r->finfo.protection & APR_GEXECUTE)) {
        f->r->no_local_copy = 1;
    }

    /* Don't allow ETag headers to be generated - see RFC2616 - 13.3.4.
     * We don't know if we are going to be including a file or executing
     * a program - in either case a strong ETag header will likely be invalid.
     */
    if (conf->etag <= 0) {
        apr_table_setn(f->r->notes, "no-etag", "");
    }

    return OK;
}

static apr_status_t includes_filter(ap_filter_t *f, apr_bucket_brigade *b)
{
    request_rec *r = f->r;
    request_rec *parent;
    include_dir_config *conf = ap_get_module_config(r->per_dir_config,
                                                    &include_module);

    include_server_config *sconf= ap_get_module_config(r->server->module_config,
                                                       &include_module);

    if (!(ap_allow_options(r) & OPT_INCLUDES)) {
        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01374)
                      "mod_include: Options +Includes (or IncludesNoExec) "
                      "wasn't set, INCLUDES filter removed: %s", r->uri);
        ap_remove_output_filter(f);
        return ap_pass_brigade(f->next, b);
    }

    if (!f->ctx) {
        struct ssi_internal_ctx *intern;
        include_ctx_t *ctx;

        /* create context for this filter */
        f->ctx = ctx = apr_palloc(r->pool, sizeof(*ctx));
        ctx->r = r;
        ctx->intern = intern = apr_palloc(r->pool, sizeof(*ctx->intern));
        ctx->pool = r->pool;
        apr_pool_create(&ctx->dpool, ctx->pool);

        /* runtime data */
        intern->tmp_bb = apr_brigade_create(ctx->pool, f->c->bucket_alloc);
        intern->seen_eos = 0;
        intern->state = PARSE_PRE_HEAD;
        ctx->flags = (SSI_FLAG_PRINTING | SSI_FLAG_COND_TRUE);
        if ((ap_allow_options(r) & OPT_INC_WITH_EXEC) == 0) {
            ctx->flags |= SSI_FLAG_NO_EXEC;
        }
        intern->legacy_expr = (conf->legacy_expr > 0);
        intern->expr_eval_ctx = NULL;
        intern->expr_err = NULL;
        intern->expr_vary_this = NULL;

        ctx->if_nesting_level = 0;
        intern->re = NULL;

        ctx->error_str = conf->default_error_msg ? conf->default_error_msg :
                         DEFAULT_ERROR_MSG;
        ctx->time_str = conf->default_time_fmt ? conf->default_time_fmt :
                        DEFAULT_TIME_FORMAT;
        intern->start_seq  = sconf->default_start_tag;
        intern->start_seq_pat = bndm_compile(ctx->pool, intern->start_seq,
                                             strlen(intern->start_seq));
        intern->end_seq = sconf->default_end_tag;
        intern->end_seq_len = strlen(intern->end_seq);
        intern->undefined_echo = conf->undefined_echo ? conf->undefined_echo :
                                 DEFAULT_UNDEFINED_ECHO;
        intern->undefined_echo_len = strlen(intern->undefined_echo);
    }

    if ((parent = ap_get_module_config(r->request_config, &include_module))) {
        /* Kludge --- for nested includes, we want to keep the subprocess
         * environment of the base document (for compatibility); that means
         * torquing our own last_modified date as well so that the
         * LAST_MODIFIED variable gets reset to the proper value if the
         * nested document resets <!--#config timefmt -->.
         */
        r->subprocess_env = r->main->subprocess_env;
        apr_pool_join(r->main->pool, r->pool);
        r->finfo.mtime = r->main->finfo.mtime;
    }
    else {
        /* we're not a nested include, so we create an initial
         * environment */
        ap_add_common_vars(r);
        ap_add_cgi_vars(r);
        add_include_vars(r);
    }
    /* Always unset the content-length.  There is no way to know if
     * the content will be modified at some point by send_parsed_content.
     * It is very possible for us to not find any content in the first
     * 9k of the file, but still have to modify the content of the file.
     * If we are going to pass the file through send_parsed_content, then
     * the content-length should just be unset.
     */
    apr_table_unset(f->r->headers_out, "Content-Length");

    /* Always unset the Last-Modified field - see RFC2616 - 13.3.4.
     * We don't know if we are going to be including a file or executing
     * a program which may change the Last-Modified header or make the
     * content completely dynamic.  Therefore, we can't support these
     * headers.
     *
     * Exception: XBitHack full means we *should* set the
     * Last-Modified field.
     *
     * SSILastModified on means we *should* set the Last-Modified field
     * if not present, or respect an existing value if present.
     */

    /* Must we respect the last modified header? By default, no */
    if (conf->lastmodified > 0) {

        /* update the last modified if we have a valid time, and only if
         * we don't already have a valid last modified.
         */
        if (r->finfo.valid & APR_FINFO_MTIME
                && !apr_table_get(f->r->headers_out, "Last-Modified")) {
            ap_update_mtime(r, r->finfo.mtime);
            ap_set_last_modified(r);
        }

    }

    /* Assure the platform supports Group protections */
    else if (((conf->xbithack == XBITHACK_FULL ||
               (conf->xbithack == XBITHACK_UNSET &&
                DEFAULT_XBITHACK == XBITHACK_FULL))
        && (r->finfo.valid & APR_FINFO_GPROT)
        && (r->finfo.protection & APR_GEXECUTE))) {
        ap_update_mtime(r, r->finfo.mtime);
        ap_set_last_modified(r);
    }
    else {
        apr_table_unset(f->r->headers_out, "Last-Modified");
    }

    /* add QUERY stuff to env cause it ain't yet */
    if (r->args) {
        char *arg_copy = apr_pstrdup(r->pool, r->args);

        apr_table_setn(r->subprocess_env, "QUERY_STRING", r->args);
        ap_unescape_url(arg_copy);
        apr_table_setn(r->subprocess_env, "QUERY_STRING_UNESCAPED",
                  ap_escape_shell_cmd(r->pool, arg_copy));
    }

    return send_parsed_content(f, b);
}

static int include_fixup(request_rec *r)
{
    if (r->handler && (strcmp(r->handler, "server-parsed") == 0))
    {
        if (!r->content_type || !*r->content_type) {
            ap_set_content_type(r, "text/html");
        }
        r->handler = "default-handler";
    }
    else
#if defined(OS2) || defined(WIN32) || defined(NETWARE)
    /* These OS's don't support xbithack. This is being worked on. */
    {
        return DECLINED;
    }
#else
    {
        include_dir_config *conf = ap_get_module_config(r->per_dir_config,
                                                        &include_module);

        if (conf->xbithack == XBITHACK_OFF ||
            (DEFAULT_XBITHACK == XBITHACK_OFF &&
             conf->xbithack == XBITHACK_UNSET))
        {
            return DECLINED;
        }

        if (!(r->finfo.protection & APR_UEXECUTE)) {
            return DECLINED;
        }

        if (!r->content_type || strncmp(r->content_type, "text/html", 9)) {
            return DECLINED;
        }
    }
#endif

    /* We always return declined, because the default handler actually
     * serves the file.  All we have to do is add the filter.
     */
    ap_add_output_filter("INCLUDES", NULL, r, r->connection);
    return DECLINED;
}


/*
 * +-------------------------------------------------------+
 * |                                                       |
 * |                Configuration Handling
 * |                                                       |
 * +-------------------------------------------------------+
 */

static void *create_includes_dir_config(apr_pool_t *p, char *dummy)
{
    include_dir_config *result = apr_pcalloc(p, sizeof(include_dir_config));

    result->xbithack          = XBITHACK_UNSET;
    result->lastmodified      = UNSET;
    result->etag              = UNSET;
    result->legacy_expr       = UNSET;

    return result;
}

#define MERGE(b,o,n,val,unset) n->val = o->val != unset  ? o->val : b->val
static void *merge_includes_dir_config(apr_pool_t *p, void *basev, void *overridesv)
{
    include_dir_config *base = (include_dir_config *)basev,
                       *over = (include_dir_config *)overridesv,
                       *new  = apr_palloc(p, sizeof(include_dir_config));
    MERGE(base, over, new, default_error_msg, NULL);
    MERGE(base, over, new, default_time_fmt,  NULL);
    MERGE(base, over, new, undefined_echo,    NULL);
    MERGE(base, over, new, xbithack,          XBITHACK_UNSET);
    MERGE(base, over, new, lastmodified,      UNSET);
    MERGE(base, over, new, etag,              UNSET);
    MERGE(base, over, new, legacy_expr,       UNSET);
    return new;
}

static void *create_includes_server_config(apr_pool_t *p, server_rec *server)
{
    include_server_config *result;

    result = apr_palloc(p, sizeof(include_server_config));
    result->default_end_tag    = DEFAULT_END_SEQUENCE;
    result->default_start_tag  = DEFAULT_START_SEQUENCE;

    return result;
}

static const char *set_xbithack(cmd_parms *cmd, void *mconfig, const char *arg)
{
    include_dir_config *conf = mconfig;

    if (!strcasecmp(arg, "off")) {
        conf->xbithack = XBITHACK_OFF;
    }
    else if (!strcasecmp(arg, "on")) {
        conf->xbithack = XBITHACK_ON;
    }
    else if (!strcasecmp(arg, "full")) {
        conf->xbithack = XBITHACK_FULL;
    }
    else {
        return "XBitHack must be set to Off, On, or Full";
    }

    return NULL;
}

static const char *set_default_start_tag(cmd_parms *cmd, void *mconfig,
                                         const char *tag)
{
    include_server_config *conf;
    const char *p = tag;

    /* be consistent. (See below in set_default_end_tag) */
    while (*p) {
        if (apr_isspace(*p)) {
            return "SSIStartTag may not contain any whitespaces";
        }
        ++p;
    }

    conf= ap_get_module_config(cmd->server->module_config , &include_module);
    conf->default_start_tag = tag;

    return NULL;
}

static const char *set_default_end_tag(cmd_parms *cmd, void *mconfig,
                                       const char *tag)
{
    include_server_config *conf;
    const char *p = tag;

    /* sanity check. The parser may fail otherwise */
    while (*p) {
        if (apr_isspace(*p)) {
            return "SSIEndTag may not contain any whitespaces";
        }
        ++p;
    }

    conf= ap_get_module_config(cmd->server->module_config , &include_module);
    conf->default_end_tag = tag;

    return NULL;
}

static const char *set_undefined_echo(cmd_parms *cmd, void *mconfig,
                                      const char *msg)
{
    include_dir_config *conf = mconfig;
    conf->undefined_echo = msg;

    return NULL;
}

static const char *set_default_error_msg(cmd_parms *cmd, void *mconfig,
                                         const char *msg)
{
    include_dir_config *conf = mconfig;
    conf->default_error_msg = msg;

    return NULL;
}

static const char *set_default_time_fmt(cmd_parms *cmd, void *mconfig,
                                        const char *fmt)
{
    include_dir_config *conf = mconfig;
    conf->default_time_fmt = fmt;

    return NULL;
}


/*
 * +-------------------------------------------------------+
 * |                                                       |
 * |        Module Initialization and Configuration
 * |                                                       |
 * +-------------------------------------------------------+
 */

static int include_post_config(apr_pool_t *p, apr_pool_t *plog,
                               apr_pool_t *ptemp, server_rec *s)
{
    include_handlers = apr_hash_make(p);

    ssi_pfn_register = APR_RETRIEVE_OPTIONAL_FN(ap_register_include_handler);

    if (ssi_pfn_register) {
        ssi_pfn_register("if", handle_if);
        ssi_pfn_register("set", handle_set);
        ssi_pfn_register("else", handle_else);
        ssi_pfn_register("elif", handle_elif);
        ssi_pfn_register("echo", handle_echo);
        ssi_pfn_register("endif", handle_endif);
        ssi_pfn_register("fsize", handle_fsize);
        ssi_pfn_register("config", handle_config);
        ssi_pfn_register("comment", handle_comment);
        ssi_pfn_register("include", handle_include);
        ssi_pfn_register("flastmod", handle_flastmod);
        ssi_pfn_register("printenv", handle_printenv);
    }

    return OK;
}

static const command_rec includes_cmds[] =
{
    AP_INIT_TAKE1("XBitHack", set_xbithack, NULL, OR_OPTIONS,
                  "Off, On, or Full"),
    AP_INIT_TAKE1("SSIErrorMsg", set_default_error_msg, NULL, OR_ALL,
                  "a string"),
    AP_INIT_TAKE1("SSITimeFormat", set_default_time_fmt, NULL, OR_ALL,
                  "a strftime(3) formatted string"),
    AP_INIT_TAKE1("SSIStartTag", set_default_start_tag, NULL, RSRC_CONF,
                  "SSI Start String Tag"),
    AP_INIT_TAKE1("SSIEndTag", set_default_end_tag, NULL, RSRC_CONF,
                  "SSI End String Tag"),
    AP_INIT_TAKE1("SSIUndefinedEcho", set_undefined_echo, NULL, OR_ALL,
                  "String to be displayed if an echoed variable is undefined"),
    AP_INIT_FLAG("SSILegacyExprParser", ap_set_flag_slot_char,
                  (void *)APR_OFFSETOF(include_dir_config, legacy_expr),
                  OR_LIMIT,
                  "Whether to use the legacy expression parser compatible "
                  "with <= 2.2.x. Limited to 'on' or 'off'"),
    AP_INIT_FLAG("SSILastModified", ap_set_flag_slot_char,
                  (void *)APR_OFFSETOF(include_dir_config, lastmodified),
                  OR_LIMIT, "Whether to set the last modified header or respect "
                  "an existing header. Limited to 'on' or 'off'"),
    AP_INIT_FLAG("SSIEtag", ap_set_flag_slot_char,
                  (void *)APR_OFFSETOF(include_dir_config, etag),
                  OR_LIMIT, "Whether to allow the generation of ETags within the server. "
                  "Existing ETags will be preserved. Limited to 'on' or 'off'"),
    {NULL}
};

static void ap_register_include_handler(char *tag, include_handler_fn_t *func)
{
    apr_hash_set(include_handlers, tag, strlen(tag), (const void *)func);
}

static void register_hooks(apr_pool_t *p)
{
    APR_REGISTER_OPTIONAL_FN(ap_ssi_get_tag_and_value);
    APR_REGISTER_OPTIONAL_FN(ap_ssi_parse_string);
    APR_REGISTER_OPTIONAL_FN(ap_register_include_handler);
    ap_hook_post_config(include_post_config, NULL, NULL, APR_HOOK_REALLY_FIRST);
    ap_hook_fixups(include_fixup, NULL, NULL, APR_HOOK_LAST);
    ap_register_output_filter("INCLUDES", includes_filter, includes_setup,
                              AP_FTYPE_RESOURCE);
}

AP_DECLARE_MODULE(include) =
{
    STANDARD20_MODULE_STUFF,
    create_includes_dir_config,   /* dir config creater */
    merge_includes_dir_config,    /* dir config merger */
    create_includes_server_config,/* server config */
    NULL,                         /* merge server config */
    includes_cmds,                /* command apr_table_t */
    register_hooks                /* register hooks */
};
