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


/*
 * mod_negotiation.c: keeps track of MIME types the client is willing to
 * accept, and contains code to handle type arbitration.
 *
 * rst
 */

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

/* Commands --- configuring document caching on a per (virtual?)
 * server basis...
 */

typedef struct {
    array_header *language_priority;
} neg_dir_config;

module negotiation_module;

void *create_neg_dir_config (pool *p, char *dummy)
{
    neg_dir_config *new =
      (neg_dir_config *) palloc (p, sizeof (neg_dir_config));

    new->language_priority = make_array (p, 4, sizeof (char *));
    return new;
}

void *merge_neg_dir_configs (pool *p, void *basev, void *addv)
{
    neg_dir_config *base = (neg_dir_config *)basev;
    neg_dir_config *add = (neg_dir_config *)addv;
    neg_dir_config *new =
      (neg_dir_config *) palloc (p, sizeof (neg_dir_config));

    /* give priority to the config in the subdirectory */
    new->language_priority = append_arrays (p, add->language_priority,
					    base->language_priority);
    return new;
}

char *set_language_priority (cmd_parms *cmd, void *n, char *lang)
{
    array_header *arr = ((neg_dir_config *) n)->language_priority;
    char **langp = (char **) push_array (arr);

    *langp = pstrdup (arr->pool, lang);
    return NULL;
}

char *cache_negotiated_docs (cmd_parms *cmd, void *dummy, char *dummy2)
{
    void *server_conf = cmd->server->module_config;
    
    set_module_config (server_conf, &negotiation_module, "Cache");
    return NULL;
}

int do_cache_negotiated_docs (server_rec *s)
{
    return (get_module_config (s->module_config, &negotiation_module) != NULL);
}

command_rec negotiation_cmds[] = {
{ "CacheNegotiatedDocs", cache_negotiated_docs, NULL, RSRC_CONF, RAW_ARGS,
    NULL },
{ "LanguagePriority", set_language_priority, NULL, OR_FILEINFO, ITERATE,
    NULL },
{ NULL }
};

/*
 * TO DO --- error code 406.  Unfortunately, the specification for
 *           a 406 reply in the current draft standard is unworkable;
 *           we return 404 for these pending a workable spec. 
 */

/* Record of available info on a media type specified by the client
 * (we also use 'em for encodings and languages)
 */

typedef struct accept_rec {
    char *type_name;
    float quality;
    float max_bytes;
    float level;
} accept_rec;

/* Record of available info on a particular variant
 *
 * Note that a few of these fields are updated by the actual negotiation
 * code.  These are:
 *
 * quality --- initialized to the value of qs, and subsequently jiggered
 *             to reflect the client's preferences.  In particular, it
 *             gets zeroed out if the variant has an unacceptable content
 *             encoding, or if it is in a language which the client
 *             doesn't accept and some other variant *is* in a language
 *             the client accepts.
 *
 * level_matched --- initialized to zero.  Set to the value of level
 *             if the client actually accepts this media type at that
 *             level (and *not* if it got in on a wildcard).  See level_cmp
 *             below.
 */

typedef struct var_rec {
    request_rec *sub_req;	/* May be NULL (is, for map files) */
    char *type_name;
    char *file_name;
    char *content_encoding;
    char *content_language;
    float level;		/* Auxiliary to content-type... */
    float qs;
    float bytes;
    int lang_index;
    int is_pseudo_html;		/* text/html, *or* the INCLUDES_MAGIC_TYPEs */

    /* Above are all written-once properties of the variant.  The
     * two fields below are changed during negotiation:
     */
    
    float quality;	
    float level_matched;
} var_rec;

/* Something to carry around the state of negotiation (and to keep
 * all of this thread-safe)...
 */

typedef struct {
    pool *pool;
    request_rec *r;
    char *dir_name;
    
    array_header *accepts;	/* accept_recs */
    array_header *accept_encodings;	/* accept_recs */
    array_header *accept_langs;	/* accept_recs */
    array_header *avail_vars;	/* available variants */
} negotiation_state;

/* A few functions to manipulate var_recs.
 * Cleaning out the fields...
 */

void clean_var_rec (var_rec *mime_info)
{
    mime_info->sub_req = NULL;
    mime_info->type_name = "";
    mime_info->file_name = "";
    mime_info->content_encoding = "";
    mime_info->content_language = "";

    mime_info->is_pseudo_html = 0.0;
    mime_info->level = 0.0;
    mime_info->level_matched = 0.0;
    mime_info->qs = 0.0;
    mime_info->quality = 0.0;
    mime_info->bytes = 0;
    mime_info->lang_index = -1;
}

/* Initializing the relevant fields of a variant record from the
 * accept_info read out of its content-type, one way or another.
 */

void set_mime_fields (var_rec *var, accept_rec *mime_info)
{
    var->type_name = mime_info->type_name;
    var->qs = mime_info->quality;
    var->quality = mime_info->quality; /* Initial quality is just qs */
    var->level = mime_info->level;

    var->is_pseudo_html = 
	(!strcmp (var->type_name, "text/html")
	 || !strcmp (var->type_name, INCLUDES_MAGIC_TYPE)
	 || !strcmp (var->type_name, INCLUDES_MAGIC_TYPE3));
}

/*****************************************************************
 *
 * Parsing (lists of) media types and their parameters, as seen in
 * HTTPD header lines and elsewhere.
 */

/* Retrieve a token, spacing over it and returning a pointer to
 * the first non-white byte afterwards.  Note that these tokens
 * are delimited by semis and commas; and can also be delimited
 * by whitespace at the caller's option.
 */

char *get_token (pool *p, char **accept_line, int accept_white)
{
    char *ptr = *accept_line;
    char *tok_start;
    char *token;
    int tok_len;
  
    /* Find first non-white byte */
    
    while (*ptr && isspace(*ptr))
      ++ptr;

    tok_start = ptr;
    
    /* find token end, skipping over quoted strings.
     * (comments are already gone).
     */
    
    while (*ptr && (accept_white || !isspace(*ptr))
	   && *ptr != ';' && *ptr != ',')
    {
	if (*ptr++ == '"')
	    while (*ptr)
	        if (*ptr++ == '"') break;
    }
	  
    tok_len = ptr - tok_start;
    token = palloc (p, tok_len + 1);
    strncpy (token, tok_start, tok_len);
    token[tok_len] = '\0';
    
    /* Advance accept_line pointer to the next non-white byte */

    while (*ptr && isspace(*ptr))
      ++ptr;

    *accept_line = ptr;
    return token;
}

/*
 * Get a single mime type entry --- one media type and parameters;
 * enter the values we recognize into the argument accept_rec
 */

char *get_entry (pool *p, accept_rec *result, char *accept_line)
{
    result->quality = 1.0;
    result->max_bytes = 0.0;
    result->level = 0.0;
    
    /* Note that this handles what I gather is the "old format",
     *
     *    Accept: text/html text/plain moo/zot
     *
     * without any compatibility kludges --- if the token after the
     * MIME type begins with a semicolon, we know we're looking at parms,
     * otherwise, we know we aren't.  (So why all the pissing and moaning
     * in the CERN server code?  I must be missing something).
     */
    
    result->type_name = get_token (p, &accept_line, 0);
    str_tolower (result->type_name); /* You want case-insensitive,
				      * you'll *get* case-insensitive.
				      */
    

    /* KLUDGE!!! Default HTML to level 2.0 unless the browser
     * *explicitly* says something else.
     */
	
    if (!strcmp (result->type_name, "text/html")
	&& result->level == 0.0)
	result->level = 2.0;
    else if (!strcmp (result->type_name, INCLUDES_MAGIC_TYPE))
	result->level = 2.0;
    else if (!strcmp (result->type_name, INCLUDES_MAGIC_TYPE3))
	result->level = 3.0;

    while (*accept_line == ';') {
	/* Parameters ... */

	char *parm;
	char *cp;
	    
	++accept_line;
	parm = get_token (p, &accept_line, 1);

	/* Look for 'var = value' --- and make sure the var is in lcase. */
	
	for (cp = parm; *cp && !isspace(*cp) && *cp != '='; ++cp)
	    *cp = tolower(*cp);

	if (!*cp) continue;	/* No '='; just ignore it. */
	    
	*cp++ = '\0';		/* Delimit var */
	while (*cp && (isspace(*cp) || *cp == '='))
	    ++cp;

	if (*cp == '"') ++cp;
	
	if (parm[0] == 'q'
	    && (parm[1] == '\0' || (parm[1] == 's' && parm[2] == '\0')))
	    result->quality = atof(cp);
	else if (parm[0] == 'm' && parm[1] == 'x' &&
		 parm[2] == 'b' && parm[3] == '\0')
	    result->max_bytes = atof(cp);
	else if (parm[0] == 'l' && !strcmp (&parm[1], "evel"))
	    result->level = atof(cp);
    }

    if (*accept_line == ',') ++accept_line;

    return accept_line;
}
		 

/*****************************************************************
 *
 * Dealing with header lines ...
 */

array_header *do_header_line (pool *p, char *accept_line)
{
    array_header *accept_recs = make_array (p, 40, sizeof (accept_rec));
  
    if (!accept_line) return accept_recs;
    
    while (*accept_line) {
        accept_rec *new = (accept_rec *)push_array (accept_recs);
	accept_line = get_entry (p, new, accept_line);
    }

    return accept_recs;
}

/*****************************************************************
 *
 * Handling header lines from clients...
 */

negotiation_state *parse_accept_headers (request_rec *r)
{
    negotiation_state *new =
        (negotiation_state *)palloc (r->pool, sizeof (negotiation_state));
    table *hdrs = r->headers_in;

    new->pool = r->pool;
    new->r = r;
    new->dir_name = make_dirstr(r->pool, r->filename, count_dirs(r->filename));
    
    new->accepts = do_header_line (r->pool, table_get (hdrs, "Accept"));
    new->accept_encodings =
      do_header_line (r->pool, table_get (hdrs, "Accept-encoding"));
    new->accept_langs =
      do_header_line (r->pool, table_get (hdrs, "Accept-language"));
    new->avail_vars = make_array (r->pool, 40, sizeof (var_rec));

    return new;
}

/* Sometimes clients will give us no Accept info at all; this routine sets
 * up the standard default for that case, and also arranges for us to be
 * willing to run a CGI script if we find one.  (In fact, we set up to
 * dramatically prefer CGI scripts in cases where that's appropriate,
 * e.g., POST).
 */

void maybe_add_default_encodings(negotiation_state *neg, int prefer_scripts)
{
    accept_rec *new_accept = (accept_rec *)push_array (neg->accepts); 
  
    new_accept->type_name = CGI_MAGIC_TYPE;
    new_accept->quality = prefer_scripts ? 1e-20 : 1e20;
    new_accept->level = 0.0;
    new_accept->max_bytes = 0.0;

    if (neg->accepts->nelts > 1) return;
    
    new_accept = (accept_rec *)push_array (neg->accepts); 
    
    new_accept->type_name = "*/*";
    new_accept->quality = 1.0;
    new_accept->level = 0.0;
    new_accept->max_bytes = 0.0;
}

/*****************************************************************
 *
 * Parsing type-map files, in Roy's meta/http format augmented with
 * #-comments.
 */

/* Reading RFC822-style header lines, ignoring #-comments and
 * handling continuations.
 */

enum header_state { header_eof, header_seen, header_sep };

enum header_state get_header_line (char *buffer, int len, FILE *map)
{
    char *buf_end = buffer + len;
    char *cp;
    int c;
    
    /* Get a noncommented line */
    
    do {
	if (fgets(buffer, MAX_STRING_LEN, map) == NULL)
	    return header_eof;
    } while (buffer[0] == '#');
    
    /* If blank, just return it --- this ends information on this variant */
    
    for (cp = buffer; *cp && isspace (*cp); ++cp)
      continue;

    if (*cp == '\0') return header_sep;

    /* If non-blank, go looking for header lines, but note that we still
     * have to treat comments specially...
     */

    cp += strlen(cp);
    
    while ((c = getc(map)) != EOF)
    {
	if (c == '#') {
	    /* Comment line */
	    while ((c = getc(map)) != EOF && c != '\n')
	       continue;
	} else if (isspace(c)) {
	    /* Leading whitespace.  POSSIBLE continuation line
	     * Also, possibly blank --- if so, we ungetc() the final newline
	     * so that we will pick up the blank line the next time 'round.
	     */
	    
	    while (c != EOF && c != '\n' && isspace(c))
	        c = getc(map);

	    ungetc (c, map);
	    
	    if (c == '\n') return header_seen; /* Blank line */

	    /* Continuation */

	    while (cp < buf_end - 2 && (c = getc(map)) != EOF && c != '\n')
	        *cp++ = c;

	    *cp++ = '\n';
	    *cp = '\0';
	} else {

	    /* Line beginning with something other than whitespace */
	    
	    ungetc (c, map);
	    return header_seen;
	}
    }

    return header_seen;
}

/* Stripping out RFC822 comments */

void strip_paren_comments (char *hdr)
{
    /* Hmmm... is this correct?  In Roy's latest draft, (comments) can nest! */
  
    while (*hdr) {
	if (*hdr == '"') {
	    while (*++hdr && *hdr != '"')
		continue;
	    ++hdr;
	}
	else if (*hdr == '(') {
	    while (*hdr && *hdr != ')')	*hdr++ = ' ';
	    
	    if (*hdr) *hdr++ = ' ';
	}
	else ++hdr;
    }
}

/* Getting to a header body from the header */

char *lcase_header_name_return_body (char *header, request_rec *r)
{
    char *cp = header;
    
    while (*cp && *cp != ':')
        *cp++ = tolower(*cp);
    
    if (!*cp) {
	log_reason ("Syntax error in type map --- no ':'", r->filename, r);
	return NULL;
    }

    do ++cp; while (*cp && isspace (*cp));

    if (!*cp) {
	log_reason ("Syntax error in type map --- no header body",
		    r->filename, r);
	return NULL;
    }

    return cp;
}

int read_type_map (negotiation_state *neg, char *map_name)
{
    request_rec *r = neg->r;
    FILE *map = pfopen (neg->pool, map_name, "r");

    char buffer[MAX_STRING_LEN];
    enum header_state hstate;
    struct var_rec mime_info;
    
    if (map == NULL) {
        log_reason("cannot access type map file", map_name, r);
	return FORBIDDEN;
    }

    clean_var_rec (&mime_info);
    
    do {
	hstate = get_header_line (buffer, MAX_STRING_LEN, map);
	
	if (hstate == header_seen) {
	    char *body = lcase_header_name_return_body (buffer, neg->r);
	    
	    if (body == NULL) return SERVER_ERROR;
	    
	    strip_paren_comments (body);
	    
	    if (!strncmp (buffer, "uri:", 4)) {
	        mime_info.file_name = get_token (neg->pool, &body, 0);
	    }
	    else if (!strncmp (buffer, "content-type:", 13)) {
		struct accept_rec accept_info;
		
		get_entry (neg->pool, &accept_info, body);
		set_mime_fields (&mime_info, &accept_info);
	    }
	    else if (!strncmp (buffer, "content-length:", 15)) {
		mime_info.bytes = atoi(body);
	    }
	    else if (!strncmp (buffer, "content-language:", 17)) {
		mime_info.content_language = get_token (neg->pool, &body, 0);
		str_tolower (mime_info.content_language);
	    }
	    else if (!strncmp (buffer, "content-encoding:", 17)) {
		mime_info.content_encoding = get_token (neg->pool, &body, 0);
		str_tolower (mime_info.content_encoding);
	    }
	} else {
	    if (mime_info.quality > 0) {
	        void *new_var = push_array (neg->avail_vars);
		memcpy (new_var, (void *)&mime_info, sizeof (var_rec));
	    }
	    
	    clean_var_rec(&mime_info);
	}
    } while (hstate != header_eof);
    
    pfclose (neg->pool, map);
    return OK;
}

/*****************************************************************
 *
 * Same, except we use a filtered directory listing as the map...
 */

int read_types_multi (negotiation_state *neg)
{
    request_rec *r = neg->r;
    
    char *filp;
    int prefix_len;
    DIR *dirp;
    struct DIR_TYPE *dir_entry;
    struct var_rec mime_info;
    struct accept_rec accept_info;
    void *new_var;

    clean_var_rec (&mime_info);

    if (!(filp = strrchr (r->filename, '/'))) return DECLINED; /* Weird... */

    ++filp;
    prefix_len = strlen (filp);

    dirp = opendir (neg->dir_name); /* Not pool protected; sigh... */

    if (dirp == NULL) {
        log_reason("cannot read directory for multi", neg->dir_name, r);
	return FORBIDDEN;
    }

    while ((dir_entry = readdir (dirp))) {
	
        request_rec *sub_req;
      
	/* Do we have a match? */
	
	if (strncmp (dir_entry->d_name, filp, prefix_len)) continue;
	if (dir_entry->d_name[prefix_len] != '.') continue;
	
	/* Yep.  See if it's something which we have access to, and 
	 * which has a known type and encoding (as opposed to something
	 * which we'll be slapping default_type on later).
	 */
	
	sub_req = sub_req_lookup_file (dir_entry->d_name, r);
	
	if (sub_req->status != 200 || !sub_req->content_type) continue;
	
	/* If it's a map file, we use that instead of the map
	 * we're building...
	 */

	if (!strcmp (sub_req->content_type, MAP_FILE_MAGIC_TYPE)) {
	    closedir(dirp);
	    
	    neg->avail_vars->nelts = 0;
	    return read_type_map (neg, sub_req->filename);
	}
	
	/* Have reasonable variant --- gather notes.
	 */
	
	mime_info.sub_req = sub_req;
	mime_info.file_name = pstrdup(neg->pool, dir_entry->d_name);
	mime_info.content_encoding = sub_req->content_encoding;
	mime_info.content_language = sub_req->content_language;
	
	get_entry (neg->pool, &accept_info, sub_req->content_type);
	set_mime_fields (&mime_info, &accept_info);
	
	new_var = push_array (neg->avail_vars);
	memcpy (new_var, (void *)&mime_info, sizeof (var_rec));
	    
	clean_var_rec(&mime_info);
    }

    closedir(dirp);
    return OK;
}


/*****************************************************************
 * And now for the code you've been waiting for... actually
 * finding a match to the client's requirements.
 */

/* Matching MIME types ... the star/star and foo/star commenting conventions
 * are implemented here.  (You know what I mean by star/star, but just
 * try mentioning those three characters in a C comment).  Using strcmp()
 * is legit, because everything has already been smashed to lowercase.
 *
 * Note also that if we get an exact match on the media type, we update
 * level_matched for use in level_cmp below...
 */

int mime_match (accept_rec *accept, var_rec *avail)
{
    char *accept_type = accept->type_name;
    char *avail_type = avail->type_name;
    int len = strlen(accept_type);
  
    if (accept_type[0] == '*')	/* Anything matches star/star */
	return 1; 
    else if (accept_type[len - 1] == '*')
	return !strncmp (accept_type, avail_type, len - 2);
    else if (!strcmp (accept_type, avail_type)
	     || (!strcmp (accept_type, "text/html")
		 && (!strcmp(avail_type, INCLUDES_MAGIC_TYPE)
		     || !strcmp(avail_type, INCLUDES_MAGIC_TYPE3)))) {
	if (accept->level >= avail->level) {
	    avail->level_matched = avail->level;
	    return 1;
	}
    }

    return OK;
}

/* This code implements a piece of the tie-breaking algorithm between
 * variants of equal quality.  This piece is the treatment of variants
 * of the same base media type, but different levels.  What we want to
 * return is the variant at the highest level that the client explicitly
 * claimed to accept.
 *
 * If all the variants available are at a higher level than that, or if
 * the client didn't say anything specific about this media type at all
 * and these variants just got in on a wildcard, we prefer the lowest
 * level, on grounds that that's the one that the client is least likely
 * to choke on.
 *
 * (This is all motivated by treatment of levels in HTML --- we only
 * want to give level 3 to browsers that explicitly ask for it; browsers
 * that don't, including HTTP/0.9 browsers that only get the implicit
 * "Accept: * / *" [space added to avoid confusing cpp --- no, that
 * syntax doesn't really work] should get HTML2 if available).
 *
 * (Note that this code only comes into play when we are choosing among
 * variants of equal quality, where the draft standard gives us a fair
 * bit of leeway about what to do.  It ain't specified by the standard;
 * rather, it is a choice made by this server about what to do in cases
 * where the standard does not specify a unique course of action).
 */

int level_cmp (var_rec *var1, var_rec *var2)
{
    /* Levels are only comparable between matching media types */

    if (var1->is_pseudo_html && !var2->is_pseudo_html)
	return 0;
    
    if (!var1->is_pseudo_html && strcmp (var1->type_name, var2->type_name))
	return 0;
    
    /* Take highest level that matched, if either did match. */
    
    if (var1->level_matched > var2->level_matched) return 1;
    if (var1->level_matched < var2->level_matched) return -1;

    /* Neither matched.  Take lowest level, if there's a difference. */

    if (var1->level < var2->level) return 1;
    if (var1->level > var2->level) return -1;

    /* Tied */

    return 0;
}

/* Finding languages.  Note that we only match the substring specified
 * by the Accept: line --- this is to allow "en" to match all subvariants
 * of English.
 *
 * Again, strcmp() is legit because we've ditched case already.
 */

int find_lang_index (array_header *accept_langs, char *lang)
{
    accept_rec *accs;
    int i;

    if (!lang)
	return -1;

    accs = (accept_rec *)accept_langs->elts;

    for (i = 0; i < accept_langs->nelts; ++i)
	if (!strncmp (lang, accs[i].type_name, strlen(accs[i].type_name)))
	    return i;
	    
    return -1;		
}

/* This function returns the priority of a given language
 * according to LanguagePriority.  It is used in case of a tie
 * between several languages.
 */

int find_default_index (neg_dir_config *conf, char *lang)
{
    array_header *arr;
    int nelts;
    char **elts;
    int i;

    if (!lang)
	return -1;

    arr = conf->language_priority;
    nelts = arr->nelts;
    elts = (char **) arr->elts;

    for (i = 0; i < nelts; ++i)
        if (!strcasecmp (elts[i], lang))
	    return i;

    return -1;
}

void find_lang_indexes (negotiation_state *neg)
{
    var_rec *var_recs = (var_rec*)neg->avail_vars->elts;
    int i;
    int found_any = 0;
    neg_dir_config *conf = NULL;
    int naccept = neg->accept_langs->nelts;

    if (naccept == 0)
	conf = (neg_dir_config *) get_module_config (neg->r->per_dir_config,
						     &negotiation_module);

    for (i = 0; i < neg->avail_vars->nelts; ++i)
	if (var_recs[i].quality > 0) {
	    int index;
	    if (naccept == 0)		/* Client doesn't care */
		index = find_default_index (conf,
					    var_recs[i].content_language);
	    else			/* Client has Accept-Language */
		index = find_lang_index (neg->accept_langs,
					 var_recs[i].content_language);

	    var_recs[i].lang_index = index;
	    if (index >= 0) found_any = 1;
	}

    /* If we have any variants in a language acceptable to the client,
     * blow away everything that isn't.
     */
    
    if (found_any)
	for (i = 0; i < neg->avail_vars->nelts; ++i) 
	    if (var_recs[i].lang_index < 0)
		var_recs[i].quality = 0;
}

/* Finding content encodings.  Note that we assume that the client
 * accepts the trivial encodings.  Strcmp() is legit because... aw, hell.
 */

int is_identity_encoding (char *enc)
{
    return (!enc || !enc[0] || !strcmp (enc, "7bit") || !strcmp (enc, "8bit")
	    || !strcmp (enc, "binary"));
}

int find_encoding (array_header *accept_encodings, char *enc)
{
    accept_rec *accs = (accept_rec *)accept_encodings->elts;
    int i;

    if (is_identity_encoding(enc)) return 1.0;

    for (i = 0; i < accept_encodings->nelts; ++i)
	if (!strcmp (enc, accs[i].type_name))
	    return 1;

    return 0;
}

void do_encodings (negotiation_state *neg)
{
    var_rec *var_recs = (var_rec*)neg->avail_vars->elts;
    int i;

    /* If no Accept-Encoding is present, everything is acceptable */

    if (!neg->accept_encodings->nelts)
	return;

    /* Lose any variant with an unacceptable content encoding */

    for (i = 0; i < neg->avail_vars->nelts; ++i)
	if (var_recs[i].quality > 0
	    && !find_encoding (neg->accept_encodings,
			       var_recs[i].content_encoding))
	    
	    var_recs[i].quality = 0;
}

/* Determining the content length --- if the map didn't tell us,
 * we have to do a stat() and remember for next time.
 *
 * Grump.  For shambhala, even the first stat here may well be
 * redundant (for multiviews) with a stat() done by the sub_req
 * machinery.  At some point, that ought to be fixed.
 */

int find_content_length(negotiation_state *neg, var_rec *variant)
{
    struct stat statb;

    if (variant->bytes == 0) {
        char *fullname = make_full_path (neg->pool, neg->dir_name,
					 variant->file_name);
	
	if (stat (fullname, &statb) >= 0) variant->bytes = statb.st_size;
    }

    return variant->bytes;
}

/* The main event. */

var_rec *best_match(negotiation_state *neg)
{
    int i, j;
    var_rec *best = NULL;
    float best_quality = 0.0;
    int levcmp;
    
    accept_rec *accept_recs = (accept_rec *)neg->accepts->elts;
    var_rec *avail_recs = (var_rec *)neg->avail_vars->elts;

    /* Nuke variants which are unsuitable due to a content encoding,
     * or possibly a language, which the client doesn't accept.
     * (If we haven't *got* a variant in a language the client accepts,
     * find_lang_indexes keeps 'em all, so we still wind up serving
     * something...).
     */
    
    do_encodings (neg);
    find_lang_indexes (neg);
    
    for (i = 0; i < neg->accepts->nelts; ++i) {

	accept_rec *type = &accept_recs[i];
	
	for (j = 0; j < neg->avail_vars->nelts; ++j) {
	    
	    var_rec *variant = &avail_recs[j];
	    float q = type->quality * variant->quality;
		
	    /* If we've already rejected this variant, don't waste time */
	    
	    if (q == 0.0) continue;	
	    
	    /* If media types don't match, forget it.
	     * (This includes the level check).
	     */
	    
	    if (!mime_match(type, variant)) continue;

	    /* Check maxbytes */
		
	    if (type->max_bytes > 0
		&& (find_content_length(neg, variant)
		    > type->max_bytes))
		continue;
		
	    /* If it lasted this far, consider it ---
	     * If better quality than our current best, take it.
	     * If equal quality, *maybe* take it.
	     *
	     * Note that the current http draft specifies no particular
	     * behavior for variants which tie in quality; the server
	     * can, at its option, return a 300 response listing all
	     * of them (and perhaps the others), or choose one of the
	     * tied variants by whatever means it likes.  This server
	     * breaks ties as follows, in order:
	     *
	     * By order of languages in Accept-language, to give the
	     * client a way to specify a language preference.  I'd prefer
	     * to give this precedence over media type, but the standard
	     * doesn't allow for that.
	     *
	     * By level preference, as defined by level_cmp above.
	     *
	     * By order of Accept: header matched, so that the order in
	     * which media types are named by the client functions as a
	     * preference order, if the client didn't give us explicit
	     * quality values.
	     *
	     * Finally, by content_length, so that among variants which
	     * have the same quality, language and content_type (including
	     * level) we ship the one that saps the least bandwidth.
	     */
		
	    if (q > best_quality
		|| (q == best_quality
		    && (variant->lang_index < best->lang_index
			|| (variant->lang_index == best->lang_index
			    && ((levcmp = level_cmp (variant, best)) == 1
				|| (levcmp == 0
				    && !strcmp (variant->type_name,
						best->type_name)
				    && (find_content_length(neg, variant)
					<
					find_content_length(neg, best))))))))
	    {
		best = variant;
		best_quality = q;
	    }
	}
    }

    return best;
}

/****************************************************************
 *
 * Executive...
 */

int handle_map_file (request_rec *r)
{
    negotiation_state *neg = parse_accept_headers (r);
    var_rec *best;
    int res;
    
    char *udir;
    
    if ((res = read_type_map (neg, r->filename))) return res;
    
    maybe_add_default_encodings(neg, 0);
    
    if (!(best = best_match(neg))) {
      /* Should be a 406 */
      log_reason ("no acceptable variant", r->filename, r);
      return NOT_FOUND;
    }

    if (!do_cache_negotiated_docs(r->server)) r->no_cache = 1;
    udir = make_dirstr (r->pool, r->uri, count_dirs (r->uri));
    udir = escape_uri(r->pool, udir);
    internal_redirect (make_full_path (r->pool, udir, best->file_name), r);
    return OK;
}

int handle_multi (request_rec *r)
{
    negotiation_state *neg;
    var_rec *best;
    request_rec *sub_req;
    int res;
    
    if (r->finfo.st_mode != 0 || !(allow_options (r) & OPT_MULTI))
        return DECLINED;
    
    neg = parse_accept_headers (r);
    
    if ((res = read_types_multi (neg))) return res;
    
    maybe_add_default_encodings(neg,
				r->method_number != M_GET
				  || r->args || r->path_info);
    
    if (neg->avail_vars->nelts == 0) return DECLINED;
    
    if (!(best = best_match(neg))) {
      /* Should be a 406 */
      log_reason ("no acceptable variant", r->filename, r);
      return NOT_FOUND;
    }

    if (! (sub_req = best->sub_req)) {
        /* We got this out of a map file, so we don't actually have
	 * a sub_req structure yet.  Get one now.
	 */
      
        sub_req = sub_req_lookup_file (best->file_name, r);
	if (sub_req->status != 200) return sub_req->status;
    }
      
    /* BLETCH --- don't multi-resolve non-ordinary files */

    if (!S_ISREG(sub_req->finfo.st_mode)) return NOT_FOUND;
    
    /* Otherwise, use it. */
    
    if (!do_cache_negotiated_docs(r->server)) r->no_cache = 1;
    r->filename = sub_req->filename;
    r->content_type = sub_req->content_type;
    r->content_encoding = sub_req->content_encoding;
    r->content_language = sub_req->content_language;
    r->finfo = sub_req->finfo;
    
    return OK;
}

handler_rec negotiation_handlers[] = {
{ MAP_FILE_MAGIC_TYPE, handle_map_file },
{ NULL }
};

module negotiation_module = {
   STANDARD_MODULE_STUFF,
   NULL,			/* initializer */
   create_neg_dir_config,	/* dir config creater */
   merge_neg_dir_configs,	/* dir merger --- default is to override */
   NULL,			/* server config */
   NULL,			/* merge server config */
   negotiation_cmds,		/* command table */
   negotiation_handlers,	/* handlers */
   NULL,			/* filename translation */
   NULL,			/* check_user_id */
   NULL,			/* check auth */
   NULL,			/* check access */
   handle_multi,		/* type_checker */
   NULL,			/* fixups */
   NULL				/* logger */
};
