/*
 * Copyright (c) 1997-2007, OpenFWTK Development Group
 * All rights reserved. See LICENSE.
 */

/* html.c */

/* Copyright 1997-2000 by Eberhard Mattes <em-gw@windhager.de>
   Donated to the public domain.  No warranty.

   1997-07-19 Initial version
   1997-09-06 Accept more broken HTML comments; "href" configuration
   1997-09-10 REJ_COMMENT and "foo--bar"
   1997-09-13 url_check_path() removed
   1997-10-21 </SCRIPTX> does not end <SCRIPT>
   1997-10-22 "script" configuration
   1997-11-01 Apply "script" configuration to attributes
   1998-02-19 End <SCRIPT> at "</"; handle script macros in attributes
   1998-03-16 End <SCRIPT> at </SCRIPT>, replace "</" with "<\/"
   1998-05-29 Don't remove </APPLET>
   1998-06-08 Don't end comment at '">' if preceded by "<"
   1998-11-12 Accept white space (not just SP) in <!DOCTYPE ...>
   1999-01-01 Accept '&' in attribute values (CTYPE_VALUE)
   1999-07-01 Parsing of incorrect tags, comments, and <!DOCTYPE ...> changed
   1999-08-26 allow/block embed; don't remove </OBJECT>
   1999-09-16 allow/block style; <STYLE TYPE="text/javascript">
   2000-04-15 Make NAME_data const
   2000-05-29 Handle JavaScript in comments
   2000-08-10 Fix handling of markup declarations containing hyphens
   2000-12-11 Fix "html-meta unknown:copy" */

#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <syslog.h>
#include "unicode.h"
#include "doctype.h"
#include "script.h"
#include "firewall.h"
#include "fwfunc.h"
#include "libemfw.h"
#include "emio.h"
#include "fastheap.h"
#include "squid-gw.h"
#define SELECT_CTYPE_HTML
#include "tables.h"

extern int multibyte_charset (const char *);

/* Paranoid HTML Rewriting.

   There's a problem with parsing and copying tags, markup
   declarations, and comments: If we parse them loosely (e.g.,
   accepting unusal stuff as attributes), we might miss a dangerous
   tag which a less strictly parsing web browser will see.  If we
   parse them strictly (accepting correct syntax only), we might miss,
   e.g., a dangerous attribute which a more strictly parsing web
   browser will see.

   To solve this dilemma, we rewrite all the output.  Only constructs
   we accept as being valid are output.  Everything else is escaped or
   dropped.  If a tag is OK, we rebuild it from the input data (this
   may change white space and escaping).  In other words, the output
   does not contain any "<" or ">" except those which delimit tags
   which all web browsers will parse correctly.  That's the basic
   promise made by this code.  There's no harm in parsing the input
   incorrectly, it will just display incorrectly, but not pose a
   security risk.

   The syntax of the output is defined by the following grammar, using
   the same augmented BNF as RFC 2068:

   OUTPUT	= *( DATA | COMMENT-TAG | DOCTYPE | START-TAG | END-TAG )

   DATA		= DATA-CHAR | ENTITY-REF | NUM-CHAR-REF

   DATA-CHAR	= <any character excluding "<", ">", "&", 0x00 through 0x1f,
   		   0x7f through 0xa0, and 0xff, but including HT, LF, and CR>

   ENTITY-REF	= "&" 1*6ALPHA ";"			; Unless unknown:copy

   NUM-CHAR-REF	= "&#" 1*3DIGIT ";"			; 0 through 255

   COMMENT-TAG	= "<!" *COMMENT ">"			; Includes "<!>"

   DOCTYPE	= "<!DOCTYPE " *(NAME | LITERAL) ">"	; Without comments!

   START-TAG	= "<" NAME *100ATTRIBUTE ">"		; See ATTR_COUNT

   END-TAG	= "</" NAME ">"

   NAME		= NAME-START *71NAME-CHAR		; See TAG_NAME_LEN

   NAME-START	= ALPHA

   NAME-CHAR	= ALPHA | DIGIT | "." | "-"

   ALPHA	= UPALPHA | LOALPHA

   UPALPHA	= <any US-ASCII uppercase letter "A".."Z">

   LOALPHA	= <any US-ASCII lowercase letter "a".."z">

   DIGIT	= <any US-ASCII digit "0".."9">

   COMMENT	= "--" *COMMENT-DATA "--" *WHITESPACE

   COMMENT-DATA	= COMMENT-CHAR | ENTITY-REF | NUM-CHAR-REF

   COMMENT-CHAR	= <any character excluding "<", ">", 0x00 through 0x1f,
		   0x7f through 0xa0, and 0xff>

   WHITESPACE	= HT | LF | CR | SP

   LITERAL	= SQ-LITERAL | DQ-LITERAL

   SQ-LITERAL	= "'" *SQ-DATA "'"			; TODO: Limit length

   DQ-LITERAL	= '"' *DQ-DATA '"'			; TODO: Limit length

   SQ-DATA	= SQ-CHAR | ENTITY-REF | NUM-CHAR-REF

   DQ-DATA	= DQ-CHAR | ENTITY-REF | NUM-CHAR-REF

   SQ-CHAR	= <any character excluding "'", "<", ">", "&", 0x00 through
		   0x1f, 0x7f through 0xa0, and 0xff>

   DQ-CHAR	= <any character excluding '"', "<", ">", "&", 0x00 through
		   0x1f, 0x7f through 0xa0, and 0xff>

   ATTRIBUTE	= " " ANAME [ "=" AVALUE ]

   ANAME	= NAME-START *71NAME-CHAR		; See ATTR_NAME_LEN

   AVALUE	= ANAME | LITERAL			; TODO: Limit length
   
   */

/* =========================== PRELIMINARY STUFF =========================== */

/* Various maximum lengths. */

#define TAG_NAME_LEN		72	/* See RFC 1866 */
#define ATTR_NAME_LEN		72	/* See RFC 1866? */
#define ATTR_COUNT		100	/* See ATTCNT in html4.decl? */
#define REF_LEN			72	/* For "&name;" and "&#123;" */
#define ATTR_VALUE_LIMIT	cf_html.attr_value_limit

/* HTML configuration. */

struct conf_html cf_html;

/* ============================== PROTOTYPES =============================== */

struct attr;

static int process_tag (int c);
static void remove_script_macros (struct attr *a);

/* ============================== CHARACTERS =============================== */

#define HTML_CHAR_TYPE(c,x) (html_char_type[(unsigned char)c] & (x))

#define ALPHA(c)	HTML_CHAR_TYPE (c, CTYPE_ALPHA)
#define DIGIT(c)	HTML_CHAR_TYPE (c, CTYPE_DIGIT)
#define NAME(c)		HTML_CHAR_TYPE (c, CTYPE_NAME)
#define WHITE(c)	HTML_CHAR_TYPE (c, CTYPE_WHITE)
#define ESCAPE(c)	HTML_CHAR_TYPE (c, CTYPE_ESCAPE)
#define VALUE(c)	HTML_CHAR_TYPE (c, CTYPE_VALUE)

static const unsigned char html_char_type[UCHAR_MAX+1] =
{
#include "html-ctype.h"
};

extern int charset_type;

/* =========================== INPUT AND OUTPUT ============================ */

/* Private global variables. */

static EMI_FILE *in;		/* Input file */
static EMO_FILE *out;		/* Output file */
static unsigned long in_start;	/* Start position of input file */
static unsigned long out_start;	/* Start position of output file */
static int document_type = DOCTYPE_UNKNOWN; /* DOCTYPE and stuff */
static int script_lang = SCRIPT_UNKNOWN;

/* Current position for use in logging. */

#define IN_POS		(emi_amount (in, 1) - in_start)
#define OUT_POS		(emo_amount (out, 1) - out_start)

/* Fast input and output. */

#define fast_getc()	emi_getc (in)
#define fast_putc(c)	emo_putc (out, (c))

/* In case we want to save space instead of time... */

static int slow_getc (void)
{
  return fast_getc ();
}

static void slow_putc (unsigned char c)
{
  fast_putc (c);
}

/* ========================= CHARACTER REFERENCES ========================== */

/* This type encodes the various sorts of references, for `ref_type'
   and `attr_value_ref'.  Some of the values are used only in one of
   those two contexts.  See also escape_string(). */

enum ref
{
  REF_ERROR,			/* Invalid as reference (ref_type) */
  REF_NUMERIC_KNOWN,		/* Numeric character reference (known) */
  REF_NUMERIC_UNKNOWN,		/* Numeric character reference (unknown) */
  REF_ENTITY_KNOWN,		/* Entity reference (known) */
  REF_ENTITY_UNKNOWN,		/* Entity reference (unknown) */
  REF_VERBATIM,			/* Output verbatim (attr_value_ref) */
  REF_AUTO			/* Escape automatically (attr_value_ref) */
};

static enum ref ref_type;
static int ref_len;
static int ref_value;		/* -1 if (unknown) entity reference */
static int ref_term;		/* Terminating char (';') if not -1 */
static char ref_buf[REF_LEN+1];	/* All characters after the "&" */

/* Hash table of HTML character entities: `entity_hash'.  Table for
   converting character codes to entity names: `entity_by_code'.  This
   include file is generated from "html-entity.tab" by maketable. */

#include "html-entity.h"

/* Log a reference. */

static void log_ref (void)
{
  DEBUG_ASSERT (ref_len >= 0 && ref_len <= REF_LEN);
  ref_buf[ref_len] = 0;
  syslog (LLEV, "unknown reference at %lu/%lu: &%.512s",
	  IN_POS, OUT_POS, ref_buf);
}

/* Output the character C using a numeric character reference
   ("&#123;"). */

static void output_num_char_ref (int c)
{
  static char buf[] = "&#???;";
  int len;

  DEBUG_ASSERT (c >= 0 && c <= 255);
  len = 2;
  if (c >= 100)
    {
      buf[len++] = (char)(c / 100 + '0');
      c %= 100;
    }
  DEBUG_ASSERT (c < 100);
  if (c >= 10 || len != 2)
    {
      buf[len++] = (char)(c / 10 + '0');
      c %= 10;
    }
  DEBUG_ASSERT (c < 10);
  buf[len++] = (char)(c + '0');
  buf[len++] = ';';
  emo_write (out, buf, len);
}

/* Output a single character using an entity reference ("&lt;") or a
   numeric character reference ("&#34;") as appropriate. */

static void escape_char (unsigned char c)
{
  /* Note: "&quot;" was dropped in HTML 3.2. */
  switch (c)
    {
    case '<':
      emo_write (out, "&lt;", 4);
      break;
    case '>':
      emo_write (out, "&gt;", 4);
      break;
    case '&':
      emo_write (out, "&amp;", 5);
      break;
    default:
      output_num_char_ref (c);
    }
}

/* Output a single character, escaping it if necessary.  TODO: Macro?  */

static void output_char (unsigned char c)
{
  if (ESCAPE (c))
    escape_char (c);
  else
    fast_putc (c);
}

/* Output the N octets pointed to by S, escaping octets according to
   F.  F[i] (after casting to `enum ref') says how to handle S[i]. */

static void escape_string (const octet *s, const octet *f, size_t n)
{
  DEBUG_ASSERT (s != NULL); DEBUG_ASSERT (f != NULL);
  while (n != 0)
    {
      switch ((enum ref)*f)
	{
	case REF_NUMERIC_KNOWN:
	  output_num_char_ref (*s);
	  break;
	case REF_ENTITY_KNOWN:
	  DEBUG_ASSERT (entity_by_code[*s] != NULL);
	  slow_putc ('&');
	  emo_write (out, entity_by_code[*s]->name, entity_by_code[*s]->len);
	  slow_putc (';');
	  break;
	case REF_VERBATIM:
	  fast_putc (*s);
	  break;
	case REF_AUTO:
/*
 *	Ugly - we just make an exception instead of parsing unicode sequence
 */
	  if (ESCAPE (*s) && !((charset_type == CHARSET_UTF8) && (*s & 0x80)))
	    escape_char (*s);
	  else 
	    fast_putc (*s);
	  break;
	default:
	  ALWAYS_ASSERT (0);
	}
      ++s; ++f; --n;
    }
}

/* Parse "&#" DIGIT. */

static int parse_num_char_ref (void)
{
  int c, i, z, number;

  DEBUG_ASSERT (ref_len == 0);
  ref_buf[ref_len++] = '#';
  c = fast_getc ();
  if (c == EMI_EOF)
    {
      /* "&#" EOF. */
      return c;
    }
  else if (DIGIT (c))
    {
      /* "&#123" */

      while (c != EMI_EOF && DIGIT (c))
	{
	  if (ref_len >= REF_LEN)
	    return c;		/* Invalid, will be escaped */
	  ref_buf[ref_len++] = (char)c;
	  c = fast_getc ();
	}

      /* Skip terminator if present.  In SGML, a linefeed can also be
	 used as terminator and will be ignored.  However, web
	 browsers seem to ignore ";" only. */

      if (c == ';')
	{
	  ref_term = c;
	  c = fast_getc ();
	}

      /* Skip leading zeros. */

      z = 1;
      while (z < ref_len - 1 && ref_buf[z] == '0')
	++z;

      /* If the number has more than five digits with leading zeros
         suppressed, it's clearly greater than 65535.  Now check
         numbers of up to five digits for overflow.  We support only
         codes 0 through 255.  256 through 65535 are treated as
         unknown (we do not support Unicode). */

      if (ref_len - z > 5)
	return c;

      DEBUG_ASSERT (INT_MAX >= 99999);
      number = 0;
      for (i = z; i < ref_len; ++i)
	number = number * 10 + ref_buf[i] - '0';
      DEBUG_ASSERT (number >= 0 && number <= 99999);
      if (number > 65535)
	return c;		/* Invalid, will be escaped */
      if (number > 255)
	{
	  ref_type = REF_NUMERIC_UNKNOWN;
	  return c;
	}

      /* Remove leading zeros. */

      if (z != 1)
	{
	  DEBUG_ASSERT (z > 1);
	  DEBUG_ASSERT (ref_len > z);
	  memmove (ref_buf + 1, ref_buf + z, ref_len - z);
	  ref_len -= z - 1;
	}

      ref_type = REF_NUMERIC_KNOWN; ref_value = number;
      return c;
    }
  else
    {
      /* "&#!" or "&#name" (the latter SGML construct is not supported
         in HTML).  Escape what we've seen and continue at the
         offending character ("&#<EVIL>"). */

      return c;
    }
}

/* Parse "&" ALPHA. */

static int parse_entity_ref (int c)
{
  const struct hash_entry *he;

  DEBUG_ASSERT (c != EMI_EOF && ALPHA (c));
  DEBUG_ASSERT (ref_len == 0);
  do
    {
      if (ref_len >= REF_LEN)
	return c;		/* Invalid, will be escaped */
      ref_buf[ref_len++] = (char)c;
      c = fast_getc ();
    } while (c != EMI_EOF && ALPHA (c));

  /* Skip the semicolon even for unknown and too-long entity names.
     In SGML, a linefeed can also be used as terminator and will be
     ignored.  However, web browsers seem to ignore ";" only. */

  if (c == ';')
    {
      ref_term = c;
      c = fast_getc ();
    }

  /* Now that we have the entity name in `ref_buf' consult our table
     of entity names.  Entity names are case-sensitive. */

  he = find_hash_entry2 (&entity_descr, ref_buf, ref_len);
  if (he == NULL)
    {
      /* Unknown entity name. */
      ref_type = REF_ENTITY_UNKNOWN;
      return c;
    }

  /* Use the character code if it fits into an octet. */

  if (he->flags <= 255)
    ref_value = he->flags;
  ref_type = REF_ENTITY_KNOWN;
  return c;
}

/* Parse "&". */

static int parse_ref (void)
{
  int c;

  ref_len = 0; ref_type = REF_ERROR; ref_value = -1; ref_term = -1;
  c = fast_getc ();
  if (c == '#')
    return parse_num_char_ref ();
  else if (c != EMI_EOF && ALPHA (c))
    return parse_entity_ref (c);
  else
    return c;
}

static void process_ref (void)
{
  switch (ref_type)
    {
    case REF_ERROR:
      /* Not a reference ("A & B", "&#foo"). */
      escape_char ('&');
      emo_write (out, ref_buf, ref_len);
      if (ref_term != -1)
	slow_putc (ref_term);
      break;

    case REF_NUMERIC_UNKNOWN:
    case REF_ENTITY_UNKNOWN:
      if (cf_html.ref_unknown.log)
	log_ref ();
      if (cf_html.ref_unknown.output != REJ_DROP)
	{
	  DEBUG_ASSERT (cf_html.ref_unknown.output == REJ_ESCAPE
			|| cf_html.ref_unknown.output == REJ_COPY);
	  if (cf_html.ref_unknown.output == REJ_COPY)
	    fast_putc ('&');
	  else
	    escape_char ('&');
	  emo_write (out, ref_buf, ref_len); /* Just letters and digits */
	  if (ref_term != -1)
	    fast_putc (ref_term);
	}
      break;

    default:
      DEBUG_ASSERT (ref_type == REF_NUMERIC_KNOWN
		    || ref_type == REF_ENTITY_KNOWN);
      DEBUG_ASSERT (ref_len >= 1);
      fast_putc ('&');
      emo_write (out, ref_buf, ref_len);
      fast_putc (';');
      break;
    }
}

/* ============================= MANAGING TAGS ============================= */

static enum html_type tag_type;
static int tag_end;		/* "<!", for HTML_TAG */
static int tag_ok;		/* Tag OK */
static int tag_gt;		/* ">" seen */
static int tag_sep;		/* " " of "<name attr>" seen */
static int tag_slashed;		/* "/" in XHTML "<hr/>" "<br/>"  */
static int tag_name_len;
static char tag_name[TAG_NAME_LEN+1];
static char lower_tag_name[TAG_NAME_LEN+1];

/* Log a tag. */

static void log_tag (const char *msg, int c)
{
  char offending[2];

  DEBUG_ASSERT (tag_name_len >= 0 && tag_name_len <= TAG_NAME_LEN);
  DEBUG_ASSERT (tag_name[tag_name_len] == 0);
  if (c == EMI_EOF || tag_sep)
    offending[0] = 0;
  else
    {
      offending[0] = (char)c;
      offending[1] = 0;
    }
  syslog (LLEV, "%.128s tag at %lu/%lu: <%s%.256s%s>",
	  msg, IN_POS, OUT_POS, tag_end ? "/" : "", tag_name, offending);
}

/* Skip the rest of the tag.  We assume that we've reached the end of
   the tag as soon as a "<" or ">" is seen.  Dilemma: Treat ">" inside
   quotes as end of tag or not?  Note that log_tag() requires tag_name
   to be null-terminated, therefore skip_tag() also does so. */

static int skip_tag (int c)
{
  while (c != EMI_EOF && c != '>' && c != '<')
    c = fast_getc ();
  if (c == EMI_EOF || c == '<')
    return c;
  DEBUG_ASSERT (c == '>');
  return slow_getc ();
}

/* We found a character which terminated the tag prematurely.  If OK
   is non-zero (IT_OK), treat what we have seen as tag, inserting ">"
   into the output here.  If OK is zero (IT_NOT_OK), treat what we
   have seen as invalid tag.  The rest of the `tag' will be ignored by
   calling skip_tag() if SKIP is true (IT_SKIP). */

#define IT_NOT_OK	0
#define IT_OK		1

#define IT_DONT_SKIP	0
#define IT_SKIP		1

static int incorrect_tag (int c, int ok, int skip)
{
  DEBUG_ASSERT (tag_name_len >= 0 && tag_name_len <= TAG_NAME_LEN);
  DEBUG_ASSERT (tag_name[tag_name_len] == 0);

  tag_ok = ok; tag_gt = 1;	/* Pretend that the tag is OK */
  if (cf_html.log_incorrect_tags)
    log_tag ("incorrect", c);

  if (skip)
    return skip_tag (c);
  else
    return c;
}

/* ========================== MANAGING ATTRIBUTES ========================== */

static fastheap attr_heap;

static int attr_name_len;
static char attr_name[ATTR_NAME_LEN+1];

/* While parsing an attribute of a tag, the value is stored here, with
   numeric character references and entity references converted to
   octets, if possible.  Two values are stored for each (converted)
   octet: The octet proper in `attr_value[i]' and an octet in
   `attr_value_ref[i]' telling if and how to escape the octet on
   output.  Actually, attr_value_ref[i] contains an `enum ref'.  We
   store it as octet to simplify things (consider fastheap_memdup()).
   `attr_value_len' is the number of used octets in `attr_value' and
   `attr_value_ref'.  After parsing the value, `attr_value' will be
   null-terminated to simplify debugging. */

static int attr_value_len;
static octet *attr_value;
static octet *attr_value_ref;

/* What to do with an attribute.  IMPORTANT: The values are sorted by
   increasing danger, so that we can just compare the values for
   making dangerous values override harmless values if we wish to
   change the type of an attribute in the tag handler. */

enum attr_type
{
  AT_UNDECIDED,			/* Don't know yet */
  AT_BENIGN,			/* Keep the attribute */
  AT_UNKNOWN,			/* Drop the attribute (unknown) */
  AT_UNKNOWN_ON,		/* Drop the attribute (onWHATEVER) */
  AT_UNKNOWN_NOVALUE,		/* Drop the attribute (no value) */
  AT_UNKNOWN_ALPHANUMERIC,	/* Drop the attribute (alphanumeric value) */
  AT_DANGEROUS			/* Drop the attribute (dangerous) */
};

struct attr
{
  char *name;			/* Attribute name (on attr_heap) */
  octet *value;			/* Attribute value (on attr_heap) or NULL */
  octet *value_ref;		/* Ref type for value (on attr_heap) or NULL */
  size_t name_len;		/* Length of name */
  size_t value_len;		/* Length of value */
  char delim1, delim2;		/* Value delimiters: 0, '"', or '\\' */
  char has_value;		/* Non-zero if there's a value (redundant) */
  enum attr_type type;		/* What to do with this attribute */
};

static int attr_count;
static struct attr attr_v[ATTR_COUNT];

/* Add an attribute to our table of attributes.  If HAS_VALUE is
   non-zero, the attribute has a value (or rather, it always has a
   value, but we take the value as name if there's no "=").  If DELIM1
   is non-zero, it denotes the character (single or double quote)
   which starts the value literal.  If DELIM2 is non-zero, it denotes
   the character (single or double escape) which ends the value
   literal.  DELIM2 is required because the attribute might have
   turned out to be invalid (when hitting EOF before the end of the
   literal).  The value and its length are taken from `attr_value' and
   `attr_value_len', respectively.  The flags for the value are taken
   from `attr_value_ref'.  The name and its length are taken from
   `attr_name' and `attr_name_len', respectively.  After adding the
   attribute to our table, we scan it for script macros by calling
   remove_script_macros(), unless JavaScript is allowed. */

static void add_attr (int has_value, int delim1, int delim2)
{
  struct attr *a;

  /* Do nothing for attributes of end tags. */

  if (tag_end)
    return;

  /* Start a new entry in `attr_v'. */

  DEBUG_ASSERT (attr_count < ATTR_COUNT);
  a = &attr_v[attr_count++];
  a->type = AT_UNDECIDED;

  /* Put the name into the table. */

  DEBUG_ASSERT (attr_name_len >= 0 && attr_name_len <= ATTR_NAME_LEN);
  a->name = fastheap_strndup (&attr_heap, attr_name, attr_name_len);
  a->name_len = attr_name_len;

  /* Put the value into the table. */

  if (has_value)
    {
      DEBUG_ASSERT (attr_value_len >= 0 && attr_value_len <= ATTR_VALUE_LIMIT);
      a->value = (octet*) fastheap_strndup (&attr_heap, (char*) attr_value, 
						attr_value_len);
      a->value_ref = (octet*) fastheap_memdup (&attr_heap, attr_value_ref,
				      		attr_value_len);
      a->value_len = attr_value_len;
      a->has_value = 1;
    }
  else
    {
      a->value = NULL; a->value_ref = NULL; a->value_len = 0;
      a->has_value = 0;
    }

  /* Store the delimiters. */

  DEBUG_ASSERT (delim1 == (char)delim1);
  DEBUG_ASSERT (delim2 == (char)delim2);
  a->delim1 = (char)delim1;
  a->delim2 = (char)delim2;

  /* Remove script macros unless JavaScript is allowed. */

  if (cf_html.block_javascript.v)
    remove_script_macros (a);
}

/* Add an attribute to our table of attributes.  This function is used
   for adding (faked) attributes not read from the input. */

static void add_attr_value (const char *name, const char *value)
{
  size_t name_len = strlen (name);
  size_t value_len = strlen (value);
  struct attr *a;

  /* Start a new entry in `attr_v'. */

  DEBUG_ASSERT (attr_count < ATTR_COUNT);
  a = &attr_v[attr_count++];
  a->type = AT_UNDECIDED;

  /* Put the name into the table. */

  DEBUG_ASSERT (name_len >= 0 && name_len <= ATTR_NAME_LEN);
  a->name = fastheap_strndup (&attr_heap, name, name_len);
  a->name_len = name_len;

  /* Put the value into the table. */

  DEBUG_ASSERT (value_len >= 0 && value_len <= ATTR_VALUE_LIMIT);
  a->value = (octet*) fastheap_strndup (&attr_heap, value, value_len);
  a->value_ref = fastheap_alloc (&attr_heap, value_len);
  memset (a->value_ref, REF_AUTO, value_len);
  a->value_len = value_len;
  a->has_value = 1;

  /* Store the delimiters. */

  a->delim1 = '"';
  a->delim2 = '"';
}

/* Append the character C with reference type R to `attr_value' and
   `attr_value_ref', respectively, updating `attr_value_len'. */

#define ATTR_VALUE_APPEND_CHAR(c,r) ( \
  DEBUG_ASSERT (attr_value_len + 1 <= ATTR_VALUE_LIMIT), \
  attr_value[attr_value_len] = (octet)(c), \
  attr_value_ref[attr_value_len++] = (octet)(r))

/* Append N characters from the buffer pointed to by S with reference
   type R to `attr_value' and `attr_value_ref', respectively, updating
   `attr_value_len'. */

#define ATTR_VALUE_APPEND_STRING(s,n,r) ( \
  DEBUG_ASSERT (attr_value_len + (n) <= ATTR_VALUE_LIMIT), \
  DEBUG_ASSERT ((s) != NULL), \
  memcpy (attr_value + attr_value_len, (s), (n)), \
  memset (attr_value_ref + attr_value_len, (r), (n)), \
  attr_value_len += (n))

/* Remove leading and trailing spaces from the value of a an
   attribute.  Note that during parsing HT and CR have been replaced
   with SP, LF has been dropped.  So we need to deal with SP only. */

static void trim_value (struct attr *a)
{
  size_t skip, len;

  if (a->has_value)
    {
      DEBUG_ASSERT (a->value != NULL);
      skip = 0; len = a->value_len;
      while (len > 0 && a->value[len-1] == SP)
	--len;
      while (skip < len && a->value[skip] == SP)
	++skip;
      if (skip != 0 || len != a->value_len)
	{
	  /* We copy the string.  TODO: This is inefficient, perhaps
             we should have another pointer in `struct attr' which
             points to the first non-SP character.  On the other hand,
             leading SP characters are rare. */

	  DEBUG_ASSERT (skip <= len);
	  len -= skip;
	  DEBUG_ASSERT (len < a->value_len);
	  memmove (a->value, a->value + skip, len);
	  memmove (a->value_ref, a->value_ref + skip, len);
	  a->value_len = len;
	  a->value[len] = 0;	/* Keep it null-terminated */
	}
    }
}

/* Ever heard of script macros in attributes?  Here's an example:

     <BODY BGCOLOR='&{foo bar};'>

   The browser will execute the (JavaScript) code "foo bar" and will
   replace the characters from "&{" through "};" with the resulting
   value.  If JavaScript is blocked, we have to remove script macros.
   Unfortunately, we cannot disable them, as they work even if
   "&#38;&#123;" is used instead of "&#": the browser resolves
   character references before looking for script macros.  We just
   replace "&{" with "${" to get rid of script macros. */

static void remove_script_macros (struct attr *a)
{
  size_t i, len;

  if (!a->has_value || a->value_len < 2)
    return;			/* Nothing to do */
  DEBUG_ASSERT (a->value != NULL);
  len = a->value_len;

  /* First look if there's any "&{". */

  for (i = 0; i < len - 1; ++i)
    if (a->value[i] == '&' && a->value[i+1] == '{')
      break;
  if (i >= len - 1)
    return;			/* No "&{" found */

  /* There's at least one "&{".  We don't look for "};" as browsers
     may also not look for "};".  Log the attribute's value. */

  if (cf_html.log_script_macros)
    {
      DEBUG_ASSERT (a->value[a->value_len] == 0);
      syslog (LLEV, "script macro in attribute: %.512s", a->value);
    }

  /* Now replace "&{" with "${". */

  for (i = 0; i < len - 1; ++i)
    if (a->value[i] == '&' && a->value[i+1] == '{')
      {
	a->value[i] = '$';
	a->value_ref[i] = REF_AUTO;
      }
}

/* ========================= UTILITIES FOR PARSING ========================= */

/* Call html_callback() if enabled.  Currently, this is just a speed
   hack but may be extended to call different functions depending on
   the configuration.  Note that for security reasons we do not use a
   function pointer. */

#define CALLBACK(t,s) \
  (cf_html.use_callback ? html_callback ((t), (s)) : (void)0)

/* Skip whitespace.  C is the current input character.  Return the
   next input character. */

static int skip_white (int c)
{
  while (c != EMI_EOF && WHITE (c))
    c = fast_getc ();
  return c;
}

/* =============== PARSING COMMENTS AND MARKUP DECLARATIONS ================ */

/* Parse an HTML comment "<!--comment-->".  It's incredible how many
   HTML scribblers don't know the syntax of HTML comments.  For
   instance, "<!=====>", "<!--->", and "<! xxx>" are considered to be
   comments by some HTML scribblers.  Web browsers usually use some
   heuristics with lots of lookahead to find the end of comments.  We
   don't want to look ahead, so we have to use our own heuristics: ">"
   ends a comment if not preceded by an unmatched "<" or if there's a
   "--" between the "<" and the ">".  We rewrite those "comments" to
   stricly follow the syntax by adding "-" characters.  For instance,
   "<!-*->" becomes "<!--*-->", "<!-----"> becomes "<!-------->",
   "<!SGML>" becomes "<!--SGML-->".  The only exception is "<!DOCTYPE
   ...>".

   Note that in SGML, "<!- " does not start a comment; rather it's
   just data characters, equivalent to "&lt;!- ".  As there are so
   many broken HTML documents out there, we have to treat "<!- " as
   start of a comment...

   `tag_name' may contain characters of type NAME seen after "<!".  C
   contains another character already read (following the characters
   in `tag_name'). */

static int parse_comment (int c)
{
  /* COMMENT indicates whether we are inside "--...--" or not.  It has
     three values: OUTSIDE if not inside "--...--", INPUT if the
     beginning "--" comes from the input, FAKED if the beginning "--"
     has been inserted by squid-gw.  The difference between INPUT and
     FAKED is required for deciding whether "<" terminates the
     comment. */

  enum { OUTSIDE, INPUT, FAKED } comment;
  int lt;			/* Unmatched "<" seen */

  DEBUG_ASSERT (ESCAPE ('<'));	/* See loop below */
  DEBUG_ASSERT (ESCAPE ('>'));	/* See loop below */

  tag_type = HTML_COMMENT;
  CALLBACK (HTML_COMMENT, 1);

  comment = OUTSIDE; lt = 0; 
  if (tag_name_len != 0)
    {
      int i;

      /* Markup declaration treated as comment. */

      emo_puts (out, "<!--");

      /* Unfortunately, TAG_NAME may end with hyphens or may contain
         "--".  Output TAG_NAME, replacing certain instances of "-"
         with "&#45;".  That's much simpler than trying to keep track
         of whether we're inside or outside a comment. */

      for (i = 0; i < tag_name_len; ++i)
	{
	  if (tag_name[i] == '-'
	      && (i == tag_name_len - 1
		  || (i > 0 && tag_name[i-1] == '-')))
	    escape_char (tag_name[i]);
	  else
	    slow_putc (tag_name[i]);
	}
      comment = FAKED;
    }
  else
    emo_puts (out, "<!");

  /* Ignoring RFC 1866 and ISO 8879, we assume that the comment ends
     at the first ">" (unless preceded by an unmatched "<").  A "<"
     outside "--...--" (syntax error) also terminates the comment. */

  while (!(c == EMI_EOF
	   || (c == '>' && !lt)
	   || (c == '<' && comment != INPUT)))
    {
      if (c == '<')
	{
	  lt = 1;
	  if (comment == OUTSIDE)
	    {
	      emo_write (out, "--", 2);
	      comment = FAKED;
	    }
	  escape_char (c);
	  c = slow_getc ();
	}
      else if (c == '>' && lt)
	{
	  lt = 0;
	  if (comment == OUTSIDE)
	    {
	      emo_write (out, "--", 2);
	      comment = FAKED;
	    }
	  escape_char (c);
	  c = slow_getc ();
	}
      else if (c == '-')
	{
	  /* Look for another "-". */

	  c = slow_getc ();
	  if (c == '-')
	    {
	      /* We found "--".  This is the start or end of an
                 internal comment. */

	      lt = 0;
	      c = slow_getc ();
	      emo_write (out, "--", 2);
	      comment = comment == OUTSIDE ? INPUT : OUTSIDE;
	      if (comment == OUTSIDE)
		{
		  /* Don't rewrite "<!---- >" to "<!------ -->". */
		  while (c != EMI_EOF && WHITE (c))
		    {
		      fast_putc (c);
		      c = fast_getc ();
		    }
		}
	    }
	  else if (c == '<' && comment != INPUT)
	    break;		/* Drop the dash and end the comment */
	  else
	    {
	      /* "-" followed by anything else. (Note that we ignore
                 `lt' here which contradicts the spec given above.) */

	      int dashdash = 0;
	      if (comment == OUTSIDE)
		{
		  emo_write (out, "--", 2);
		  comment = FAKED; dashdash = 1;
		}

	      if (c == EMI_EOF || c == '>')
		break;
	      if (!dashdash)
		slow_putc ('-');

	      /* The character after the dash will be processed by the
                 next iteration of the loop. */
	    }
	}
      else if (c != EMI_EOF && c != '>')
	{
	  if (comment == OUTSIDE)
	    {
	      emo_write (out, "--", 2);
	      comment = FAKED;
	    }
	  /* Quick and dirty solution for "<!--&{js};-->".  I don't
             want to handle character entity references for now. */
	  if (c == '&' && !cf_html.block_javascript.v)
	    slow_putc (c);
	  else
	    output_char (c);
	  c = fast_getc ();

	  /* Quickly skip over uninteresting parts.  Note that this
	     loop stops at "<" and ">", see DEBUG_ASSERT at the start
	     of this function.  (This loop is for speed only.)  */

	  while (c != EMI_EOF && c != '-' && !ESCAPE (c))
	    {
	      fast_putc (c);
	      c = fast_getc ();
	    }
	}
    }
  if (comment != OUTSIDE)
    emo_write (out, "--", 2);
  slow_putc ('>');
  CALLBACK (HTML_COMMENT, 0);
  DEBUG_ASSERT (c == EMI_EOF || c == '<' || c == '>');
  if (c == '>')
    c = slow_getc ();
  return c;
}

/* Parse a markup declaration such as "<!DOCTYPE>".  C is the
   character following "<!". */

static int parse_markup_decl (int c)
{
  int delim, c2;
  char doctype[MAX_DOCTYPE_LEN];
  int doctype_len;
  char tokbuf[MAX_DOCTYPE_LEN];
  char *doctype_entry[256];
  int doctype_entry_c;

  tag_type = HTML_MARKUPDECL;

  /* "<!name" */

  while (c != EMI_EOF && NAME (c) && tag_name_len < TAG_NAME_LEN)
    {
      tag_name[tag_name_len++] = (char)c;
      c = fast_getc ();
    }
  tag_name[tag_name_len] = 0;	/* For syslog() */

  /* HTML supports the markup declaration DOCTYPE only.  We treat
     everything else as comment (which breaks broken pages containing,
     say, "<A>...<!/A>" -- yes, I've seen such brain damage!). */

  if (!(tag_name_len == 7 && lower_cmpn (tag_name, "doctype", 7) == 0))
    {
      /* In SGML markup declarations can contain literals and
	 comments.  Web browsers, however, just skip anything up to
	 the next ">" character.  Let's do the same thing. */

      return parse_comment (c);
    }

  /* OK, it's <!DOCTYPE>. */

  CALLBACK (HTML_MARKUPDECL, 1);
  emo_puts (out, "<!");
  emo_write (out, tag_name, tag_name_len);

  /* Parse the stuff that comes after "<!DOCTYPE ".  In SGML this can
     include ">" in literals and can contain comments.  Web browsers,
     however, stop at the first ">" no matter whether inside a literal
     or comment or not.  Let's do the same thing.  However, we try to
     keep quotes balanced and we drop comments.  Note that SGML does
     not allow entity and numeric character references here, so we
     can't escape ">".  When a syntax error (such as "PUBLIC=...") is
     encoutered, a ">" is inserted and all characters until the next
     ">" or "<" are skipped.  Copying those extra characters after
     inserting ">" (instead of skipping them) would break too many
     pages as only comments and "<!DOCTYPE" may precede "<HTML>". */

  doctype_len = 0;

  delim = 0;			/* Not inside a literal */
  while (c != '>')
    {
      if (c == EMI_EOF)
	return c;		/* Unterminated markup declaration */
      if (c == '"' || c == '\'')
	{
	  if (delim == 0)
	    delim = c;
	  else if (c == delim)
	    delim = 0;
	  slow_putc (c);
          doctype[doctype_len++] = (char)c;
	  c = slow_getc ();
	}
      else if (c == '-' && delim == 0)
	{
	  c = slow_getc ();
	  if (c != '-') 
	    {
	      slow_putc ('-');
              doctype[doctype_len++] = '-';
	    }
	  else
	    {
	      /* It's a comment.  Drop it.  As this code is probably
		 never reached, we can make it simple and slow. */

	      c = 0;		/* Anything but '-' */
	      do
		{
		  c2 = c; c = slow_getc ();
		} while (c != EMI_EOF && c != '<' && c != '>'
			 && !(c == '-' && c2 == '-'));
	      if (c == '-')
		c = slow_getc ();
	    }
	}
      else if (ESCAPE (c) || (delim == 0 && !WHITE (c) && !NAME (c)))
	break;
      else
	{
	  fast_putc (c);
          doctype[doctype_len++] = (char)c;
	  c = fast_getc ();
	}
    }
  if (delim != 0)
    {
      slow_putc (delim);
      doctype[doctype_len++] = (char)delim;
    }
  slow_putc ('>');
  doctype[doctype_len] = '\0';
  doctype_entry_c = enargv(doctype, doctype_entry, 
			   sizeof(doctype_entry) / sizeof(char *),
			   tokbuf, sizeof(tokbuf));
  if (doctype_entry_c >= 3) 
    {
      if (strcasecmp(doctype_entry[0],"html")) 
        {
	  syslog(LLEV,"non-html document, doctype declaration: %s",doctype);
	  document_type = DOCTYPE_XML_OTHER;	
        }
      else if (strcasecmp(doctype_entry[1],"PUBLIC")) 
	{
	  syslog(LLEV,"html DOCTYPE is %s instead of PUBLIC",doctype_entry[1]);
	}
      else if (strncasecmp(doctype_entry[2],"-//W3C//DTD ",12) &&
	       strncasecmp(doctype_entry[2],"-//IETF//DTD ",13))
	{
	  syslog(LLEV,"DTD is not w3c nor ietf, %s",doctype_entry[2]);
	}
      else if (strlen(doctype_entry[2]) <= 12)
	{
	  syslog(LLEV,"DTD is empty");
	}
      else if (strstr(doctype_entry[2],"//DTD HTML"))
        {
	  document_type = DOCTYPE_HTML;	
	}
      else if (strstr(doctype_entry[2],"//DTD XHTML"))
        {
	  document_type = DOCTYPE_XHTML;	
	}
    }
  else
    syslog(LLEV,"incomplete doctype declaration: %s",doctype);

  CALLBACK (HTML_MARKUPDECL, 0);
  tag_ok = 1;
  return skip_tag (c);
}

/* Parse "<!".  This includes comments ("<!--comment-->") and markup
   declarations ("<!DOCTYPE>").  When this function is called, the "!"
   has just been read. */

static int parse_bang (void)
{
  int c;

  tag_type = HTML_NONE;		/* To be ignored by process_tag() */
  tag_ok = 0;
  c = fast_getc ();
  if (c == '<')
    {
      emo_puts (out, "<!>");
      return c;
    }
  else if (ALPHA (c))
    return parse_markup_decl (c);
  else
    return parse_comment (c);
}

/* Parse "<?xml". Ugly - run through until "?> */
static int parse_special (void)
{
  int c;

  tag_name_len = 0;
  if ((c = fast_getc ()) == EMI_EOF)
    return (c); 

  while (c != EMI_EOF && NAME (c)  && tag_name_len < TAG_NAME_LEN)
    {
      tag_name[tag_name_len++] = (char)c;
      c = fast_getc ();
    }
  tag_name[tag_name_len] = 0;	/* For syslog() */

  tag_type = HTML_NONE;
  if ((tag_name_len == 3 && lower_cmpn (tag_name, "xml", 3) == 0))
    {
      emo_puts (out, "<?xml ");
      c = fast_getc ();
      while (c != EMI_EOF && c != '?' && c != '<' && c != '>')
        {
  	  fast_putc (c);
          c = fast_getc ();
        }
      emo_puts (out, "?>");
      if ((c != EMI_EOF)  && (c == '?'))
          c = fast_getc ();
      if ((c != EMI_EOF)  && (c != '<'))
          c = fast_getc ();
      return (c); 
    }
    return incorrect_tag (c, IT_NOT_OK, IT_SKIP);
}



/* ========================== PARSING ATTRIBUTES =========================== */

/* Append an entity reference or a numeric character reference to the
   value of an attribute.  This function assumes that parse_ref() has
   just been called.  P_ECHO_REF must point to an object, but the
   value returned there is not yet used.  Return 0 if the caller can
   continue, return -1 if the caller should stop (value too long). */

static int value_append_ref (int *p_echo_ref)
{
  *p_echo_ref = 1;
  if (ref_value != -1)
    {
      DEBUG_ASSERT (ref_type == REF_NUMERIC_KNOWN
		    || ref_type == REF_ENTITY_KNOWN);
      DEBUG_ASSERT (ref_value >= 0 && ref_value <= 255);

      /* Delete LF, replace HT and CR with SP (as required by
	 SGML). */

      if (ref_value == HT || ref_value == CR)
	ref_value = SP;
      if (ref_value != LF)
	{
	  if (attr_value_len >= ATTR_VALUE_LIMIT)
	    return -1;
	  ATTR_VALUE_APPEND_CHAR (ref_value, ref_type);
	}
      *p_echo_ref = 0;
    }
  else if (ref_type == REF_NUMERIC_KNOWN || ref_type == REF_ENTITY_KNOWN)
    {
      if (attr_value_len + 1 + ref_len + 1 > ATTR_VALUE_LIMIT)
	return -1;
      ATTR_VALUE_APPEND_CHAR ('&', REF_VERBATIM);
      ATTR_VALUE_APPEND_STRING (ref_buf, ref_len, REF_VERBATIM);
      ATTR_VALUE_APPEND_CHAR (';', REF_VERBATIM);
      *p_echo_ref = 0;
    }
  else if (ref_type == REF_NUMERIC_UNKNOWN || ref_type == REF_ENTITY_UNKNOWN)
    {
      if (cf_html.ref_unknown.log)
	log_ref ();
      if (cf_html.ref_unknown.output == REJ_ESCAPE
	  || cf_html.ref_unknown.output == REJ_COPY)
	{
	  enum ref r;
	  if (attr_value_len + 1 + ref_len + (ref_term != -1)
	      > ATTR_VALUE_LIMIT)
	    return -1;
	  if (cf_html.ref_unknown.output == REJ_COPY)
	    r = REF_VERBATIM;
	  else
	    r = REF_AUTO;
	  ATTR_VALUE_APPEND_CHAR ('&', r);
	  ATTR_VALUE_APPEND_STRING (ref_buf, ref_len, REF_VERBATIM);
	  if (ref_term != -1)
	    ATTR_VALUE_APPEND_CHAR (ref_term, REF_VERBATIM);
	  *p_echo_ref = 0;
	}
      else
	{
	  DEBUG_ASSERT (cf_html.ref_unknown.output == REJ_DROP);
	  *p_echo_ref = 0;
	}
    }
  else
    {
      /* Not a reference ("A & B", "&#foo"). */
      DEBUG_ASSERT (ref_type == REF_ERROR);
      if (attr_value_len + 1 + ref_len + (ref_term != -1) > ATTR_VALUE_LIMIT)
	return -1;
      ATTR_VALUE_APPEND_CHAR ('&', REF_AUTO);
      ATTR_VALUE_APPEND_STRING (ref_buf, ref_len, REF_VERBATIM);
      if (ref_term != -1)
	ATTR_VALUE_APPEND_CHAR (ref_term, REF_VERBATIM);
      *p_echo_ref = 0;
    }
  return 0;
}

/* Parse a literal used as attribut value.  C is the delimiter.  If
   the literal is not terminated properly or is too long, return the
   offending character (or EMI_EOF).  If the literal is terminated
   properly, return the delimiter. */

static int parse_value_literal (int c)
{
  int delim, echo_ref;

  echo_ref = 0; delim = c; c = fast_getc ();
  while (c != EMI_EOF && c != delim)
    {
      if (c == '&')
	{
	  c = parse_ref ();
	  if (value_append_ref (&echo_ref) != 0)
	    break;
	}
      else
	{
	  /* Delete LF, replace HT and CR with SP (as required by
             SGML). */

	  if (c == HT || c == CR)
	    c = SP;
	  if (c != LF)
	    {
	      if (attr_value_len >= ATTR_VALUE_LIMIT)
		break;
	      ATTR_VALUE_APPEND_CHAR (c, REF_AUTO);
	    }
	  c = fast_getc ();
	}
    }

  /* TODO: The case of attr_value being overflowed by a reference is
     not yet handled correctly.  We should call process_ref()
     somewhere later if echo_ref is true.  Problem: we may return the
     correct delimiter, causing the delimiter to be output before we
     have a good chance for calling process_ref().  The simplest
     solution is to allocate attr_value and attr_value_ref
     dynamically.  Or just drop the attribute. */

  return c;
}

/* Parse a name used as attribut value.  C is the first character.  If
   the name is too long, return the offending character (or EMI_EOF).
   If the name is formed properly, return the next character.  If a
   delimiter is required (because the name isn't a name), it will be
   stored to the object pointed to by DELIM.  If no delimiter is
   required, zero will be stored. */

static int parse_value_name (int c, int *delim)
{
  int echo_ref;			/* See comment in parse_value_literal() */

  /* Unfortunately, there are a lot of broken HTML pages which contain
     "<FONT SIZE=+1>" or "<A HREF=foo/bar"> or "<A HREF=foo?a=1&b=2>".
     Silently accept the configured characters and escape the
     value. */

  *delim = 0;
  while (c != EMI_EOF && (VALUE (c) || (*delim == '"' && c == '\'')))
    {
      if (!NAME (c))
	*delim = '"';
      if (c == '&')
	{
	  c = parse_ref ();
	  if (value_append_ref (&echo_ref) != 0)
	    break;
	}
      else
	{
	  if (attr_value_len >= ATTR_VALUE_LIMIT)
	    break;
	  ATTR_VALUE_APPEND_CHAR (c, REF_VERBATIM);
	  c = slow_getc ();
	}
    }
  return c;
}

/* Parse the attributes of a tag.  Note: Splitting this function would
   require another return value for indicating whether parsing the tag
   should continue or not.  This would also be necessary if
   parse_tag() were not returning immediately after calling this
   function.  Don't store the attributes if `tag_end' is true. */

static int parse_attributes (int c)
{
  int delim;

  for (;;)
    {
      /* Skip white space preceding and following attributes. If we
         happen to escape the entire end tag, we've lost the white
         space, but that shouldn't matter. */

      c = skip_white (c);
      if (c == '/')
	{
	  tag_slashed = 1; c = fast_getc ();
	  if (c != '>')
	    return incorrect_tag (c, IT_OK, IT_SKIP);
	}
      if (c == '>')
	{
	  tag_ok = 1; tag_gt = 1;
	  return slow_getc ();	/* Done! */
	}
      /* Common javascript typo: ';' outside of the attrubute value.
	 Just skip. */

      if (c == ';')
	{
          c = fast_getc ();
	  continue;
	}

      /* Note that "<tag a1='v1'a2='v2'>" is valid, so we don't check
         for white space here.  process_tag() will insert a space,
         however. */

      /* Assume that we reached the end of the tag when reaching EOF
         or when there are too many attributes. */

      if (c == EMI_EOF || attr_count >= ATTR_COUNT)
	{
	  /* "<tag attr=value<EVIL>" */
	  /* "<tag " EOF */
	  /* "<tag a1 a2 ... too-many" */

	  return incorrect_tag (c, IT_OK, IT_SKIP);
	}

      /* Now we expect an attribute.  Read the attribute name.  (In
         SGML speak, the token will be the value, not the name, if
         there's no "=".  Treating it always as name makes checking
         simpler, however.) */

      attr_name_len = 0; attr_value_len = 0;

      /* Handle "<tag <EVIL>" and "<tag a=v <EVIL>". */

      if (!ALPHA (c))
	return incorrect_tag (c, IT_OK, IT_SKIP);

      /* Be careful from now on to record everything read in attr_v[]
         so that we can rebuild the tag (escaped).  That is, each
         return statement must be preceded by a call to add_attr(). */

      attr_name[attr_name_len++] = (char)c;
      c = fast_getc ();
      while (c != EMI_EOF && (NAME (c) || c == ':')
             && attr_name_len < ATTR_NAME_LEN)
	{
	  attr_name[attr_name_len++] = (char)c;
	  c = fast_getc ();
	}
      if (c == EMI_EOF)
	{
	  /* "<tag name" EOF */

	  add_attr (0, 0, 0);
	  return incorrect_tag (c, IT_OK, IT_SKIP);
	}
      DEBUG_ASSERT (c != EMI_EOF);
      if (attr_name_len >= ATTR_NAME_LEN && NAME (c))
	{
	  /* "<tag toolong<EVIL>" */

	  add_attr (0, 0, 0);
	  return incorrect_tag (c, IT_OK, IT_SKIP);
	}

      /* If there's an "=" sign, it can be preceded and followed by
	 white space. */

      c = skip_white (c);
      if (c == '=')
	{
	  c = slow_getc ();
	  c = skip_white (c);

	  /* Here we expect the value.  It can be either an escaped
	     string or a name token (which does not have to start with
	     a letter). */

	  if (c == '"' || c == '\'')
	    {
	      delim = c;
	      c = parse_value_literal (c);
	      if (c != delim)
		{
		  add_attr (1, delim, 0);
		  return c;	/* Invalid tag, don't prented it's valid */
		}
	      add_attr (1, delim, delim);
	      c = slow_getc ();
	    }
	  else
	    {
	      c = parse_value_name (c, &delim);
	      if (attr_value_len == 0)
		{
		  /* "<tag attr=<EVIL>" */

		  add_attr (0, 0, 0);
		  return incorrect_tag (c, IT_OK, IT_SKIP);
		}
	      if (c != EMI_EOF && VALUE (c))
		{
		  /* Value too long. */
		  add_attr (1, delim, delim);
		  return incorrect_tag (c, IT_OK, IT_SKIP);
		}
	      add_attr (1, delim, delim);
	    }
	}
      else
	{
	  /* Attribute without value. */

	  add_attr (0, 0, 0);
	}
    }
}

/* ================== PROCESSING AND FILTERING ATTRIBUTES ================== */

/* Process attributes such as HREF taking an "%URL;" value. */

static int attr_url (struct attr *a)
{
  struct url u;

  if (!a->has_value)
    return 0;			/* Keep the attribute TODO? */

  /* ASP (Microsoft IIS) appends a NUL character for redirects.
     Remove it. */

  if (a->value_len > 0 && a->value[a->value_len-1] == 0)
    {
      syslog (LLEV, "Trailing NUL character removed");
      a->value_len -= 1;
    }

  trim_value (a);		/* Remove leading and trailing SP */
  if (url_parse (&u, a->value, a->value_len, 0) != 0)
    return 1;			/* Remove the attribute */

  /* Perform some scheme-specific checks. */

  switch (u.scheme)
    {
    case URL_EMPTY:
	return 0;
    case URL_CLSID:
      if (cf_html.block_object.v)
	return 1;
      break;
    case URL_JAVA:
      if (cf_html.block_java.v)
	return 1;
      break;
    case URL_JAVASCRIPT:
      if (cf_html.block_javascript.v)
	return 1;
      break;
    default:
      break;
    }

  /* Apply "href" configuration rules. */

  if (config_href ((char*) a->value, &u) != 0)
    return 1;			/* Remove the attribute */

  /* Keep the attribute. */

  return 0;
}

/* Process attributes such as ONWHATEVER taking a "%SCRIPT;" value. */

static int attr_script (struct attr *a)
{
  /* Remove the attribute if JavaScript is to be blocked. */

  if (cf_html.block_javascript.v)
    return 1;			/* Remove the attribute */
  if (cf_html.script == CDATA_VERBATIM)
    {
      /* Don't automatically escape characters in the value. */
      size_t i;
      for (i = 0; i < a->value_len; ++i)
	if (a->value_ref[i] == REF_AUTO)
	  a->value_ref[i] = REF_VERBATIM;
    }
  return 0;
}

/* Hash table of values for %ContentType;.  This include file is
   generated from "html-type.tab" by maketable. */

#include "html-type.h"

/* Process attributes such as TYPE taking an "%ContentType;" value.
   For now, the acceptable content types are quite restricted unless
   JavaScript is allowed. */

static int attr_ctype (struct attr *a)
{
  if (!a->has_value)
    return 0;			/* Keep the attribute TODO? */

  trim_value (a);		/* Remove leading and trailing SP */
  if (cf_html.block_javascript.v)
    {
      char tmp[32];
      const struct hash_entry *he;

      if (a->value_len >= sizeof (tmp))
	return 1;
      lower_copy (tmp, (char*) a->value, a->value_len);
      he = find_hash_entry2 (&type_descr, tmp, a->value_len);
      if (he == NULL || he->flags != 0)
	return 1;
    }
  return 0;
}

/* Process the STYLE attribute. */

static int attr_style (struct attr *a)
{
  /* Remove the attribute if style sheets are blocked. */

  if (cf_html.block_style.v)
    return 1;			/* Remove the attribute */
  return 0;			/* Keep the attribute */
}

/* Leave the attribute unprocessed. */
 
static int attr_default (struct attr *a)
{
  return 0;			/* Keep the attribute */
}

/* Table of HTML attributes: `attr_hash'.  This include file is
   generated from "html-attr.tab" by maketable. */

#define AF_LOG_KEY	0x01

#include "html-attr.h"

/* Output the value of an attribute if the attribute has a value.  As
   we haven't recorded white space aroud the "=" character, we cannot
   output it here.  This doesn't matter and keeps things simple.  Note
   that we escape the value in order to cope with broken web browsers
   which take a ">" inside a literal to terminate the tag!  This also
   prevents the web browser from interpreting "<TAG>" inside the
   literal as tag.  I think this is a good thing.  Just consider what
   happens if a web browser does not recognize the tag this attribute
   is part of as tag. */

static void output_value (const struct attr *a)
{
  if (a->has_value)
    {
      DEBUG_ASSERT (a->value != NULL);
      slow_putc ('=');
      if (a->delim1 != 0)
	slow_putc (a->delim1);
      escape_string (a->value, a->value_ref, a->value_len);
      if (a->delim2 != 0)
	slow_putc (a->delim2);
    }
}

/* Output a `removed' attribute.  We `remove' the attribute by
   prepending "REMOVED-" to its name.  There's no danger as the value
   will be escaped. TODO: Configure to prefix with "REMOVED-" or to
   really remove. */

static void prefix_attr (const struct attr *a)
{
  const char prefix[] = "REMOVED-";
#define PREFIX_LEN (sizeof (prefix) - 1)

  /* Attributes are separated by spaces even when this is not required
     (as in "<tag a1='v1'a2=v2>".  Always play safe. */

  slow_putc (' ');

  /* Prefix with "REMOVED-" unless already present. */

  if (a->name_len >= PREFIX_LEN && memcmp (a->name, prefix, PREFIX_LEN) == 0)
    emo_write (out, a->name, a->name_len);
  else if (a->name_len + PREFIX_LEN <= ATTR_NAME_LEN)
    {
      emo_write (out, prefix, PREFIX_LEN);
      emo_write (out, a->name, a->name_len);
    }
  else
    {
      emo_write (out, prefix, PREFIX_LEN);
      emo_write (out, a->name, ATTR_NAME_LEN - PREFIX_LEN);
    }

  output_value (a);
#undef PREFIX_LEN
}

/* Output an attribute. */

static void output_attr (struct attr *a)
{
  /* Attributes are separated by spaces even when this is not required
     (as in "<tag a1='v1'a2=v2>".  Always play safe. */

  slow_putc (' ');
  emo_write (out, a->name, a->name_len);
  output_value (a);
}

/* Remove an attribute according to POLICY.  MSG identifies why the
   attribute is removed. */

static void remove_attr (const struct attr *a, const struct rej_policy *rp,
			 const char *msg)
{
  DEBUG_ASSERT (tag_name_len >= 0 && tag_name_len <= TAG_NAME_LEN);
  DEBUG_ASSERT (tag_name[tag_name_len] == 0);
  if (rp->log)
    syslog (LLEV, "%.128s attribute for <%.256s> at %lu/%lu: %.128s%s%.256s",
	    msg, tag_name, IN_POS, OUT_POS,
	    a->name,
	    a->has_value ? "=" : "",
	    a->has_value ? (const char *)a->value : "");

  switch (rp->output)
    {
    case REJ_DROP:
    case REJ_COMMENT:	/* We don't comment out attributes. */
    case REJ_ESCAPE:	/* We don't escape attributes */
      return;
    case REJ_PREFIX:
      prefix_attr (a);
      break;
    default:
      ALWAYS_ASSERT (0);
    }
}

/* Return 1 if the string contains solely letters and digit.  Return 0
   otherwise.  (Return 1 if the string is empty.) */

static int is_alphanumeric (const octet *s, size_t n)
{
  while (n != 0)
    {
      if (!(ALPHA (*s) || DIGIT (*s)))
	return 0;
      ++s; --n;
    }
  return 1;
}

/* Process one attribute for output_tag().  This function returns the
   new value for the `type' member of the attribute.  Note that the
   value of the attribute might be changed (attr_url!). */

static enum attr_type process_attribute (struct attr *a)
{
  const struct hash_entry *he;
  int (*handler)(struct attr *);
  static char key[TAG_NAME_LEN + 1 + ATTR_NAME_LEN + 1];

  /* If there's a handler for the attribute, call it to find out what
     to do with the attribute, no matter what tag we're processing.
     Keep the attribute by default.  First search for "tag/attr".  If
     there's no entry, search for "/attr". */

  DEBUG_ASSERT (tag_name_len + 1 + a->name_len + 1 <= sizeof (key));
  lower_copy (key, tag_name, tag_name_len);
  key[tag_name_len] = '/';
  lower_copy (key + tag_name_len + 1, a->name, a->name_len);
  key[tag_name_len + 1 + a->name_len] = 0; /* For syslog() */
  he = find_hash_entry2 (&attr_descr, key, tag_name_len + 1 + a->name_len);
  if (he == NULL)
    he = find_hash_entry2 (&attr_descr, key + tag_name_len, 1 + a->name_len);

  if (he == NULL)
    {
      /* Unknown attribute.  Check for "onWHATEVER". */

      if (a->name_len >= 2 && key[tag_name_len+1] == 'o'
	  && key[tag_name_len+2] == 'n')
	{
	  /* Unknown attribute starting with "on". */

	  if (cf_html.attr_on.output == REJ_COPY)
	    return AT_BENIGN;	/* TODO: log */
	  else
	    return AT_UNKNOWN_ON;
	}
      else if (!a->has_value)
	{
	  /* Unknown attribute without value and not starting with
             "on". */

	  if (cf_html.attr_novalue.output == REJ_COPY)
	    return AT_BENIGN;	/* TODO: log */
	  else
	    return AT_UNKNOWN_NOVALUE;
	}
      else if (is_alphanumeric (a->value, a->value_len))
	{
	  /* Unknown attribute with alphanumeric value and not
             stabrting with "on". */

	  if (cf_html.attr_alphanumeric.output == REJ_COPY)
	    return AT_BENIGN;	/* TODO: log */
	  else
	    return AT_UNKNOWN_ALPHANUMERIC;
	}
      else
	{
	  /* Unknown attribute not starting with "on" and having a
             value containing non-alphanumeric characters. */

	  if (cf_html.attr_unknown.output == REJ_COPY)
	    return AT_BENIGN;	/* TODO: log */
	  else
	    return AT_UNKNOWN;
	}
    }
  else
    {
      /* Known attribute. */

      if (cf_html.log_key && (he->flags & AF_LOG_KEY))
	syslog (LLEV, "unexpected tag/attribute pair: %.128s", key);
      handler = he->handler;
      ALWAYS_ASSERT (ATTR_HANDLER (handler)); /* Paranoia */
      if (handler (a))
	return AT_DANGEROUS;
      else
	return AT_BENIGN;
    }
}

/* Escape an attribute for escape_tag(). */

static void escape_attribute (struct attr *a)
{
  slow_putc (' ');
  emo_write (out, a->name, a->name_len);
  if (a->has_value)
    {
      slow_putc ('=');
      if (a->delim1 != 0)
	output_char (a->delim1);
      escape_string (a->value, a->value_ref, a->value_len);
      if (a->delim2 != 0)
	output_char (a->delim2);
    }
}

/* ============================= PARSING TAGS ============================== */

/* Parse "<". */

static int parse_tag (void)
{
  int c;

  tag_type = HTML_TAG; tag_ok = 0; tag_end = 0; tag_gt = 0; tag_slashed = 0;
  tag_name_len = 0; attr_count = 0; tag_sep = 0;
  tag_name[tag_name_len] = 0;	/* For syslog() */

  /* Processing depends heavily on the next character. */

  c = fast_getc ();
  switch (c)
    {
    case EMI_EOF:		/* "<" EOF */
      return c;
    case '!':
      /* "<!>", "<!--comment-->", "<!DOCTYPE ...>" */
      return parse_bang ();
    case '?':
      return parse_special ();
    case '/':
      /* End tag "</" */
      tag_end = 1;
      c = fast_getc ();
      if (c == EMI_EOF)		/* "</" EOF */
	return c;
      break;
    }

  /* The empty tags "<>" and "</>" are allowed in theory, but we don't
     know what web browsers support them.  Therefore we escape
     them. */

  DEBUG_ASSERT (c != EMI_EOF);
  if (c == '>')
    {
      tag_gt = 1;
      return slow_getc ();
    }

  /* Read the first character of the tag name.  Return if there's no
     tag name.  Note that, for instance, "</ " is not a valid tag and
     should be escaped. */

  if (!ALPHA (c))
    return c;			/* Note: "<<EVIL>" and "</<EVIL>" */
  tag_name[tag_name_len++] = (char)c;
  c = fast_getc ();

  /* Read any subsequent characters of the tag name. */

  /* Special case ':' is workaround for MS-generated pseudo-xml. We have no such
     tags defined so there will be *a lot* of messages in logfile if we log
     unknown tags. */

  while (c != EMI_EOF && ( NAME (c) || (c == ':')) && tag_name_len < TAG_NAME_LEN)
    {
      tag_name[tag_name_len++] = (char)c;
      c = fast_getc ();
    }

  tag_name[tag_name_len] = 0;	/* For syslog() */

  /* Handle too long a tag name.  We treat the tag as invalid and skip
     all characters until reaching a ">". */

  if (c != EMI_EOF && NAME (c))
    {
      DEBUG_ASSERT (tag_name_len == TAG_NAME_LEN);
      return incorrect_tag (c, IT_NOT_OK, IT_SKIP);
    }

  /* Handle incorrect and invalid tags. */

  if (c != '>' && !WHITE (c))
    {
      /* <hr/> and <br/> */

      if (c == '/')
	tag_slashed = 1;
      else {
        /* In particular, "<foo<EVIL>" is a shortcut or typo for
           "<foo><EVIL>".  As browsers ignore the "<foo" part, we treat
           it as invalid, to match that behavior.  For "<foo!bar>" we
           skip all characters after "!" until reaching a ">". */
        if (c == '<')
	  return incorrect_tag (c, IT_NOT_OK, IT_DONT_SKIP);
        else
	  return incorrect_tag (c, IT_NOT_OK, IT_SKIP);
      }
    }

  /* Record whether we've seen white space following the tag name. */

  DEBUG_ASSERT (c != EMI_EOF);
  if (WHITE (c) && !tag_end)
    tag_sep = 1;

  /* Parse the attributes and return (see comment for
     parse_attributes()).  An end tag cannot have attributes.  Tell
     that HTML authors!  Web browsers seem to accept the same syntax
     as for start tags.  We also do so and then throw away the
     attributes. */

  return parse_attributes (c);
}

/* ===================== PROCESSING AND FILTERING TAGS ===================== */

/* Output the tag as tag, filtering the attributes.  Prepend
   "REMOVED-" if DO_PREFIX is true. */

static void output_tag (int do_prefix)
{
  int i;
  enum attr_type at;
  const char prefix[] = "REMOVED-";
#define PREFIX_LEN (sizeof (prefix) - 1)

  /* Rebuild the tag from the data we've saved.  The output will be
     normalized.  First, start the tag and output the name. */

  CALLBACK (HTML_TAG, 1);
  fast_putc ('<');
  if (tag_end)
    fast_putc ('/');
  if (!do_prefix)
    emo_write (out, tag_name, tag_name_len);
  else if (tag_name_len >= PREFIX_LEN
	   && memcmp (tag_name, prefix, PREFIX_LEN) == 0)
    emo_write (out, tag_name, tag_name_len);
  else if (tag_name_len + PREFIX_LEN <= TAG_NAME_LEN)
    {
      emo_write (out, prefix, PREFIX_LEN);
      emo_write (out, tag_name, tag_name_len);
    }
  else
    {
      emo_write (out, prefix, PREFIX_LEN);
      emo_write (out, tag_name, TAG_NAME_LEN - PREFIX_LEN);
    }

  /* Output the attributes. */

  for (i = 0; i < attr_count; ++i)
    {
      at = process_attribute (&attr_v[i]);
      if (at > attr_v[i].type)
	attr_v[i].type = at;
      switch (attr_v[i].type)
	{
	case AT_BENIGN:
	  output_attr (&attr_v[i]);
	  break;
	case AT_DANGEROUS:
	  remove_attr (&attr_v[i], &cf_html.attr_dangerous, "dangerous");
	  break;
	case AT_UNKNOWN:
	  remove_attr (&attr_v[i], &cf_html.attr_unknown, "unknown");
	  break;
	case AT_UNKNOWN_ON:
	  remove_attr (&attr_v[i], &cf_html.attr_on, "unknown");
	  break;
	case AT_UNKNOWN_NOVALUE:
	  remove_attr (&attr_v[i], &cf_html.attr_novalue, "unknown");
	  break;
	case AT_UNKNOWN_ALPHANUMERIC:
	  remove_attr (&attr_v[i], &cf_html.attr_alphanumeric, "unknown");
	  break;
	default:		/* AT_UNDECIDED */
	  ALWAYS_ASSERT (0);
	}
    }

  /* This is the end of the tag. */

  if (tag_slashed)
    fast_putc ('/');

  fast_putc ('>');
  CALLBACK (HTML_TAG, 0);
#undef PREFIX_LEN
}

/* Escape the tag or what looked like a tag. */

static void escape_tag (void)
{
  int i;

  output_char ('<');
  if (tag_end)
    slow_putc ('/');
  emo_write (out, tag_name, tag_name_len);

  /* White space might be significant here; however, we no longer know
     of what characters it was made up exactly. */

  if (attr_count == 0 && tag_sep)
    slow_putc (' ');

  for (i = 0; i < attr_count; ++i)
    escape_attribute (&attr_v[i]);

  if (tag_gt)
    output_char ('>');
}

/* Remove a tag according to POLICY.  MSG identifies why the tag is
   removed, C is the next input character (used only for logging
   invalid tags).  Note that log_tag() requires tag_name to be
   null-terminated, therefore remove_tag() also does so. */

static void remove_tag (const struct rej_policy *rp, const char *msg, int c)
{
  int i;

  DEBUG_ASSERT (tag_name_len >= 0 && tag_name_len <= TAG_NAME_LEN);
  DEBUG_ASSERT (tag_name[tag_name_len] == 0);
  if (rp->log)
    log_tag (msg, c);

  switch (rp->output)
    {
    case REJ_DROP:
      /* This works because we rewrite numeric character references
         and entity references.  If we didn't, we would have to insert
         "<!>" to avoid turning "&a<foo>mp;" into "&amp;". */
      break;

    case REJ_PREFIX:
      output_tag (1);
      break;

    case REJ_COMMENT:
      emo_puts (out, "<!-- ");
      if (tag_end)
	slow_putc ('/');
      for (i = 0; i < tag_name_len; ++i)
	if (i > 0 && tag_name[i] == '-' && tag_name[i-1] == '-')
	  escape_char (tag_name[i]);
	else
	  slow_putc (tag_name[i]);
      emo_puts (out, " removed by squid-gw -->");
      break;

    case REJ_ESCAPE:
      escape_tag ();
      break;

    case REJ_COPY:
      output_tag (0);
      break;

    default:
      ALWAYS_ASSERT (0);
    }
}

/* Remove an invalid tag according to the configuration. */

static void invalid_tag (int c)
{
  DEBUG_ASSERT (cf_html.tag_invalid.output != REJ_PREFIX);
  remove_tag (&cf_html.tag_invalid, "invalid", c);
}

/* Remove a dangerous tag according to the configuration. */

static void dangerous_tag (void)
{
  remove_tag (&cf_html.tag_dangerous, "dangerous", EMI_EOF);
}

/* Remove an unknown tag according to the configuration. */

static void unknown_tag (void)
{
  remove_tag (&cf_html.tag_unknown, "unknown", EMI_EOF);
}

/* Process a script (that is, everything between <SCRIPT> and
   </SCRIPT>) or a style sheet (that is, everything between <STYLE>
   and </STYLE>) according to POLICY.  END_TAG points to the end tag
   using lower-case letters, e.g., "</script>".  Remove the CDATA for
   CDATA_DROP, just return and let HTML parser handle the CDATA for
   CDATA_HTML (which shouldn't be done for CDATA), or copy the CDATA
   verbatim for CDATA_VERBATIM (dangerous). */

/* TODO: Add another policy which automatically wraps the CDATA in an
   HTML comment. */

static int process_cdata (int c, const char *end_tag,
			  enum cdata_policy policy)
{
  int i;

  DEBUG_ASSERT (!tag_end);	/* Avoid recursion */

  /* Let HTML rewriting mangle the CDATA. */

  if (policy == CDATA_HTML)
    return c;

  for (;;)
    {
      /* We skip quickly over the CDATA, without paying attention to
	 HTML syntax.  If there's a "<", we have to look closer, it
	 might be "</SCRIPT>".  Web browsers use some heuristic for
	 ignoring "</SCRIPT>" embedded in scripts (e.g., in strings).
	 Trying to reseble this algorithm, we track single and double
	 quotes (those may be nested) and javascript comments (// ).
	 Please note that, opposing to standards and common sense,
	 commented out </SCRIPT> *DOES* terminate the script, because 
	 browsers behave this way.  We also replace "</" with "<\/", 
	 which works correctly inside JavaScript string literals.  
	 It might break scripts written in other languages, but these 
	 scripts are broken anyway (as "</" must not occur inside the 
	 script). */

      while (c != EMI_EOF && c != '<')
	{
	  if (policy == CDATA_VERBATIM)
	    fast_putc (c);
	  c = fast_getc ();
	}
      i = 0;
      while (end_tag[i] != '>' && c != EMI_EOF && lower_table[c] == end_tag[i])
	{
	  if (i >= 2)
	    tag_name[i-2] = (char)c;
	  ++i;
	  c = slow_getc ();
	}
      if (c == EMI_EOF)
	{
	  if (policy == CDATA_VERBATIM)
	    {
	      /* Copy the buffered characters (same code as below). */
	      emo_write (out, "<\\/", i < 2 ? i : 3);
	      if (i >= 2)
		emo_write (out, tag_name, i - 2);
	      /* Attempt to prevent the browser's heuristic from
		 interpreting the script as HTML. */
	      emo_puts (out, end_tag);
	    }
	  return c;
	}
      if (end_tag[i] == '>' && !NAME (c))
	break;
      if (policy == CDATA_VERBATIM)
	{
	  /* Copy the buffered characters, replacing "</" with "<\/"
             (same code as above). */

	  emo_write (out, "<\\/", i < 2 ? i : 3);
	  if (i >= 2)
	    emo_write (out, tag_name, i - 2);
	}
      /* Be careful to not miss "</</SCRIPT>" */
    }

  /* We found "</SCRIPT". */

  DEBUG_ASSERT (i - 2 == tag_name_len);
  DEBUG_ASSERT (tag_name[tag_name_len] == 0);
  tag_end = 1; attr_count = 0; tag_sep = 0;

  /* See process_tag(). */

  if (c != '>' && !WHITE (c))
    {
      /* For web browsers, "</SCRIPT<FOO" does not end the script.  We
         play safe and assume that "<SCRIPT<FOO" does end the script
         and "<FOO" is the next tag.  Ditto for "</SCRIPT*" etc. */

      emo_puts (out, end_tag);
      return incorrect_tag (c, IT_OK, IT_DONT_SKIP);
    }
  DEBUG_ASSERT (c != EMI_EOF);
  c = parse_attributes (c);
  return process_tag (c);
}

/* Known tag which is to be dropped */

static int tag_remove (int c)
{
  dangerous_tag ();
  return c;
}

/* "<APPLET>" */

static int tag_applet (int c)
{
  /* TODO: look at attributes, configuration */
  if (cf_html.block_java.v)
    dangerous_tag ();
  else
    output_tag (0);
  return c;
}

/* Process "<SCRIPT> ... </SCRIPT>". */

static int tag_script (int c)
{
  /* Well, we block any script languages, not just JavaScript and
     LiveScript. (TODO: look for the LANGUAGE and TYPE attributes.) */

  if (cf_html.block_javascript.v)
    {
      dangerous_tag ();
      if (!tag_end)
	c = process_cdata (c, "</script>", CDATA_DROP); /* Remove */
    }
  else
    {
      if (!tag_end)
       	 {
      	   int i;
           script_lang = SCRIPT_JAVASCRIPT;
           for (i = 0; i < attr_count; ++i)
	     {
	       struct attr *a = &attr_v[i];
	       if (a->name_len == 8 && lower_cmpn (a->name, "language", 8) == 0)
	         {
	      	   if (a->value_len == 10
		       && lower_cmpn ((char*) a->value, "javascript", 10) == 0)
		     script_lang = SCRIPT_JAVASCRIPT;
	      	   else if (a->value_len == 7
		       && lower_cmpn ((char*) a->value, "jscript", 7) == 0)
		     script_lang = SCRIPT_JAVASCRIPT;
	      	   else if (a->value_len == 8
		       && lower_cmpn ((char*) a->value, "vbscript", 8) == 0)
		     script_lang = SCRIPT_VBSCRIPT;
	      	   else if (a->value_len == 10
		       && lower_cmpn ((char*) a->value, "perlscript", 10) == 0)
		     script_lang = SCRIPT_PERLSCRIPT;
		   else
		     script_lang = SCRIPT_UNKNOWN;
		 }
	     }
	 }

      /* According to most browsers behavior, we do not allow script tag
	 to have embedded termination ( <script ... src="..." /> without
	 </script> tag, though XHTML 1.0 permits it. If we need to change
	 this, we should comment out the next line, or (better) make it
	 DOCTYPE - dependant
      */
      if (document_type != DOCTYPE_XHTML)
        tag_slashed = 0;
      output_tag (0);
      if (!tag_end && !tag_slashed)
	c = process_cdata (c, "</script>", cf_html.script);
    }
  return c;
}

/* Process "<STYLE> ... </STYLE>". */

static int tag_style (int c)
{
  if (cf_html.block_style.v)
    {
      dangerous_tag ();
      if (!tag_end)
	c = process_cdata (c, "</style>", CDATA_DROP);
      return c;
    }

  if (!tag_end)
    {
      /* Protect against "<STYLE TYPE='text/javascript'>". */

      int type_seen = 0;
      int i;
      script_lang = SCRIPT_UNKNOWN;
      for (i = 0; i < attr_count; ++i)
	{
	  struct attr *a = &attr_v[i];
	  if (a->name_len == 4 && lower_cmpn (a->name, "type", 4) == 0)
	    {
	      if (a->value_len == 8
		  && lower_cmpn ((char*) a->value, "text/css", 8) == 0)
		type_seen = 1;
	      else if (a->value_len == 15
		       && lower_cmpn ((char*) a->value, "text/javascript", 15)
			 == 0)
		{
		  /* TODO: other script languages */
		  if (cf_html.block_javascript.v)
		    a->type = AT_DANGEROUS;
		  else
		    type_seen = 1;
		  script_lang = SCRIPT_JAVASCRIPT;
		}
	      else
		a->type = AT_DANGEROUS;	/* Unknown type */
	    }
	}
      if (!type_seen)
	add_attr_value ("type", "text/css");
    }
  output_tag (0);
  if (!tag_end && !tag_slashed)
    c = process_cdata (c, "</style>", cf_html.style);
  return c;
}

/* Flags in the link type table.  Note: Numbers ordered by severity
   (except for LT_STYLESHEET). */

#define LT_OK		0
#define LT_UNKNOWN	1
#define LT_STYLESHEET	2
#define LT_DANGEROUS	3

/* Hash table of REL and REV values for <LINK>.  This include file is
   generated from "html-rel.tab" by maketable. */

#include "html-rel.h"

/* Parse link types. Return LT_OK, LT_UNKNOWN, or LT_DANGEROUS. */

static int parse_link_type (const char *s, size_t n)
{
  char buf[32+1];
  size_t i;
  int rc  = LT_OK;
  int nrc;
  const struct hash_entry *he;

  while (n != 0)
    {
      while (n != 0 && *s == ' ')
	{
	  ++s; --n;
	}
      if (n == 0)
	break;
      i = 0;
      while (i < n && i < sizeof (buf) && s[i] != ' ')
	++i;
      DEBUG_ASSERT (i != 0);
      lower_copy (buf, s, i);
      s += i; n -= i;
      he = find_hash_entry2 (&rel_descr, buf, i);
      nrc = LT_UNKNOWN;
      if (he != NULL)
	nrc = he->flags;
      if (nrc == LT_STYLESHEET)
	nrc = cf_html.block_style.v ? LT_DANGEROUS : LT_OK;
      if (nrc > rc)
	rc = nrc;
    }
  return rc;
}

/* Process "<LINK>". */

static int tag_link (int c)
{
  if (!tag_end)
    {
      int i;
      for (i = 0; i < attr_count; ++i)
	{
	  struct attr *a = &attr_v[i];
	  if (a->name_len == 3 && (lower_cmpn (a->name, "rel", 3) == 0
				   || lower_cmpn (a->name, "rev", 3) == 0))
	    {
	      if (parse_link_type ((char*) a->value, a->value_len) != LT_OK)
		{
		  dangerous_tag ();
		  return c;
		}
	    }
	}
    }
  output_tag (0);
  return c;
}

/* Process <OBJECT>. */

static int tag_object (int c)
{
  if (cf_html.block_object.v)
    dangerous_tag ();
  else
    output_tag (0);
  return c;
}

/* Process <EMBED>. */

static int tag_embed (int c)
{
  if (cf_html.block_embed.v)
    dangerous_tag ();
  else
    output_tag (0);
  return c;
}

/* Return values for <meta> handlers. */

#define MA_OUTPUT	0
#define MA_DANGEROUS	1
#define MA_DROP_SILENT	2
#define MA_UNKNOWN_NAME	3

/* Flags in html-meta.tab and passed to <meta> handlers.  These flags
   indicate whether the entry applies to "name", "http-equiv", or both
   For <meta> handlers, these flags indicate the actual type of the
   META tag. */

#define MF_NAME		0x01	/* "name" */
#define MF_HTTP		0x02	/* "http-equiv" */

/* Keep the <META> tag. */

static int meta_keep (struct attr *v, struct attr *scheme, int flags)
{
  return MA_OUTPUT;
}

/* Benign <META> tag. */

static int meta_default (struct attr *v, struct attr *scheme, int flags)
{
  return MA_OUTPUT;
}

/* Drop <META> tag silently. */

static int meta_drop_silent (struct attr *v, struct attr *scheme, int flags)
{
  return MA_DROP_SILENT;
}

/* Process "<META HTTP-EQUIV='Content-Type' CONTENT=v SCHEME=scheme>".
   Don't care about whether "http-equiv" or "name" is used. */

static int meta_content_type (struct attr *v, struct attr *scheme, int flags)
{
  /* The content type must be "text/html". */

  if (v->value_len < 9 || lower_cmpn ((char*) v->value, "text/html", 9) != 0)
    return MA_DANGEROUS;
  if (v->value_len > 9)
    {
      /* If there's something after "text/html", it must start with a
	 semicolon. */

      if (v->value[9] != ';')
	return MA_DANGEROUS;
      /* TODO: Check charset */
      /* TODO: Reject EBCDIC :-) */
    }
  if (cf_html.drop_meta_content_type.v)
    return MA_DROP_SILENT;
  else {
    charset_type = multibyte_charset ((char*)v->value);
    return MA_OUTPUT;
  }
}

/* Process "<META HTTP-EQUIV='Pragma' CONTENT=v SCHEME=scheme>".
   Don't care about whether "http-equiv" or "name" is used. */

static int meta_pragma (struct attr *v, struct attr *scheme, int flags)
{
  if (v->has_value && check_pragma (v->value, v->value_len) == 0)
    return MA_OUTPUT;
  else
    return MA_DANGEROUS;
}

/* Process "<META HTTP-EQUIV='Refresh' CONTENT=v SCHEME=scheme>".
   Don't care about whether "http-equiv" or "name" is used. */

static int meta_refresh (struct attr *v, struct attr *scheme, int flags)
{
  if (v->has_value && check_refresh (v->value, v->value_len,
				     cf_html.block_javascript.v) == 0)
    return MA_OUTPUT;
  else
    return MA_DANGEROUS;
}

/* Process "<META HTTP-EQUIV='Set-Cookie' CONTENT=v SCHEME=scheme>".
   Don't care about whether "http-equiv" or "name" is used. */

static int meta_set_cookie (struct attr *v, struct attr *scheme, int flags)
{
  int r;
  size_t nlen;

  if (!v->has_value)
    return MA_DANGEROUS;

  r = cookies_parse_set_cookie (v->value);
  if (r != 0)
    return MA_DANGEROUS;
  nlen = cookies_rebuild_length ();

  /* Don't overwrite the old value as cookies.c still has pointers
     into the old value. */

  v->value = fastheap_alloc (&attr_heap, nlen + 1);
  if (nlen > v->value_len)
    v->value_ref = fastheap_alloc (&attr_heap, nlen);
  memset (v->value_ref, REF_AUTO, nlen);
  if (cookies_rebuild (v->value, nlen) != 0)
    ALWAYS_ASSERT (0);
  v->value_len = nlen;
  v->value[nlen] = 0;
  return MA_OUTPUT;
}

/* Hash table of "http-equiv" and "name" values for <META>.  This
   include file is generated from "html-meta.tab" by maketable. */

#include "html-meta.h"

/* Process <META>. */

static int tag_meta (int c)
{
  int i, rc, log, flags = 0;
  struct attr *a_content = NULL;
  struct attr *a_name = NULL;
  struct attr *a_scheme = NULL;
  const struct hash_entry *he;
  int (*handler) (struct attr *, struct attr *, int);

  if (tag_end)
    goto danger;

  /* Collect the attributes.  Reject the tag if there are attributes
     not supported for <META>.  Reject the tag if any attribute is
     given more than once or if the value is missing. */

  for (i = 0; i < attr_count; ++i)
    {
      struct attr *a = &attr_v[i];

      /* As the attributes we're interested in have mostly different
         lengths, there's no point in replacing lower_cmpn() with
         lower_copy() and memcmp(). */

      if (a->name_len == 7 && lower_cmpn (a->name, "content", 7) == 0)
	{
	  if (a_content != NULL || !a->has_value)
	    goto danger;
	  a_content = a;
	}
      else if ((a->name_len == 10
		&& lower_cmpn (a->name, "http-equiv", 10) == 0)
	       || (a->name_len == 4 && lower_cmpn (a->name, "name", 4) == 0))
	{
	  /* Don't accept both "name" and "http-equiv". */

	  if (a_name != NULL || !a->has_value)
	    goto danger;
	  a_name = a;
	  if (a->name_len == 4)
	    flags = MF_NAME;	/* "name" */
	  else
	    flags = MF_HTTP;	/* "http-equiv" */
	}
      else if (a->name_len == 6 && lower_cmpn (a->name, "scheme", 6) == 0)
	{
	  if (a_scheme != NULL || !a->has_value)
	    goto danger;
	  a_scheme = a;
	}
      else if ((a->name_len == 3 && lower_cmpn (a->name, "dir", 3) == 0)
	       || (a->name_len == 4 && lower_cmpn (a->name, "lang", 4) == 0))
	{
	  /* We ignore these attributes.  It doesn't matter if there's
             no value. */
	}
      else
	goto danger;
    }

  /* The "content" and "http-equiv" (or "name") attributes are
     required. */

  if (a_content == NULL || a_name == NULL)
    goto danger;

  /* Remove leading and trailing spaces. */

  trim_value (a_name);
  trim_value (a_content);
  if (a_scheme != NULL)
    trim_value (a_scheme);

  /* Dispatch according to the value of the "http-equiv" or "name"
     attribute. */

  DEBUG_ASSERT (a_name->value_len <= ATTR_VALUE_LIMIT);
  lower_copy ((char*) attr_value, (char*) a_name->value, a_name->value_len);
  he = find_hash_entry2 (&meta_descr, (char*) attr_value, a_name->value_len);
  if (he == NULL)
    {
      if (flags == MF_NAME)
	rc = MA_UNKNOWN_NAME;
      else
	rc = MA_DANGEROUS;
    }
  else if (flags == MF_NAME && !(he->flags & MF_NAME))
    {
      rc = MA_DANGEROUS;
      syslog (LLEV, "NAME used instead of HTTP-EQUIV for <META>");
    }
  else if (flags == MF_HTTP && !(he->flags & MF_HTTP))
    {
      rc = MA_DANGEROUS;
      syslog (LLEV, "HTTP-EQUIV used instead of NAME for <META>");
    }
  else
    {
      handler = he->handler;
      ALWAYS_ASSERT (META_HANDLER (handler)); /* Paranoia */
      rc = handler (a_content, a_scheme, flags);
    }
  if (rc == MA_OUTPUT)
    output_tag (0);
  else if (rc != MA_DROP_SILENT)
    {
      if (rc == MA_UNKNOWN_NAME)
	{
	  log = cf_html.meta_unknown_name.log;
	  remove_tag (&cf_html.meta_unknown_name, "unknown", EMI_EOF);
	}
      else
	{
	  log = cf_html.tag_dangerous.log;
	  dangerous_tag ();
	}
      if (log)
	syslog (LLEV, "<meta> attributes: %s=%.128s %s=%.128s",
		a_name->name, a_name->value,
		a_content->name, a_content->value);
    }
  return c;

danger:
  dangerous_tag ();
  return c;
}

/* Leave the tag unprocessed. */

static int tag_default (int c)
{
  output_tag (0);
  return c;
}

/* HTML levels (currently not used). */

#define HTML_20		0x01	/* HTML 2.0 (RFC 1866) */
#define HTML_32		0x02	/* HTML 3.2 */
#define HTML_40		0x04	/* HTML 4.0 (draft) */
#define HTML_MS		0x08	/* Microsoft extensions */
#define HTML_NS		0x10	/* Netscape extensions */
#define HTML_MO		0x20	/* Mosaic extensions */
#define HTML_UNKNOWN	0x00	/* Unknown origin */

/* Table of HTML tags: `tag_hash'.  This include file is generated
   from "html-tag.tab" by maketable. */

#include "html-tag.h"

static int process_tag (int c)
{
  int (*handler)(int);
  const struct hash_entry *he;

  if (tag_type == HTML_TAG)
    {
      if (!tag_ok)
	{
	  invalid_tag (c);
	  return c;
	}
      else
	{
	  /* Find the handler for this tag. */

	  lower_copy (lower_tag_name, tag_name, tag_name_len);
	  he = find_hash_entry2 (&tag_descr, lower_tag_name, tag_name_len);
	  if (he == NULL)
	    {
	      unknown_tag ();	/* Unknown tag */
	      return c;
	    }
	  else
	    {
	      /* Known tag which is to be processed. */

	      handler = he->handler;
	      ALWAYS_ASSERT (TAG_HANDLER (handler)); /* Paranoia */
	      return handler (c);
	    }
	}
    }
  return c;
}

/* ================================= HTML ================================== */

/* Filter HTML while copying from IN to OUT.  Limit the number of
   characters read to SIZE, unless SIZE is -1 (see emi_limit() for
   this special value).  TODO: Limit the size arbitrarily if SIZE is
   -1. */

void html_copy (EMO_FILE *out0, EMI_FILE *in0, long size)
{
  int c;

  out = out0; in = in0;
  in_start = emi_amount (in, 1);
  out_start = emo_amount (out, 1);
  emi_limit (in, size);
  fastheap_init (&attr_heap, 8192);
  /* TODO: fastheap_add_static() */
  attr_value = xmalloc (ATTR_VALUE_LIMIT + 1);
  attr_value_ref = xmalloc (ATTR_VALUE_LIMIT);

  c = slow_getc ();
  while (c != EMI_EOF && !emo_error (out))
    {
      /* Make the most common case fast.  No error checking in this
         loop (see outer loop). */

      while (c != EMI_EOF && !ESCAPE (c) && (charset_type == CHARSET_GENERIC))
	{
	  fast_putc (c);
	  c = fast_getc ();
	}

      switch (c)
	{
	case EMI_EOF:
	  break;
	case '<':
	  c = parse_tag ();
	  c = process_tag (c);
	  fastheap_reset (&attr_heap);
	  break;
	case '>':
	  output_char (c);
	  c = fast_getc ();
	  break;
	case '&':
	  c = parse_ref ();
	  process_ref ();
	  break;
	default:
	  if ((c >= 0x20) && (c < 0x7f)) {
	    fast_putc (c);
	    c = fast_getc ();
	    break;
          }
	  if ((c <0x20) || (c == 0x7f) || (charset_type == CHARSET_GENERIC)) {
	    output_char (c);
	    c = fast_getc ();
	    break;
	  } else if (charset_type == CHARSET_UTF8) {
/*
 *	  From RFC2278:
 *	In UTF-8, characters are encoded using sequences of 1 to 6 octets.
 *  	The only octet of a "sequence" of one has the higher-order bit set to
 *  	0, the remaining 7 bits being used to encode the character value. In
 *  	a sequence of n octets, n>1, the initial octet has the n higher-order
 *  	bits set to 1, followed by a bit set to 0.  The remaining bit(s) of
 *  	that octet contain bits from the value of the character to be
 *	encoded.  The following octet(s) all have the higher-order bit set to
 *  	1 and the following bit set to 0, leaving 6 bits in each to contain
 *  	bits from the character to be encoded.
 */
	    int nc;
	    if (!(c & 0x40) && (c > 0xfd)) {
		syslog(LLEV,"securityalert: bad unicode character");
		return;
	    }
	    for (nc = c; nc & 0x80 ; nc <<= 1) {
		if ((nc != c) && !((c & 0x80) && !(c & 0x40))) {
			syslog(LLEV,"securityalert: bad unicode character");
			return;
		}
		fast_putc (c);
		c = fast_getc ();
		if (c == EMI_EOF)
			break;
	    }
	  } else {
	    syslog(LLEV,"fwtksyserr: charset class not implemented");
	    return;
          }	
	}
    }
  if (emo_error (out))
    syslog (LLEV, "write failed: %s", strerror(errno));
}

/* ============================= CONFIGURATION ============================= */

/* Initialize `cf_html' to the default configuration. */

void html_default_config (void)
{
  cf_html.block_java.v = 1;	  cf_html.block_java.force = 0;
  cf_html.block_object.v = 1;	  cf_html.block_object.force = 0;
  cf_html.block_embed.v = 1;	  cf_html.block_embed.force = 0;
  cf_html.block_javascript.v = 1; cf_html.block_javascript.force = 0;
  cf_html.block_style.v = 1;	  cf_html.block_style.force = 0;
  cf_html.use_callback = 0;
  cf_html.log_incorrect_tags = 0;
  cf_html.log_key = 0;
  cf_html.log_script_macros = 0;
  cf_html.attr_value_limit = 1024; /* RFC 1866 */
  cf_html.drop_meta_content_type.v = 0;
  cf_html.drop_meta_content_type.force = 0;
  cf_html.tag_invalid.output = REJ_ESCAPE;
  cf_html.tag_invalid.log = 0;
  cf_html.tag_dangerous.output = REJ_PREFIX;
  cf_html.tag_dangerous.log = 0;
  cf_html.tag_unknown.output = REJ_PREFIX;
  cf_html.tag_unknown.log = 0;
  cf_html.attr_dangerous.output = REJ_PREFIX;
  cf_html.attr_dangerous.log = 0;
  cf_html.attr_on.output = REJ_PREFIX;
  cf_html.attr_on.log = 1;
  cf_html.attr_unknown.output = REJ_PREFIX;
  cf_html.attr_unknown.log = 0;
  cf_html.attr_novalue.output = REJ_COPY;
  cf_html.attr_novalue.log = 0;
  cf_html.attr_alphanumeric.output = REJ_PREFIX;
  cf_html.attr_alphanumeric.log = 0;
  cf_html.ref_unknown.output = REJ_ESCAPE;
  cf_html.ref_unknown.log = 0;
  cf_html.meta_unknown_name.output = REJ_PREFIX;
  cf_html.meta_unknown_name.log = 0;
  cf_html.script = CDATA_HTML;
  cf_html.style = CDATA_HTML;
}
