##################################################
#
# cgi.tcl - routines for writing CGI scripts in Tcl
# Author: Don Libes <libes@nist.gov>, January '95
#
# These routines implement the code described in the paper
# "Writing CGI scripts in Tcl" which appeared in the Tcl '96 conference.
# Please read the paper before using this code.  The paper is:
# http://www.cme.nist.gov/msid/pubs/libes96c.ps
#
##################################################

##################################################
# http header support
##################################################

proc cgi_http_head {args} {
    global _cgi env errorInfo

    if {[info exists _cgi(http_head_done)]} return

    set _cgi(http_head_in_progress) 1

    if {0 == [llength $args]} {
	cgi_content_type
    } else {
	if {[catch {uplevel 1 [lindex $args 0]} errMsg]} {
	    set savedInfo $errorInfo
	    cgi_content_type
	}
    }
    cgi_puts ""

    unset _cgi(http_head_in_progress)
    set _cgi(http_head_done) 1

    if {[info exists savedInfo]} {
	error $errMsg $savedInfo
    }
}

# avoid generating http head if not in CGI environment
# to allow generation of pure HTML files
proc _cgi_http_head_implicit {} {
    global env

    if {[info exists env(REQUEST_METHOD)]} cgi_http_head
}

proc cgi_status {num str} {
    global _cgi

    if {[info exists _cgi(http_status_done)]} return
    set _cgi(http_status_done) 1
    puts "Status: $num $str"
}

# If these are called manually, they automatically generate the extra newline

proc cgi_content_type {args} {
    global _cgi

    if {0==[llength $args]} {
	set t text/html
    } else {
	set t [lindex $args 0]
	if {[regexp ^multipart/ $t]} {
	    set _cgi(multipart) 1
	}
    }

    if {[info exists _cgi(http_head_in_progress)]} {
	cgi_puts "Content-type: $t"
    } else {
	cgi_http_head "cgi_content_type $t"
    }
}

proc cgi_redirect {t} {
    global _cgi

    if {[info exists _cgi(http_head_in_progress)]} {
	cgi_status 302 Redirected
	cgi_puts "Uri: $t"
	cgi_puts "Location: $t"
    } else {
	cgi_http_head {
	    cgi_redirect $t
	}
    }
}

# deprecated, use cgi_redirect
proc cgi_location {t} {
    global _cgi

    if {[info exists _cgi(http_head_in_progress)]} {
	cgi_puts "Location: $t"
    } else {
	cgi_http_head "cgi_location $t"
    }
}

proc cgi_target {t} {
    global _cgi

    if {![info exists _cgi(http_head_in_progress)]} {
	error "cgi_target must be set from within cgi_http_head."
    }
    cgi_puts "Window-target: $t"
}

# Make client retrieve url in this many seconds ("client pull").
# With no 2nd arg, current url is retrieved.
proc cgi_refresh {seconds {url ""}} {
    global _cgi

    if {![info exists _cgi(http_head_in_progress)]} {
	error "cgi_refresh must be set from within cgi_http_head.  Try using cgi_http_equiv instead."
    }
    cgi_put "Refresh: $seconds"

    if {0==[string compare $url ""]} {
	cgi_put "; $url"
    }
    cgi_puts ""
}

# Example: cgi_pragma no-cache
proc cgi_pragma {arg} {
    global _cgi

    if {![info exists _cgi(http_head_in_progress)]} {
	error "cgi_pragma must be set from within cgi_http_head."
    }
    cgi_puts "Pragma: $arg"
}

##################################################
# support for debugging or other crucial things we need immediately
##################################################

proc cgi_comment	{args}	{}	;# need this asap

proc cgi_html_comment	{args}	{
    regsub -all {>} $args {\&gt;} args
    cgi_put "<!--[_cgi_list_to_string $args] -->"
}

set _cgi(debug) -off
proc cgi_debug {args} {
    global _cgi

    set old $_cgi(debug)
    set arg [lindex $args 0]
    if {$arg == "-on"} {
	set _cgi(debug) -on
	set args [lrange $args 1 end]
    } elseif {$arg == "-off"} {
	set _cgi(debug) -off
	set args [lrange $args 1 end]
    } elseif {[regexp "^-t" $arg]} {
	set temp 1
	set _cgi(debug) -on
	set args [lrange $args 1 end]
    } elseif {[regexp "^-noprint$" $arg]} {
	set noprint 1
	set args [lrange $args 1 end]
    }

    set arg [lindex $args 0]
    if {$arg == "--"} {
	set args [lrange $args 1 end]
    }

    if {[llength $args]} {
	if {$_cgi(debug) == "-on"} {

	    _cgi_close_tag
	    # force http head and open html, head, body
	    catch {
		if {[info exists noprint]} {
		    uplevel 1 [lindex $args 0]
		} else {
		    cgi_html {
			cgi_head {
			    cgi_title "debugging before complete HTML head"
			}
			# force body open and leave open
			_cgi_body_start
			uplevel 1 [lindex $args 0]
			# bop back out to catch, so we don't close body
			error "ignore"
		    }
		}
	    }
	}
    }

    if {[info exists temp]} {
	set _cgi(debug) $old
    }
    return $old
}

proc cgi_uid_check {user} {
    global env

    # leave in so old scripts don't blow up
    if {[regexp "^-off$" $user]} return

    if {[info exists env(USER)]} {
	set whoami $env(USER)
    } elseif {0==[catch {exec whoami} whoami]} {
	# "who am i" on some Linux hosts returns "" so try whoami first
    } elseif {0==[catch {exec who am i} whoami]} {
	# skip over "host!"
	regexp "(.*!)?(\[^ \t]*)" $whoami dummy dummy whoami
    } elseif {0==[catch {package require registry}]} {
	set whoami [registry get HKEY_LOCAL_MACHINE\\Network\\Logon username]
    } else {
	set whoami $user  ;# give up and let go
    }
    if {$whoami != "$user"} {
	error "Warning: This CGI script expects to run with uid \"$user\".  However, this script is running as \"$whoami\"."
    }
}

# print out elements of an array
# like Tcl's parray, but formatted for browser
proc cgi_parray {a {pattern *}} {
    upvar 1 $a array
    if {![array exists array]} {
	error "\"$a\" isn't an array"
    }

    set maxl 0
    foreach name [lsort [array names array $pattern]] {
	if {[string length $name] > $maxl} {
	    set maxl [string length $name]
	}
    }
    cgi_preformatted {
	set maxl [expr {$maxl + [string length $a] + 2}]
	foreach name [lsort [array names array $pattern]] {
	    set nameString [format %s(%s) $a $name]
	    cgi_puts [cgi_quote_html [format "%-*s = %s" $maxl $nameString $array($name)]]
	}
    }
}

proc cgi_eval {cmd} {
    global env _cgi

    # put cmd somewhere that uplevel can find it
    set _cgi(body) $cmd

    uplevel 1 {
	global env _cgi errorInfo

	if {1==[catch $_cgi(body) errMsg]} {
	    # error occurred, handle it
	    set _cgi(errorInfo) $errorInfo

	    if {![info exists env(REQUEST_METHOD)]} {
		puts stderr $_cgi(errorInfo)
		return
	    }
	    # the following code is all to force browsers into a state
	    # such that diagnostics can be reliably shown

	    # close irrelevant things
	    _cgi_close_procs
	    # force http head and open html, head, body
	    cgi_html {
		cgi_body {
		    if {[info exists _cgi(client_error)]} {
			cgi_h3 "Client Error"
			cgi_p "$errMsg  Report this to your system administrator or browser vendor."
		    } else {
			cgi_h3 "An internal error was detected in the service\
				software.  The diagnostics are being emailed to\
				the service system administrator ($_cgi(admin_email))."

			if {$_cgi(debug) == "-on"} {
			    cgi_puts "Heck, since you're debugging, I'll show you the\
				    errors right here:"
			    # suppress formatting
			    cgi_preformatted {
				cgi_puts [cgi_quote_html $_cgi(errorInfo)]
			    }
			} else {
			    cgi_mail_start $_cgi(admin_email)
			    cgi_mail_add "Subject: [cgi_name] CGI problem"
			    cgi_mail_add
			    cgi_mail_add "CGI environment:"
			    cgi_mail_add "REQUEST_METHOD: $env(REQUEST_METHOD)"
			    cgi_mail_add "SCRIPT_NAME: $env(SCRIPT_NAME)"
			    # this next few things probably don't need
			    # a catch but I'm not positive
			    catch {cgi_mail_add "HTTP_USER_AGENT: $env(HTTP_USER_AGENT)"}
			    catch {cgi_mail_add "HTTP_REFERER: $env(HTTP_REFERER)"}
			    catch {cgi_mail_add "HTTP_HOST: $env(HTTP_HOST)"}
			    catch {cgi_mail_add "REMOTE_HOST: $env(REMOTE_HOST)"}
			    catch {cgi_mail_add "REMOTE_ADDR: $env(REMOTE_ADDR)"}
			    cgi_mail_add "cgi.tcl version: 1.3.0"
			    cgi_mail_add "input:"
			    catch {cgi_mail_add $_cgi(input)}
			    cgi_mail_add "cookie:"
			    catch {cgi_mail_add $env(HTTP_COOKIE)}
			    cgi_mail_add "errorInfo:"
			    cgi_mail_add "$_cgi(errorInfo)"
			    cgi_mail_end
			}
		    }
		} ;# end cgi_body
	    } ;# end cgi_html
	} ;# end catch
    } ;# end uplevel
}

# return true if cgi_eval caught an error
proc cgi_error_occurred {} {
    global _cgi

    return [info exists _cgi(errorInfo)]
}

##################################################
# CGI URL creation
##################################################

# declare location of root of CGI files
# this allows all CGI references to be relative in the source
# making it easy to move everything in the future
# If you have multiple roots, just don't call this.
proc cgi_root {args} {
    global _cgi

    if {[llength $args]} {
	set _cgi(root) [lindex $args 0]
    } else {
	set _cgi(root)
    }
}

# make a URL for a CGI script
proc cgi_cgi {args} {
    global _cgi

    set root $_cgi(root)
    if {0!=[string compare $root ""]} {
	if {![regexp "/$" $root]} {
		append root "/"
	}
    }
		
    set suffix [cgi_suffix]
    set arg [lindex $args 0]
    if {0==[string compare $arg "-suffix"]} {
	set suffix [lindex $args 1]
	set args [lrange $args 2 end]
    }

    if {[llength $args]==1} {
	return $root[lindex $args 0]$suffix
    } else {
	return $root[lindex $args 0]$suffix?[join [lrange $args 1 end] &]
    }
}

proc cgi_suffix {args} {
    global _cgi
    if {[llength $args] > 0} {
	set _cgi(suffix) [lindex $args 0]
    }
    if {![info exists _cgi(suffix)]} {
	return .cgi
    } else {
	return $_cgi(suffix)
    }
}

proc cgi_cgi_set {variable value} {
    regsub -all {%}  $value "%25" value
    regsub -all {&}  $value "%26" value
    regsub -all {\+} $value "%2b" value
    regsub -all { }  $value "+"   value
    regsub -all {=}  $value "%3d" value
    regsub -all {#}  $value "%23" value
    regsub -all {/}  $value "%2f" value   ;# Added...
    return $variable=$value
}

##################################################
# URL dictionary support
##################################################

proc cgi_link {args} {
    global _cgi_link _cgi_link_url

    set tag [lindex $args 0]
    if {[llength $args] >= 3} {
	set _cgi_link_url($tag) [lrange $args 2 end]
	set _cgi_link($tag) [eval cgi_url [lrange $args 1 end]]
    } elseif {[llength $args] == 2} {
	return [eval cgi_url [lindex $args end] $_cgi_link_url($tag)]
    }

    return $_cgi_link($tag)
}

# same as above but for images
# note: uses different namespace
proc cgi_imglink {args} {
    global _cgi_imglink

    set tag [lindex $args 0]
    if {[llength $args] >= 2} {
	set _cgi_imglink($tag) [eval cgi_img [lrange $args 1 end]]
    }
    return $_cgi_imglink($tag)
}

##################################################
# hyperlink support
##################################################

# construct a hyperlink labeled "display"
# last arg is the link destination
# any other args are passed through into <a> display
proc cgi_url {display args} {
    set buf "<a href=\"[lindex $args 0]\""
    foreach a [lrange $args 1 end] {
	if {[regexp "^(target|onClick|onMouseOver|onMouseOut)=(.*)" $a dummy attr str]} {
	    append buf " $attr=\"$str\""
	} else {
	    append buf " $a"
	}
    }
    return "$buf>$display</a>"
}

# generate an image reference (<img ...>)
# first arg is image url
# other args are passed through into <img> tag
proc cgi_img {args} {
    set buf "<img src=\"[lindex $args 0]\""
    foreach a [lrange $args 1 end] {
	if {[regexp "^(alt|width|height|lowsrc|usemap)=(.*)" $a dummy attr str]} {
	    append buf " $attr=[cgi_dquote_html $str]"
	} elseif {[regexp "^onError" $a dummy str]} {
	    append buf " onError=\"$str\""
	} else {
	    append buf " $a"
	}
    }
    return "$buf>"
}

# names an anchor so that it can be linked to
proc cgi_anchor_name {name} {
    return "<a name=\"$name\">"
}

proc cgi_base {args} {
    cgi_put "<base"
    foreach a $args {
	if {[regexp "^href=(.*)" $a dummy str]} {
	    cgi_put " href=[cgi_dquote_html $str]"
	} elseif {[regexp "^target=(.*)" $a dummy str]} {
	    cgi_put " target=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_puts ">"
}

##################################################
# quoting support
##################################################

if {[info tclversion] >= 8.2} {
    proc cgi_unquote_input buf {
	# rewrite "+" back to space
	# protect \ from quoting another \ and throwing off other things
	# replace line delimiters with newlines
	set buf [string map -nocase [list + { } "\\" "\\\\" %0d%0a \n] $buf]

	# prepare to process all %-escapes
	regsub -all -nocase {%([a-f0-9][a-f0-9])} $buf {\\u00\1} buf

	# process \u unicode mapped chars
	encoding convertfrom [subst -novar -nocommand $buf]
    }
} elseif {[info tclversion] >= 8.1} {
    proc cgi_unquote_input buf {
	# rewrite "+" back to space
	regsub -all {\+} $buf { } buf
	# protect \ from quoting another \ and throwing off other things
	regsub -all {\\} $buf {\\\\} buf

	# replace line delimiters with newlines
	regsub -all -nocase "%0d%0a" $buf "\n" buf

	# prepare to process all %-escapes
	regsub -all -nocase {%([a-f0-9][a-f0-9])} $buf {\\u00\1} buf
	# process \u unicode mapped chars
	return [subst -novar -nocommand $buf]
    }
} else {
    proc cgi_unquote_input {buf} {
	# rewrite "+" back to space
	regsub -all {\+} $buf { } buf
	# protect \ from quoting another \ and throwing off other things first
	# protect $ from doing variable expansion
	# protect [ from doing evaluation
	# protect " from terminating string
	regsub -all {([\\["$])} $buf {\\\1} buf
	
	# replace line delimiters with newlines
	regsub -all -nocase "%0d%0a" $buf "\n" buf
	# Mosaic sends just %0A.  This is handled in the next command.

	# prepare to process all %-escapes 
	regsub -all -nocase {%([a-f0-9][a-f0-9])} $buf {[format %c 0x\1]} buf
	# process %-escapes and undo all protection
	eval return \"$buf\"
    }
}

# return string but with html-special characters escaped,
# necessary if you want to send unknown text to an html-formatted page.
proc cgi_quote_html {s} {
    regsub -all {&}	$s {\&amp;}	s	;# must be first!
    regsub -all {"}	$s {\&quot;}	s
    regsub -all {<}	$s {\&lt;}	s
    regsub -all {>}	$s {\&gt;}	s
    return $s
}

proc cgi_dquote_html {s} {
    return \"[cgi_quote_html $s]\"
}

# return string quoted appropriately to appear in a url
proc cgi_quote_url {in} {
    regsub -all {%}  $in "%25" in
    regsub -all { }  $in "%20" in
    regsub -all {"}  $in "%22" in
    regsub -all {\?} $in "%3f" in
    return $in
}

##################################################
# short or single paragraph support
##################################################

proc cgi_br {args} {
    cgi_put "<br"
    if {[llength $args]} {
	cgi_put "[_cgi_list_to_string $args]"
    }
    cgi_put ">"
}

# generate cgi_h1 and others
for {set _cgi(tmp) 1} {$_cgi(tmp)<8} {incr _cgi(tmp)} {
    proc cgi_h$_cgi(tmp) {{args}} "eval cgi_h $_cgi(tmp) \$args"
}
proc cgi_h {num args} {
    cgi_put "<h$num"
    if {[llength $args] > 1} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
	set args [lrange $args end end]
    }
    cgi_put ">[lindex $args 0]</h$num>"
}

proc cgi_p {args} {
    cgi_put "<p"
    if {[llength $args] > 1} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
	set args [lrange $args end end]
    }
    cgi_put ">[lindex $args 0]</p>"
}

proc cgi_address      {s} {cgi_put <address>$s</address>}
proc cgi_blockquote   {s} {cgi_puts <blockquote>$s</blockquote>}

##################################################
# long or multiple paragraph support
##################################################

# Shorthand for <div align=center>.  We used to use <center> tags but that
# is now officially unsupported.
proc cgi_center	{cmd}	{
    uplevel 1 "cgi_division align=center [list $cmd]"
}

proc cgi_division {args} {
    cgi_put "<div"
    _cgi_close_proc_push "cgi_put </div>"

    if {[llength $args]} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">"
    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

proc cgi_preformatted {args} {
    cgi_put "<pre"
    _cgi_close_proc_push "cgi_put </pre>"

    if {[llength $args]} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">"
    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

##################################################
# list support
##################################################

proc cgi_li {args} {
    cgi_put <li
    if {[llength $args] > 1} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">[lindex $args end]"
}

proc cgi_number_list {args} {
    cgi_put "<ol"
    _cgi_close_proc_push "cgi_put </ol>"

    if {[llength $args] > 1} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">"
    uplevel 1 [lindex $args end]

    _cgi_close_proc
}

proc cgi_bullet_list {args} {
    cgi_put "<ul"
    _cgi_close_proc_push "cgi_put </ul>"

    if {[llength $args] > 1} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">"
    uplevel 1 [lindex $args end]

    _cgi_close_proc
}

# Following two are normally used from within definition lists
# but are actually paragraph types on their own.
proc cgi_term            {s} {cgi_put <dt>$s}
proc cgi_term_definition {s} {cgi_put <dd>$s}

proc cgi_definition_list {cmd} {
    cgi_put "<dl>"
    _cgi_close_proc_push "cgi_put </dl>"

    uplevel 1 $cmd
    _cgi_close_proc
}

proc cgi_menu_list {cmd} {
    cgi_put "<menu>"
    _cgi_close_proc_push "cgi_put </menu>"

    uplevel 1 $cmd
    _cgi_close_proc
}
proc cgi_directory_list {cmd} {
    cgi_put "<dir>"
    _cgi_close_proc_push "cgi_put </dir>"

    uplevel 1 $cmd
    _cgi_close_proc
}

##################################################
# text support
##################################################

proc cgi_put	    {s} {cgi_puts -nonewline $s}

# some common special characters
proc cgi_lt	     {}  {return "&lt;"}
proc cgi_gt	     {}  {return "&gt;"}
proc cgi_amp	     {}  {return "&amp;"}
proc cgi_quote	     {}  {return "&quot;"}
proc cgi_enspace     {}  {return "&ensp;"}
proc cgi_emspace     {}  {return "&emsp;"}
proc cgi_nbspace     {}  {return "&#160;"} ;# nonbreaking space
proc cgi_tm	     {}  {return "&#174;"} ;# registered trademark
proc cgi_copyright   {}  {return "&#169;"}
proc cgi_isochar     {n} {return "&#$n;"}
proc cgi_breakable   {}  {return "<wbr>"}

proc cgi_unbreakable_string {s} {return "<nobr>$s</nobr>"}
proc cgi_unbreakable {cmd} {
    cgi_put "<nobr>"
    _cgi_close_proc_push "cgi_put </nobr>"
    uplevel 1 $cmd
    _cgi_close_proc
}

proc cgi_nl          {args} {
    set buf "<br"
    if {[llength $args]} {
	append buf "[_cgi_list_to_string $args]"
    }
    return "$buf>"
}

proc cgi_bold	    {s} {return "<b>$s</b>"}
proc cgi_italic     {s} {return "<i>$s</i>"}
proc cgi_underline  {s} {return "<u>$s</u>"}
proc cgi_strikeout  {s} {return "<s>$s</s>"}
proc cgi_subscript  {s} {return "<sub>$s</sub>"}
proc cgi_superscript {s} {return "<sup>$s</sup>"}
proc cgi_typewriter {s} {return "<tt>$s</tt>"}
proc cgi_blink	    {s} {return "<blink>$s</blink>"}
proc cgi_emphasis   {s} {return "<em>$s</em>"}
proc cgi_strong	    {s} {return "<strong>$s</strong>"}
proc cgi_cite	    {s} {return "<cite>$s</cite>"}
proc cgi_sample     {s} {return "<samp>$s</samp>"}
proc cgi_keyboard   {s} {return "<kbd>$s</kbd>"}
proc cgi_variable   {s} {return "<var>$s</var>"}
proc cgi_definition {s} {return "<dfn>$s</dfn>"}
proc cgi_big	    {s} {return "<big>$s</big>"}
proc cgi_small	    {s} {return "<small>$s</small>"}

proc cgi_basefont   {size} {cgi_put "<basefont size=$size>"}

proc cgi_font {args} {
    set buf "<font"
    foreach a [lrange $args 0 [expr [llength $args]-2]] {
	if {[regexp "^color=(.*)" $a dummy str]} {
	    append buf " color=\"$str\""
	} else {
	    append buf " $a"
	}
    }
    return "$buf>[lindex $args end]</font>"
}

# take a cgi func and have it return what would normally print
# This command is reentrant (that's why it's so complex).
proc cgi_buffer {cmd} {
    global _cgi

    if {0==[info exists _cgi(returnIndex)]} {
	set _cgi(returnIndex) 0
    }

    rename cgi_puts cgi_puts$_cgi(returnIndex)
    incr _cgi(returnIndex)
    set _cgi(return[set _cgi(returnIndex)]) ""

    proc cgi_puts args {
	global _cgi
	upvar #0 _cgi(return[set _cgi(returnIndex)]) buffer

	append buffer [lindex $args end]
	if {[llength $args] == 1} {
	    append buffer $_cgi(buffer_nl)
	}
    }

    # must restore things before allowing the eval to fail
    # so catch here and rethrow later
    if {[catch {uplevel 1 $cmd} errMsg]} {
	global errorInfo
	set savedInfo $errorInfo
    }

    # not necessary to put remainder of code in close_proc_push since it's
    # all buffered anyway and hasn't yet put browser into a funky state.

    set buffer $_cgi(return[set _cgi(returnIndex)])

    incr _cgi(returnIndex) -1
    rename cgi_puts ""
    rename cgi_puts$_cgi(returnIndex) cgi_puts

    if {[info exists savedInfo]} {
	error $errMsg $savedInfo
    }
    return $buffer
}

set _cgi(buffer_nl) "\n"
proc cgi_buffer_nl {nl} {
    global _cgi

    set old $_cgi(buffer_nl)
    set _cgi(buffer_nl) $nl
    return $old
}

##################################################
# html and tags that can appear in html top-level
##################################################

proc cgi_html {html} {
    _cgi_html_start
    uplevel 1 $html
    _cgi_html_end
}

proc _cgi_html_start {} {
    global _cgi
    
    if {[info exists _cgi(html_in_progress)]} return
    _cgi_http_head_implicit

    set _cgi(html_in_progress) 1
    cgi_doctype
    cgi_puts "<html>"
}

proc _cgi_html_end {} {
    global _cgi
    unset _cgi(html_in_progress)
    set _cgi(html_done) 1
    cgi_puts "</html>"
}

# force closure of all tags and exit without going through normal returns.
# Very useful if you want to call exit from a deeply stacked CGI script
# and still have the HTML be correct.
proc cgi_exit {} {
    _cgi_close_procs
    cgi_html {cgi_body {}}
    exit
}

##################################################
# head support
##################################################

proc cgi_head {{head {}}} {
    global _cgi

    if {[info exists _cgi(head_done)]} {
	return
    }

    # allow us to be recalled so that we can display errors
    if {0 == [info exists _cgi(head_in_progress)]} {
	_cgi_http_head_implicit
	set _cgi(head_in_progress) 1
	cgi_puts "<head>"
    }

    # prevent cgi_html (during error handling) from generating html tags
    set _cgi(html_in_progress) 1
    # don't actually generate html tags since there's nothing to clean
    # them up

    if {0 == [string length $head]} {
	if {[catch {cgi_title}]} {
	    set head "cgi_title untitled"
	}
    }
    uplevel 1 $head
    if {![info exists _cgi(head_suppress_tag)]} {
	cgi_puts "</head>"
    } else {
	unset _cgi(head_suppress_tag)
    }

    set _cgi(head_done) 1

    # debugging can unset this in the uplevel above
    catch {unset _cgi(head_in_progress)}
}

# with one arg: set, print, and return title
# with no args: return title
proc cgi_title {args} {
    global _cgi

    set title [lindex $args 0]

    if {[llength $args]} {
	_cgi_http_head_implicit

	# we could just generate <head></head> tags, but head-level commands
	# might follow so just suppress the head tags entirely
	if {![info exists _cgi(head_in_progress)]} {
	    set _cgi(head_in_progress) 1
	    set _cgi(head_suppress_tag) 1
	}

	set _cgi(title) $title
	cgi_puts "<title>$title</title>"
    }
    return $_cgi(title)
}

# This tag can only be called from with cgi_head.
# example: cgi_http_equiv Refresh 1
# There's really no reason to call this since it can be done directly
# from cgi_http_head.
proc cgi_http_equiv {type contents} {
    _cgi_http_head_implicit
    cgi_puts "<meta http-equiv=\"$type\" content=[cgi_dquote_html $contents]>"
}

# Do whatever you want with meta tags.
# Example: <meta name="author" content="Don Libes">
proc cgi_meta {args} {
    cgi_put "<meta"
    foreach a $args {
	if {[regexp "^(name|content|http-equiv)=(.*)" $a dummy attr str]} {
	    cgi_put " $attr=[cgi_dquote_html $str]"
	} else {
	    cgi_put " $a"
	}
    }
    cgi_puts ">"
}

proc cgi_relationship {rel href args} {
    cgi_puts "<link rel=$rel href=\"$href\""
    foreach a $args {
	if {[regexp "^title=(.*)" $a dummy str]} {
	    cgi_put " title=[cgi_dquote_html $str]"
	} elseif {[regexp "^type=(.*)" $a dummy str]} {
	    cgi_put " type=[cgi_dquote_html $str]"
	} else {
	    cgi_put " $a"
	}
    }
    cgi_puts ">"
}

proc cgi_name {args} {
    global _cgi

    if {[llength $args]} {
	set _cgi(name) [lindex $args 0]
    }
    return $_cgi(name)
}

##################################################
# body and other top-level support
##################################################

proc cgi_body {args} {
    global errorInfo _cgi

    # allow user to "return" from the body without missing _cgi_body_end
    if {1==[catch {
	eval _cgi_body_start [lrange $args 0 [expr [llength $args]-2]]
	uplevel 1 [lindex $args end]
    } errMsg]} {
	set savedInfo $errorInfo
	error $errMsg $savedInfo
    }
    _cgi_body_end
}

proc _cgi_body_start {args} {
    global _cgi
    if {[info exists _cgi(body_in_progress)]} return

    cgi_head

    set _cgi(body_in_progress) 1

    cgi_put "<body"
    foreach a "$args $_cgi(body_args)" {
	if {[regexp "^(background|bgcolor|text|link|vlink|alink|onLoad|onUnload)=(.*)" $a dummy attr str]} {
	    cgi_put " $attr=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_puts ">"

    cgi_debug {
	global env
	catch {cgi_puts "Input: <pre>$_cgi(input)</pre>"}
	catch {cgi_puts "Cookie: <pre>$env(HTTP_COOKIE)</pre>"}
    }

    if {![info exists _cgi(errorInfo)]} {
	uplevel 2 app_body_start
    }
}

proc _cgi_body_end {} {
    global _cgi
    if {![info exists _cgi(errorInfo)]} {
	uplevel 2 app_body_end
    }
    unset _cgi(body_in_progress)
    cgi_puts "</body>"

    if {[info exists _cgi(multipart)]} {
	unset _cgi(http_head_done)
	catch {unset _cgi(http_status_done)}
	unset _cgi(head_done)
	catch {unset _cgi(head_suppress_tag)}
    }
}

proc cgi_body_args {args} {
    global _cgi

    set _cgi(body_args) $args
}

proc cgi_script {args} {
    cgi_puts "<script[_cgi_lrange $args 0 [expr [llength $args]-2]]>"
    _cgi_close_proc_push "cgi_puts </script>"

    uplevel 1 [lindex $args end]

    _cgi_close_proc
}

proc cgi_javascript {args} {
    cgi_puts "<script[_cgi_lrange $args 0 [expr [llength $args]-2]]>"
    cgi_puts "<!--- Hide script from browsers that don't understand JavaScript"
    _cgi_close_proc_push {cgi_puts "// End hiding -->\n</script>"}

    uplevel 1 [lindex $args end]

    _cgi_close_proc
}

proc cgi_noscript {args} {
    cgi_puts "<noscript[_cgi_lrange $args 0 [expr [llength $args]-2]]>"
    _cgi_close_proc_push {puts "</noscript>"}

    uplevel 1 [lindex $args end]

    _cgi_close_proc
}

proc cgi_applet {args} {
    cgi_puts "<applet[_cgi_lrange $args 0 [expr [llength $args]-2]]>"
    _cgi_close_proc_push "cgi_puts </applet>"

    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

proc cgi_param {nameval} {
    regexp "(\[^=]*)(=?)(.*)" $nameval dummy name q value

    if {$q != "="} {
	set value ""
    }
    cgi_puts "<param name=\"$name\" value=[cgi_dquote_html $value]>"
}

# record any proc's that must be called prior to displaying an error
proc _cgi_close_proc_push {p} {
    global _cgi
    if {![info exists _cgi(close_proc)]} {
	set _cgi(close_proc) ""
    }
    set _cgi(close_proc) "$p; $_cgi(close_proc)"
}

proc _cgi_close_proc_pop {} {
    global _cgi
    regexp "^(\[^;]*);(.*)" $_cgi(close_proc) dummy lastproc _cgi(close_proc)
    return $lastproc
}

# generic proc to close whatever is on the top of the stack
proc _cgi_close_proc {} {
    eval [_cgi_close_proc_pop]
}

proc _cgi_close_procs {} {
    global _cgi

    _cgi_close_tag
    if {[info exists _cgi(close_proc)]} {
	uplevel #0 $_cgi(close_proc)
    }
}

proc _cgi_close_tag {} {
    global _cgi

    if {[info exists _cgi(tag_in_progress)]} {
	cgi_put ">"
	unset _cgi(tag_in_progress)
    }
}

##################################################
# hr support
##################################################

proc cgi_hr {args} {
    set buf "<hr"
    foreach a $args {
	if {[regexp "^width=(.*)" $a dummy str]} {
	    append buf " width=\"$str\""
	} else {
	    append buf " $a"
	}
    }
    cgi_put "$buf>"
}

##################################################
# form & isindex
##################################################

proc cgi_form {action args} {
    global _cgi

    _cgi_form_multiple_check
    set _cgi(form_in_progress) 1

    _cgi_close_proc_push _cgi_form_end
    cgi_put "<form action="
    if {[regexp {^[a-z]*:} $action]} {
	cgi_put "\"$action\""
    } else {
	cgi_put "\"[cgi_cgi $action]\""
    }
    set method "method=post"
    foreach a [lrange $args 0 [expr [llength $args]-2]] {
	if {[regexp "^method=" $a]} {
	    set method $a
	} elseif {[regexp "^(target|onReset|onSubmit)=(.*)" $a dummy attr str]} {
	    cgi_put " $attr=\"$str\""
	} elseif {[regexp "^enctype=(.*)" $a dummy str]} {
	    cgi_put " enctype=\"$str\""
	    set _cgi(form,enctype) $str
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put " $method>"
    uplevel 1 [lindex $args end]
    catch {unset _cgi(form,enctype)}
    _cgi_close_proc
}

proc _cgi_form_end {} {
    global _cgi
    unset _cgi(form_in_progress)
    cgi_put "</form>"
}

proc _cgi_form_multiple_check {} {
    global _cgi
    if {[info exists _cgi(form_in_progress)]} {
	error "Cannot create form (or isindex) with form already in progress."
    }
}

proc cgi_isindex {args} {
    _cgi_form_multiple_check

    cgi_put "<isindex"
    foreach a $args {
	if {[regexp "^href=(.*)" $a dummy str]} {
	    cgi_put " href=\"$str\""
	} elseif {[regexp "^prompt=(.*)" $a dummy str]} {
	    cgi_put " prompt=[cgi_dquote_html $str]"
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put ">"
}

##################################################
# argument handling
##################################################

proc cgi_input {{fakeinput {}} {fakecookie {}}} {
    global env _cgi _cgi_uservar _cgi_cookie _cgi_cookie_shadowed

    set _cgi(uservars) {}

    if {[info exists env(CONTENT_TYPE)] && [regexp ^multipart/form-data $env(CONTENT_TYPE)]} {
	if {![info exists env(REQUEST_METHOD)]} {
	    # running by hand
	    set fid [open $fakeinput]
	} else {
	    set fid stdin
	}
	if {([info tclversion] >= 8.1) || [catch exp_version] || [info exists _cgi(no_binary_upload)]} {
	    _cgi_input_multipart $fid
	} else {
	    _cgi_input_multipart_binary $fid
	}
    } else {
	if {![info exists env(REQUEST_METHOD)]} {
	    set input $fakeinput
	    set env(HTTP_COOKIE) $fakecookie
	} elseif { $env(REQUEST_METHOD) == "GET" } {
	    set input ""
	    catch {set input $env(QUERY_STRING)} ;# doesn't have to be set
	} elseif { $env(REQUEST_METHOD) == "HEAD" } {
	    set input ""
	} elseif {![info exists env(CONTENT_LENGTH)]} {
	    set _cgi(client_error) 1
	    error "Your browser failed to generate the content-length during a POST method."
	} else {
	    set length $env(CONTENT_LENGTH)
	    if {0!=[string compare $length "-1"]} {
		set input [read stdin $env(CONTENT_LENGTH)]		
	    } else {
		set _cgi(client_error) 1
		error "Your browser generated a content-length of -1 during a POST method."
	    }
	}
	# save input for possible diagnostics later
	set _cgi(input) $input

	set pairs [split $input &]
	foreach pair $pairs {
	    if {0 == [regexp "^(\[^=]*)=(.*)$" $pair dummy varname val]} {
		# if no match, unquote and leave it at that
		# this is typical of <isindex>-style queries
		set varname anonymous
		set val $pair
	    }

	    set varname [cgi_unquote_input $varname]
	    set val [cgi_unquote_input $val]
	    _cgi_set_uservar $varname $val
	}
    }

    # O'Reilly's web server incorrectly uses COOKIE
    catch {set env(HTTP_COOKIE) $env(COOKIE)}
    if {![info exists env(HTTP_COOKIE)]} return
    foreach pair [split $env(HTTP_COOKIE) ";"] {
	# pairs are actually split by "; ", sigh
	set pair [string trimleft $pair " "]
	# spec is not clear but seems to allow = unencoded
	# only sensible interpretation is to assume no = in var names
	# appears MS IE can omit "=val"
	set val ""
	regexp (\[^=]*)=?(.*) $pair dummy varname val

	set varname [cgi_unquote_input $varname]
	set val [cgi_unquote_input $val]

	if {[info exists _cgi_cookie($varname)]} {
	    lappend _cgi_cookie_shadowed($varname) $val
	} else {
	    set _cgi_cookie($varname) $val
	}
    }
}

proc _cgi_input_multipart {fin} {
    global env _cgi _cgi_uservar _cgi_userfile

    cgi_debug -noprint {
	# save file for debugging purposes
	set dbg_filename [file join $_cgi(tmpdir) CGIdbg.[pid]]
	# explicitly flush all writes to fout, because sometimes the writer
	# can hang and we won't get to the termination code
	set dbg_fout [open $dbg_filename w]
	set _cgi(input) $dbg_filename
	catch {fconfigure $dbg_fout -translation binary}
    }

    # figure out boundary
    if {0==[regexp boundary=(.*) $env(CONTENT_TYPE) dummy boundary]} {
	set _cgi(client_error) 1
	error "Your browser failed to generate a \"boundary=\" line in a multipart response (CONTENT_TYPE: $env(CONTENT_TYPE)).  Please upgrade (or fix) your browser."
    }
    set boundary --$boundary

    # don't corrupt or modify uploads yet allow Tcl 7.4 to work
    catch {fconfigure $fin -translation binary}

    # get first boundary line
    gets $fin buf
    if {[info exists dbg_fout]} {puts $dbg_fout $buf; flush $dbg_fout}

    set filecount 0
    while {1} {
	# process Content-Disposition:
	if {-1 == [gets $fin buf]} break
	if {[info exists dbg_fout]} {puts $dbg_fout $buf; flush $dbg_fout}
	catch {unset filename}
	foreach b $buf {
	    regexp {^name="(.*)"} $b dummy varname
	}
	if {0==[info exists varname]} {
	    set _cgi(client_error) 1
	    error "In response to a request for a multipart form, your browser generated a part header without a name field.  Please upgrade (or fix) your browser."
	}	    
	# Lame-o encoding (on Netscape at least) doesn't escape field
	# delimiters (like quotes)!!  Since all we've ever seen is filename=
	# at end of line, assuming nothing follows.  Sigh.
	regexp {filename="(.*)"} $buf dummy filename

	# Skip remaining headers until blank line.
	# Content-Type: can appear here.
	set conttype ""
	while {1} {
	    if {-1 == [gets $fin buf]} break
	    if {[info exists dbg_fout]} {puts $dbg_fout $buf; flush $dbg_fout}
	    if {0==[string compare $buf "\r"]} break
	    regexp -nocase "^Content-Type:\[ \t]+(.*)\r" $buf x conttype
	}

	if {[info exists filename]} {
	    # read the part into a file
	    set foutname /tmp/CGI[pid].[incr filecount]
	    set foutname [file join $_cgi(tmpdir) CGI[pid].[incr filecount]]
	    set fout [open $foutname w]
	    # "catch" permits this to work with Tcl 7.4
	    catch {fconfigure $fout -translation binary}
	    _cgi_set_uservar $varname [list $foutname $filename $conttype]
	    set _cgi_userfile($varname) [list $foutname $filename $conttype]

	    #
	    # Look for a boundary line preceded by \r\n.
	    #
	    # To do this, we buffer line terminators that might
	    # be the start of the special \r\n$boundary sequence.
	    # The buffer is called "leftover" and is just inserted
	    # into the front of the next output (assuming it's
	    # not a boundary line).

	    set leftover ""
	    while {1} {
		if {-1 == [gets $fin buf]} break
		if {[info exists dbg_fout]} {puts $dbg_fout $buf; flush $dbg_fout}

		if {0 == [string compare "\r\n" $leftover]} {
		    if {[regexp ^[set boundary](--)?\r$ $buf dummy dashdash]} {
			if {$dashdash == "--"} {set eof 1}
			break
		    }
		}
		if {[regexp (.*)\r$ $buf x data]} {
		    puts -nonewline $fout $leftover$data
		    set leftover "\r\n"
		} else {
		    puts -nonewline $fout $leftover$buf
		    set leftover "\n"
		}
	    }

	    close $fout
	    unset fout
	} else {
	    # read the part into a variable
	    set val ""
	    while {1} {
		if {-1 == [gets $fin buf]} break
		if {[info exists dbg_fout]} {puts $dbg_fout $buf; flush $dbg_fout}
		if {[regexp ^[set boundary](--)?\r$ $buf dummy dashdash]} {
		    if {$dashdash == "--"} {set eof 1}
		    break
		}
		if {0!=[string compare $val ""]} {
		    append val \n
		}
		regexp (.*)\r$ $buf dummy buf
		append val $buf
	    }
	    _cgi_set_uservar $varname $val
	}
        if {[info exists eof]} break
    }
    if {[info exists dbg_fout]} {close $dbg_fout}
}

proc _cgi_input_multipart_binary {fin} {
    global env _cgi _cgi_uservar _cgi_userfile

    log_user 0
    set timeout -1

    cgi_debug -noprint {
	# save file for debugging purposes
	set dbg_filename [file join $_cgi(tmpdir) CGIdbg.[pid]]
	set _cgi(input) $dbg_filename
	spawn -open [open $dbg_filename w]
	set dbg_sid $spawn_id
    }
    spawn -open $fin
    set fin_sid $spawn_id
    remove_nulls 0

    if {0} {
	# dump input to screen
	cgi_debug {
	    puts "<xmp>"
	    expect {
		-i $fin_sid
		-re ^\r {puts -nonewline "CR"; exp_continue}
		-re ^\n {puts "NL"; exp_continue}
		-re . {puts -nonewline $expect_out(buffer); exp_continue}
	    }
	    puts "</xmp>"
	    exit
	}
    }

    # figure out boundary
    if {0==[regexp boundary=(.*) $env(CONTENT_TYPE) dummy boundary]} {
	set _cgi(client_error) 1
	error "Your browser failed to generate a \"boundary=\" definition in a multipart response (CONTENT_TYPE: $env(CONTENT_TYPE)).  Please upgrade (or fix) your browser."
    }

    set boundary --$boundary
    set linepat "(\[^\r]*)\r\n"

    # get first boundary line
    expect {
	-i $fin_sid
	-re $linepat {
	    set buf $expect_out(1,string)
	    if {[info exists dbg_sid]} {send -i $dbg_sid -- $buf\n}
	}
	eof {
	    set _cgi(client_error) 1
	    error "Your browser failed to provide an initial boundary ($boundary) in a multipart response.  Please upgrade (or fix) your browser."
	}
    }

    set filecount 0
    while {1} {
	# process Content-Disposition:
	expect {
	    -i $fin_sid
	    -re $linepat {
		set buf $expect_out(1,string)
		if {[info exists dbg_sid]} {send -i $dbg_sid -- $buf\n}
	    }
	    eof break
	}
	catch {unset filename}
	foreach b $buf {
	    regexp {^name="(.*)"} $b dummy varname
	}
	if {0==[info exists varname]} {
	    set _cgi(client_error) 1
	    error "In response to a request for a multipart form, your browser generated a part header without a name field.  Please upgrade (or fix) your browser."
	}	    

	# Lame-o encoding (on Netscape at least) doesn't escape field
	# delimiters (like quotes)!!  Since all we've ever seen is filename=
	# at end of line, assuming nothing follows.  Sigh.
	regexp {filename="(.*)"} $buf dummy filename

	# Skip remaining headers until blank line.
	# Content-Type: can appear here.
	set conttype ""
	expect {
	    -i $fin_sid
	    -re $linepat {
		set buf $expect_out(1,string)
		if {[info exists dbg_sid]} {send -i $dbg_sid -- $buf\n}
		if {0!=[string compare $buf ""]} exp_continue
		regexp -nocase "^Content-Type:\[ \t]+(.*)\r" $buf x conttype
	    }
	    eof break
	}

	if {[info exists filename]} {
	    # read the part into a file
	    set foutname [file join $_cgi(tmpdir) CGI[pid].[incr filecount]]
	    spawn -open [open $foutname w]
	    set fout_sid $spawn_id

	    _cgi_set_uservar $varname [list $foutname $filename $conttype]
	    set _cgi_userfile($varname) [list $foutname $filename $conttype]

	    # This is tricky stuff - be very careful changing anything here!
	    # In theory, all we have to is record everything up to
	    # \r\n$boundary\r\n.  Unfortunately, we can't simply wait on
	    # such a pattern because the input can overflow any possible
	    # buffer we might choose.  We can't simply catch buffer_full
	    # because the boundary might straddle a buffer.  I doubt that
	    # doing my own buffering would be any faster than taking the
	    # approach I've done here.
	    #
	    # The code below basically implements a simple scanner that
	    # keeps track of whether it's seen crlfs or pieces of them.
	    # The idea is that we look for crlf pairs, separated by
	    # things that aren't crlfs (or pieces of them).  As we encounter
	    # things that aren't crlfs (or pieces of them), or when we decide
	    # they can't be, we mark them for output and resume scanning for
	    # new pairs.
	    #
	    # The scanner runs tolerably fast because the [...]+ pattern picks
	    # up most things.  The \r and \n are ^-anchored so the pattern
	    # match is pretty fast and these don't happen that often so the
	    # huge \n action is executed rarely (once per line on text files).
	    # The null pattern is, of course, only used when everything
	    # else fails.

	    # crlf	== "\r\n" if we've seen one, else == ""
	    # cr	== "\r" if we JUST saw one, else == ""
	    #           Yes, strange, but so much more efficient
	    #		that I'm willing to sacrifice readability, sigh.
	    # buf	accumulated data between crlf pairs

	    set buf ""
	    set cr ""
	    set crlf ""

	    expect {
		-i $fin_sid
		-re "^\r" {
		    if {$cr == "\r"} {
			append buf "\r"
		    }
		    set cr \r
		    exp_continue
		} -re "^\n" {
		    if {$cr == "\r"} {
			if {$crlf == "\r\n"} {
			    # do boundary test
			    if {[regexp ^[set boundary](--)?$ $buf dummy dashdash]} {
				if {$dashdash == "--"} {
				    set eof 1
				}
			    } else {
				# boundary test failed
				if {[info exists dbg_sid]} {send -i $dbg_sid -- \r\n$buf}
				send -i $fout_sid \r\n$buf ; set buf ""
				set cr ""
				exp_continue
			    }
			} else {
			    set crlf "\r\n"
			    set cr ""
			    if {[info exists dbg_sid]} {send -i $dbg_sid -- $buf}
			    send -i $fout_sid -- $buf ; set buf ""
			    exp_continue
			}
		    } else {
			if {[info exists dbg_sid]} {send -i $dbg_sid -- $crlf$buf\n}
			send -i $fout_sid -- $crlf$buf\n ; set buf ""
			set crlf ""
			exp_continue
		    }
		} -re "\[^\r\n]+" {
		    if {$cr == "\r"} {
			set buf $crlf$buf\r$expect_out(buffer)
			set crlf ""
			set cr ""
		    } else {
			append buf $expect_out(buffer)
		    }
		    exp_continue
		} null {
		    if {[info exists dbg_sid]} {
			send -i $dbg_sid -- $crlf$buf$cr
			send -i $dbg_sid -null
		    }
		    send -i $fout_sid -- $crlf$buf$cr ; set buf ""
		    send -i $fout_sid -null
		    set cr ""
		    set crlf ""
		    exp_continue
		} eof {
		    set _cgi(client_error) 1
		    error "Your browser failed to provide an ending boundary ($boundary) in a multipart response.  Please upgrade (or fix) your browser."
		}
	    }
	    exp_close -i $fout_sid    ;# implicitly closes fout
	    exp_wait -i $fout_sid
	    unset fout_sid
	} else {
	    # read the part into a variable
	    set val ""
	    expect {
		-i $fin_sid
		-re $linepat {
		    set buf $expect_out(1,string)
		    if {[info exists dbg_sid]} {send -i $dbg_sid -- $buf\n}
		    if {[regexp ^[set boundary](--)?$ $buf dummy dashdash]} {
			if {$dashdash == "--"} {set eof 1}
		    } else {
			regexp (.*)\r$ $buf dummy buf
			if {0!=[string compare $val ""]} {
			    append val \n
			}
			append val $buf
			exp_continue
		    }
		}
	    }
	    _cgi_set_uservar $varname $val
	}	    
        if {[info exists eof]} break
    }
    if {[info exists fout]} {
	exp_close -i $dbg_sid
	exp_wait -i $dbg_sid
    }

    # no need to close fin, fin_sid, or dbg_sid
}

# internal routine for defining user variables
proc _cgi_set_uservar {varname val} {
    global _cgi _cgi_uservar

    set exists [info exists _cgi_uservar($varname)]

    if {0==$exists} {
	lappend _cgi(uservars) $varname
    }

    # handle lists of values correctly
    if {[regexp List$ $varname]} {
	lappend _cgi_uservar($varname) $val
    } else {
	if {$exists} {
	    error "Multiple definitions of $varname encountered in input.
	    If you're trying to do this intentionally (such as with select),
	    the variable must have a \"List\" suffix."
	} else {
	    set _cgi_uservar($varname) $val
	}
    }
}

# export named variable
proc cgi_export {nameval} {
    regexp "(\[^=]*)(=?)(.*)" $nameval dummy name q value

    if {$q != "="} {
	set value [uplevel 1 set [list $name]]
    }

    cgi_put "<input type=hidden name=\"$name\" value=[cgi_dquote_html $value]>"
}

proc cgi_export_cookie {name args} {
    upvar 1 $name x
    eval cgi_cookie_set [list $name=$x] $args
}

# return list of variables available for import
# Explicit list is used to keep items in order originally found in form.
proc cgi_import_list {} {
    global _cgi

    return $_cgi(uservars)
}

# import named variable
proc cgi_import {name} {
    global _cgi_uservar
    upvar 1 $name var

    set var $_cgi_uservar($name)
}

proc cgi_import_as {name tclvar} {
    global _cgi_uservar
    upvar 1 $tclvar var

    set var $_cgi_uservar($name)
}

# like cgi_import but if not available, try cookie
proc cgi_import_cookie {name} {
    global _cgi_uservar
    upvar 1 $name var

    if {0==[catch {set var $_cgi_uservar($name)}]} return
    set var [cgi_cookie_get $name]
}

# like cgi_import but if not available, try cookie
proc cgi_import_cookie_as {name tclvar} {
    global _cgi_uservar
    upvar 1 $tclvar var

    if {0==[catch {set var $_cgi_uservar($name)}]} return
    set var [cgi_cookie_get $name]
}

proc cgi_import_file {type name} {
    global _cgi_userfile
    upvar 1 $name var

    set var $_cgi_userfile($name)
    switch -- $type {
	"-server" {
	    lindex $var 0
	} "-client" {
	    lindex $var 1
	} "-type" {
	    lindex $var 2
	}
    }
}

# deprecated, use cgi_import_file
proc cgi_import_filename {type name} {
    global _cgi_userfile
    upvar 1 $name var

    set var $_cgi_userfile($name)
    if {$type == "-server" || $type == "-local"} {
	# -local is deprecated
	lindex $var 0
    } else {
	lindex $var 1
    }
}

##################################################
# button support
##################################################

# not sure about arg handling, do we need to support "name="?
proc cgi_button {value args} {
    cgi_put "<input type=button value=[cgi_dquote_html $value]"
    foreach a $args {
	if {[regexp "^onClick=(.*)" $a dummy str]} {
	    cgi_put " onClick=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put ">"
}

proc cgi_submit_button {{nameval {=Submit Query}} args} {
    regexp "(\[^=]*)=(.*)" $nameval dummy name value
    cgi_put "<input type=submit"
    if {0!=[string compare "" $name]} {
	cgi_put " name=\"$name\""
    }
    cgi_put " value=[cgi_dquote_html $value]"
    foreach a $args {
	if {[regexp "^onClick=(.*)" $a dummy str]} {
	    cgi_put " onClick=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put ">"
}


proc cgi_reset_button {{value Reset} args} {
    cgi_put "<input type=reset value=[cgi_dquote_html $value]"

    foreach a $args {
	if {[regexp "^onClick=(.*)" $a dummy str]} {
	    cgi_put " onClick=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put ">"
}

proc cgi_radio_button {nameval args} {
    regexp "(\[^=]*)=(.*)" $nameval dummy name value

    cgi_put "<input type=radio name=\"$name\" value=[cgi_dquote_html $value]"

    foreach a $args {
	if {[regexp "^checked_if_equal=(.*)" $a dummy default]} {
	    if {0==[string compare $default $value]} {
		cgi_put " checked"
	    }
	} elseif {[regexp "^onClick=(.*)" $a dummy str]} {
	    cgi_put " onClick=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put ">"
}

proc cgi_image_button {nameval args} {
    regexp "(\[^=]*)=(.*)" $nameval dummy name value
    cgi_put "<input type=image"
    if {0!=[string compare "" $name]} {
	cgi_put " name=\"$name\""
    }
    cgi_put " src=\"$value\""
    foreach a $args {
	if {[regexp "^onClick=(.*)" $a dummy str]} {
	    cgi_put " onClick=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put ">"
}

# map/area implement client-side image maps
proc cgi_map {name cmd} {
    cgi_put "<map name=\"$name\">"
    _cgi_close_proc_push "cgi_put </map>"

    uplevel 1 $cmd
    _cgi_close_proc
}

proc cgi_area {args} {
    cgi_put "<area"
    foreach a $args {
	if {[regexp "^(coords|shape|href|target|onMouseOut|alt)=(.*)" $a dummy attr str]} {
	    cgi_put " $attr=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put ">"
}

##################################################
# checkbox support
##################################################

proc cgi_checkbox {nameval args} {
    regexp "(\[^=]*)(=?)(.*)" $nameval dummy name q value
    cgi_put "<input type=checkbox name=\"$name\""

    if {0!=[string compare "" $value]} {
	cgi_put " value=[cgi_dquote_html $value]"
    }

    foreach a $args {
	if {[regexp "^checked_if_equal=(.*)" $a dummy default]} {
	    if {0==[string compare $default $value]} {
		cgi_put " checked"
	    }
	} elseif {[regexp "^onClick=(.*)" $a dummy str]} {
	    cgi_put " onClick=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put ">"
}

##################################################
# textentry support
##################################################

proc cgi_text {nameval args} {
    regexp "(\[^=]*)(=?)(.*)" $nameval dummy name q value

    cgi_put "<input name=\"$name\""

    if {$q != "="} {
	set value [uplevel 1 set [list $name]]
    }
    cgi_put " value=[cgi_dquote_html $value]"

    foreach a $args {
	if {[regexp "^on(Select|Focus|Blur|Change)=(.*)" $a dummy event str]} {
	    cgi_put " on$event=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put ">"
}

##################################################
# textarea support
##################################################

proc cgi_textarea {nameval args} {
    regexp "(\[^=]*)(=?)(.*)" $nameval dummy name q value

    cgi_put "<textarea name=\"$name\""
    foreach a $args {
	if {[regexp "^on(Select|Focus|Blur|Change)=(.*)" $a dummy event str]} {
	    cgi_put " on$event=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_put ">"

    if {$q != "="} {
	set value [uplevel 1 set [list $name]]
    }
    cgi_put "[cgi_quote_html $value]</textarea>"
}

##################################################
# file upload support
##################################################

# for this to work, pass enctype=multipart/form-data to cgi_form
proc cgi_file_button {name args} {
    global _cgi
    if {[info exists _cgi(formtype)] && ("multipart/form-data" != $_cgi(form,enctype))} {
	error "cgi_file_button requires that cgi_form have the argument enctype=multipart/form-data"
    }
    cgi_put "<input type=file name=\"$name\"[_cgi_list_to_string $args]>"
}

##################################################
# select support
##################################################

proc cgi_select {name args} {
    cgi_put "<select name=\"$name\""
    _cgi_close_proc_push "cgi_put </select>"
    foreach a [lrange $args 0 [expr [llength $args]-2]] {
	if {[regexp "^on(Focus|Blur|Change)=(.*)" $a dummy event str]} {
	    cgi_put " on$event=\"$str\""
	} else {
	    if {0==[string compare multiple $a]} {
		;# sanity check
		if {![regexp "List$" $name]} {
		    cgi_puts ">" ;# prevent error from being absorbed
		    error "When selecting multiple options, select variable \
			    must end in \"List\" to allow the value to be \
			    recognized as a list when it is processed later."
		}
	    }
	    cgi_put " $a"
	}
    }
    cgi_put ">"
    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

proc cgi_option {o args} {
    cgi_put "<option"
    set value $o
    set selected 0
    foreach a $args {
	if {[regexp "^selected_if_equal=(.*)" $a dummy selected_if_equal]} {
	} elseif {[regexp "^value=(.*)" $a dummy value]} {
	    cgi_put " value=[cgi_dquote_html $value]"
	} else {
	    cgi_put " $a"
	}
    }
    if {[info exists selected_if_equal]} {
	if {0 == [string compare $selected_if_equal $value]} {
	    cgi_put " selected"
	}
    }
    cgi_puts ">[cgi_quote_html $o]"
}

##################################################
# plug-in support
##################################################

proc cgi_embed {src wh args} {
    regexp (.*)x(.*) $wh dummy width height
    cgi_put "<embed src=[cgi_dquote_html $src] width=\"$width\" height=\"$height\""
    foreach a $args {
	if {[regexp "^palette=(.*)" $a dummy str]} {
	    cgi_put " palette=\"$str\""
	} elseif {[regexp -- "-quote" $a]} {
	    set quote 1
	} else {
	    if {[info exists quote]} {
		regexp "(\[^=]*)=(.*)" $a dummy var val
		cgi_put " var=[cgi_dquote_html $var]"
	    } else {
		cgi_put " $a"
	    }
	}
    }
    cgi_put ">"
}

##################################################
# mail support
##################################################

# mail to/from the service itself
proc cgi_mail_addr {args} {
    global _cgi

    if {[llength $args]} {
	set _cgi(email) [lindex $args 0]
    }
    return $_cgi(email)
}

proc cgi_mail_start {to} {
    global _cgi

    set _cgi(mailfile) [file join $_cgi(tmpdir) cgimail.[pid]]
    set _cgi(mailfid) [open $_cgi(mailfile) w+]
    set _cgi(mailto) $to

    # mail is actually sent by "nobody".  To force bounce messages
    # back to us, override the default return-path.
    cgi_mail_add "Return-Path: <$_cgi(email)>"
    cgi_mail_add "From: [cgi_name] <$_cgi(email)>"
    cgi_mail_add "To: $to"
}

# add another line to outgoing mail
# if no arg, add a blank line
proc cgi_mail_add {{arg {}}} {
    global _cgi

    puts $_cgi(mailfid) $arg
}	

# end the outgoing mail and send it
proc cgi_mail_end {} {
    global _cgi

    flush $_cgi(mailfid)

    if {[file executable /usr/sbin/sendmail]} {
	exec /usr/sbin/sendmail -t -odb < $_cgi(mailfile)
	# Explanation:
	# -t   means: pick up recipient from body
	# -odb means: deliver in background
	# note: bogus local address cause sendmail to fail immediately
    } else {
	# fallback for sites without sendmail

	if {0==[info exists _cgi(mail_relay)]} {
	    regexp "@(.*)" $_cgi(mailto) dummy _cgi(mail_relay)
	}

	set s [socket $_cgi(mail_relay) 25]
	gets $s answer
	if {[lindex $answer 0] != 220} {error $answer} 
	puts $s "EHLO $host";flush $s
	gets $s answer
	if {[lindex $answer 0] != 250} {error $answer}  
	puts $s "MAIL FROM:<$_cgi(email)>";flush $s
	gets $s answer
	if {[lindex $answer 0] != 250} {error $answer}  
	puts $s "RCPT TO:<$_cgi(mailto)>";flush $s
	gets $s answer
	if {[lindex $answer 0] != 250} {error $answer}  
	puts $s DATA;flush $s
	gets $s answer
	if {[lindex $answer 0] != 354} {error $answer}  
	seek $_cgi(mailfid) 0 start
	puts $s [read $_cgi(mailfid)];flush $s
	puts $s .;flush $s
	gets $s answer
	if {[lindex $answer 0] != 250} {error $answer}  
	close $s
    }
    close $_cgi(mailfid)
    file delete -force $_cgi(mailfile)
}

proc cgi_mail_relay {host} {
    global _cgi

    set _cgi(mail_relay) $host
}

##################################################
# cookie support
##################################################

# calls to cookie_set look like this:
#   cgi_cookie_set user=don domain=nist.gov expires=never
#   cgi_cookie_set user=don domain=nist.gov expires=now
#   cgi_cookie_set user=don domain=nist.gov expires=...actual date...

proc cgi_cookie_set {nameval args} {
    global _cgi

    if {![info exists _cgi(http_head_in_progress)]} {
	error "Cookies must be set from within cgi_http_head."
    }
    cgi_puts -nonewline "Set-Cookie: [cgi_cookie_encode $nameval];"

    foreach a $args {
	if {[regexp "^expires=(.*)" $a dummy expiration]} {
	    if {0==[string compare $expiration "never"]} {
		set expiration "Friday, 11-Jan-2038 23:59:59 GMT"
	    } elseif {0==[string compare $expiration "now"]} {
		set expiration "Friday, 31-Dec-1990 23:59:59 GMT"
	    }
	    cgi_puts -nonewline " expires=$expiration;"
	} elseif {[regexp "^(domain|path)=(.*)" $a dummy attr str]} {
	    cgi_puts -nonewline " $attr=[cgi_cookie_encode $str];"
	} elseif {[regexp "^secure$" $a]} {
	    cgi_puts -nonewline " secure;"
	}
    }
    cgi_puts ""
}

# return list of cookies available for import
proc cgi_cookie_list {} {
    global _cgi_cookie

    array names _cgi_cookie
}

proc cgi_cookie_get {args} {
    global _cgi_cookie

    set flag ""
    if {[llength $args]>1} {
	set flag [lindex $args 0]
	set args [lrange $args 1 end]
    }
    set name [lindex $args 0]

    if {$flag == "-all"} {
	global _cgi_cookie_shadowed

	foreach {flag name} $args {}
	if {[info exists $_cgi_cookie_shadowed($name)]} {
	    return [concat $_cgi_cookie($name) $_cgi_cookie_shadowed($name)]
	} else {
	    return [concat $_cgi_cookie($name)]
	}
    }
    return $_cgi_cookie($name)
}

proc cgi_cookie_encode {in} {
    regsub -all " " $in "+" in
    regsub -all "%" $in "%25" in   ;# must preceed other subs that produce %
    regsub -all ";" $in "%3B" in
    regsub -all "," $in "%2C" in
    regsub -all "\n" $in "%0D%0A" in
    return $in
}

##################################################
# table support
##################################################

proc cgi_table {args} {
    cgi_put "<table"
    _cgi_close_proc_push "cgi_put </table>"

    if {[llength $args]} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">"
    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

proc cgi_caption {args} {
    cgi_put "<caption"
    _cgi_close_proc_push "cgi_put </caption>"

    if {[llength $args]} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">"
    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

proc cgi_table_row {args} {
    cgi_put "<tr"
    _cgi_close_proc_push "cgi_put </tr>"
    if {[llength $args]} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">"
    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

# like table_row but without eval
proc cgi_tr {args} {
    cgi_put <tr
    if {[llength $args] > 1} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">"
    foreach i [lindex $args end] {
	cgi_td $i
    }
    cgi_put </tr>
}

proc cgi_table_head {args} {
    cgi_put "<th"
    _cgi_close_proc_push "cgi_put </th>"

    if {[llength $args]} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">"
    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

# like table_head but without eval
proc cgi_th {args} {
    cgi_put "<th"

    if {[llength $args] > 1} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">[lindex $args end]</th>"
}

proc cgi_table_data {args} {
    cgi_put "<td"
    _cgi_close_proc_push "cgi_put </td>"

    if {[llength $args]} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">"
    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

# like table_data but without eval
proc cgi_td {args} {
    cgi_put "<td"

    if {[llength $args] > 1} {
	cgi_put "[_cgi_lrange $args 0 [expr [llength $args]-2]]"
    }
    cgi_put ">[lindex $args end]</td>"
}

##################################################
# stylesheets - not yet documented
##################################################

proc cgi_stylesheet {href} {
    puts "<link rel=stylesheet href=\"$href\" type=\"text/css\">"
}

proc cgi_span {args} {
    set buf "<span"
    foreach a [lrange $args 0 [expr [llength $args]-2]] {
	if {[regexp "style=(.*)" $a dummy str]} {
	    append buf " style=\"$str\""
	} elseif {[regexp "class=(.*)" $a dummy str]} {
	    append buf " class=\"$str\""
	} else {
	    append buf " $a"
	}
    }
    return "$buf>[lindex $args end]</span>"
}

##################################################
# frames
##################################################

proc cgi_frameset {args} {
    cgi_head ;# force it out, just in case none

    cgi_put "<frameset"
    _cgi_close_proc_push "cgi_puts </frameset>"

    foreach a [lrange $args 0 [expr [llength $args]-2]] {
	if {[regexp "^(rows|cols|onUnload|onLoad|onBlur)=(.*)" $a dummy attr str]} {
	    cgi_put " $attr=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_puts ">"
    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

proc cgi_frame {namesrc args} {
    cgi_put "<frame"

    regexp "(\[^=]*)(=?)(.*)" $namesrc dummy name q src

    if {$name != ""} {
	cgi_put " name=\"$name\""
    }

    if {$src != ""} {
	cgi_put " src=\"$src\""
    }

    foreach a $args {
	if {[regexp "^(marginwidth|marginheight|scrolling|onFocus)=(.*)" $a dummy attr str]} {
	    cgi_put " $attr=\"$str\""
	} else {
	    cgi_put " $a"
	}
    }
    cgi_puts ">"
}

proc cgi_noframes {args} {
    cgi_puts "<noframes>"
    _cgi_close_proc_push "cgi_puts </noframes>"
    uplevel 1 [lindex $args end]
    _cgi_close_proc
}

##################################################
# admin support
##################################################

# mail address of the administrator
proc cgi_admin_mail_addr {args} {
    global _cgi

    if {[llength $args]} {
	set _cgi(admin_email) [lindex $args 0]
    }
    return $_cgi(admin_email)
}

##################################################
# if possible, make each cmd available without cgi_ prefix
##################################################

if {[info tclversion] >= 7.5} {
    foreach _cgi(old) [info procs cgi_*] {
	regexp "^cgi_(.*)" $_cgi(old) _cgi(dummy) _cgi(new)
	if {[llength [info commands $_cgi(new)]]} continue
	interp alias {} $_cgi(new) {} $_cgi(old)
    }
} else {
    foreach _cgi(old) [info procs cgi_*] {
	regexp "^cgi_(.*)" $_cgi(old) _cgi(dummy) _cgi(new)
	if {[llength [info commands $_cgi(new)]]} continue
	proc $_cgi(new) {args} "uplevel 1 $_cgi(old) \$args"
	#proc $_cgi(new) {args} "upvar 1 _cgi_local x; set x \$args; uplevel 1 \"$_cgi(old) \$x\""
    }
}

##################################################
# internal utilities
##################################################

# undo Tcl's quoting due to list protection
# This leaves a space at the beginning if the string is non-null
# but this is always desirable in the HTML context in which it is called
# and the resulting HTML looks more readable.
# (It makes the Tcl callers a little less readable - however, there aren't
# more than a handful and they're all right here, so we'll live with it.)
proc _cgi_list_to_string {list} {
    set string ""
    foreach l $list {
	append string " $l"
    }
    # remove first space if possible
    # regexp "^ ?(.*)" $string dummy string
    return $string
}

# do lrange but return as string
# needed for stuff like: cgi_puts "[_cgi_lrange $args ...]
# Like _cgi_list_to_string, also returns string with initial blank if non-null
proc _cgi_lrange {list i1 i2} {
    _cgi_list_to_string [lrange $list $i1 $i2]
}

##################################################
# user-defined procedures
##################################################

# User-defined procedure called immediately after <body>
# Good mechanism for controlling things such as if all of your pages
# start with the same graphic or other boilerplate.
proc app_body_start {} {}

# User-defined procedure called just before </body>
# Good place to generate signature lines, last-updated-by, etc.
proc app_body_end {} {}

proc cgi_puts {args} {
    eval puts $args
}

# User-defined procedure to generate DOCTYPE declaration
proc cgi_doctype {} {}

##################################################
# do some initialization
##################################################

cgi_name ""
cgi_root ""
cgi_body_args ""

# email addr of person responsible for this service
cgi_admin_mail_addr "root"	;# you should override this!

# most services won't have an actual email addr
cgi_mail_addr "CGI script - do not reply"

# deduce tmp directory
switch $tcl_platform(platform) {
    unix {
	set _cgi(tmpdir) /tmp
    } macintosh {
	set _cgi(tmpdir) [pwd]
    } default {
	set _cgi(tmpdir) [pwd]
	catch {set _cgi(tmpdir) $env(TMP)}
	catch {set _cgi(tmpdir) $env(TEMP)}
    }
}

package provide cgi 1.3.0
