#!/bin/bash
#
# Cgi interface to dpkg. Show information about debian packages.
#
# Copyright (C) 1999-2006  Massimo Dal Zotto <dz@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

PROG_VERSION=2.56
SELF=${0##*/}; export SELF
HELP_PAGE="/usr/share/man/man8/dpkg-www.8.gz"
COLUMNS=256; export COLUMNS
SECTION_DESCRIPTIONS="
admin		Utilities to administer the system
base		Basic needed utilities of every Debian system
comm		Software to use your modem in the old fashioned style
devel		Development utilities, compilers, environments, libraries, etc.
doc		FAQs, HOWTOs, documents and software to browse documentation 
editors		Software to edit files and programming environments
electronics	Electronics utilities
games		Programs to spend a nice time with after setting up Debian
graphics	Editors, viewers, converters... Everything to become an artist
hamradio	Software for ham radio
interpreters	Interpreters for interpreted languages and macro processors
libs		Libraries to make other programs work
mail		Programs to route, read, and compose E-mail messages
math		Math software
misc		Miscellaneous utilities that didn't fit well anywhere else
net		Daemons and clients to connect your system to the world
news		Software to access Usenet, to set up news servers, etc.
non-us		Software restricted in the US by export regulations or patents
oldlibs		Old versions of libraries, kept for backward compatibility
otherosfs	Software to use programs and filesystems for other O.S.
science		Basic tools for scientific work
shells		Command shells and friendly user interfaces for beginners.
sound		Utilities to deal with sound: mixers, players, recorders, etc. 
tex		The famous typesetting software and related programs
text		Utilities to format and print text documents
utils		Utilities for file/disk manipulation, backup, monitoring, etc. 
web		Web servers, browsers, proxies, download tools etc.
x11		X servers, libraries, fonts, window managers, and applications
"

TASK_DESCRIPTIONS="
basic-desktop	X window system
c-dev		C and C++
chinese-s	simplified Chinese environment
chinese-t	traditional Chinese environment
cyrillic	Cyrillic environment
database-server	SQL database
desktop		desktop environment
dialup		dialup system
dns-server	DNS server
file-server	file server
fortran-dev	fortran
french		French environment
games		games
german		German environment
japanese	Japanese environment
junior		Debian Jr.
korean		Korean environment
laptop		laptop system
mail-server	mail server
news-server	usenet news server
polish		Polish environment
print-server	print server
python-dev	Python
russian		Russian environment
science		scientific applications
spanish		Spanish environment
tcltk-dev	Tcl/Tk
tex		TeX/LaTeX environment
unix-server	conventional unix server
web-server	web server
"

# Configuration defaults, can be overriden in /etc/dpkg-www.conf
#
SHOW_LOCAL_FILES=auto	# Show local files directly (automatic)
LIST_UNAVAILABLE=0	# List also unavailable packages
LIST_DOCUMENTATION=1	# List registered package documentation
DPKG=auto		# Select the dpkg program (automatic)
MAN=auto		# Select the man program (automatic)
DEBIAN_CONTENTS=	# List of Contents-xxx.gz files
INSTALLED_ONLY=0	# Only installed packages
DPKG_STATUS=/var/lib/dpkg/status
DPKG_AVAILABLE=/var/lib/dpkg/available
TASK_DESCRIPTIONS_FILE=/usr/share/tasksel/debian-tasks.desc
APT_CACHE_OPTS="-o APT::Cache::AllVersions=0"
CHECK_PACKAGE_VERSION=true
DPKG_WWW_BROWSER_HACK=false
GREP_DCTRL_OPTS="-i"
BGCOLOR="#c0c0c0"
KEEP_INPUT=0
DEBUG=0
DWWW_PATH="/cgi-bin/dwww"
INFO2WWW_PATH="/cgi-bin/info2www"

# Install buttons support. This require that users define the following
# mime action in their netscape mailcap file:
#
# application/dpkg-www-installer;/usr/sbin/dpkg-www-installer -f '%s'
#
CHECK_BUTTONS=0		# Enable install check-buttons in package list
INSTALL_BUTTON=0	# Enable install button in package info

# Source optional local configuration file
test -e /etc/dpkg-www.conf && . /etc/dpkg-www.conf 2>/dev/null
set -o noglob

HOSTNAME_FULL="$(hostname -f)"
HOSTNAME="${HOSTNAME_FULL%%.*}"

# Use dlocate, if installed, to speedup certain queries
if [ ! "$DPKG" -o "$DPKG" = auto ]; then
    if [ -x /usr/bin/dlocate ]; then
	DPKG=dlocate
    else
	DPKG=dpkg
    fi
fi

# Select the Manpage to HTML translator
if [ "$MAN" = dwww ]; then
	MAN="${DWWW_PATH}?type=man\\\\&location="
fi
if [ "$MAN" = man2html ]; then
    if [ -x /usr/lib/cgi-bin/man/man2html ]; then
	MAN="/cgi-bin/man/man2html?"
    elif [ -x /usr/lib/cgi-bin/man2html ]; then
	MAN="/cgi-bin/man2html?"
    else
	MAN="${DWWW_PATH}?type=man\\\\&location="
    fi
fi
if [ ! "$MAN" -o "$MAN" = auto ]; then
    if [ -x /usr/lib/cgi-bin/man/man2html ]; then
	MAN="/cgi-bin/man/man2html?"
    elif [ -x /usr/lib/cgi-bin/man2html ]; then
	MAN="/cgi-bin/man2html?"
    else
	MAN="${DWWW_PATH}?type=man\\\\&location="
    fi
fi

# If remote host is localhost generate local URL's for local files
case "$REMOTE_HOST" in
    localhost|$HOSTNAME|$HOSTNAME_FULL)
	if [ ! "$SHOW_LOCAL_FILES" -o "$SHOW_LOCAL_FILES" = auto ]; then
	    SHOW_LOCAL_FILES=true
	fi
	if [ "$HTTP_USER_AGENT" != "${HTTP_USER_AGENT#*Mozilla/[56]}" ]; then
	    # It seems that Mozilla/5.0 refuses to access local files from
	    # a web page for security reasons.
	    SHOW_LOCAL_FILES=false
	fi
	;;
    *)
	SHOW_LOCAL_FILES=false
	;;
esac

# Print a short help for the cgi-program
usage() {
    cat <<- EOF
	<P>
	You can query information about Debian packages and installed files.
	The search argument can be:
	<P>
	<TABLE WIDTH="80%">
	  <TR><TD>
	    <TD> <TT>?</TT>
	    <TD> show this help page
	  <TR><TD>
	    <TD> <TT>*</TT>
	    <TD> list all packages available on your system
	  <TR><TD>
	    <TD> <I>&lt;empty&gt;</I>
	    <TD> list all packages installed on your system
	  <TR><TD>
	    <TD> <I>&lt;space&gt;</I>
	    <TD> print only the input form, for use from wm menus
	  <TR><TD>
	    <TD> <I>package</I>
	    <TD> list the required package and its owned files
	  <TR><TD>
	    <TD> <I>list of packages</I>
	    <TD> list required packages concisely
	  <TR><TD>
	    <TD> <I>wilcard expression</I>
	    <TD> list matching packages concisely
	  <TR><TD>
	    <TD> <I>absolute pathname</I>
	    <TD> list packages owning pathname
	  <TR><TD>
	    <TD> <I>field=value</I>
	    <TD> list packages with control field matching value
	  <TR><TD>
	    <TD> <I>=value</I>
	    <TD> list packages with any field matching value
	  <TR><TD>
	    <TD> <I>section=?</I>
	    <TD> list available package sections
	  <TR><TD>
	    <TD> <I>task=?</I>
	    <TD> list available tasks
	  <TR><TD>
	    <TD> <I>recent=[&lt;N&gt;]</I>
	    <TD> list packages installed in last N days (default 1)
	</TABLE>
	<P>
	<H3>Examples:</H3>
	<P>
	<FORM METHOD="GET" ACTION="$SELF">
	<TABLE WIDTH="80%">
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=*"><TT>*</TT></A>
	    <TD> list all packages available on your system
	  <TR><TD>
	    <TD> <A HREF="$SELF?query="><I>&lt;empty&gt;</I></A>
	    <TD> list all packages installed on your system
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=binutils"><TT>binutils<TT></A>
	    <TD> list verbosely the package binutils
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=dpkg+apt"><TT>dpkg apt</TT></A>
	    <TD> list the dpkg and apt packages
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=jpeg*"><TT>jpeg*</TT></A>
	    <TD> list packages with name beginning with "jpeg"
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=*jpeg*"><TT>*jpeg*</TT></A>
	    <TD> list packages with name containing "jpeg"
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=/usr/bin/tail"><TT>/usr/bin/tail</TT></A>
	    <TD> list packages owning the file /usr/bin/tail
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=/.*/postfix"><TT>/.*/postfix</TT></A>
	    <TD> list packages owning any file named "postfix" (1)
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=/.*pbm.*"><TT>/.*pbm.*</TT></A>
	    <TD> list packages owning any file matching *pbm* (1)
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=maintainer%3DJoey+Hess"
	         ><TT>maintainer=Joey Hess</TT></A>
	    <TD> list packages with maintainer Joey Hess (2)
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=section%3Dnet"><TT>section=net</TT></A>
	    <TD> list packages with section net (2)
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=%3Dfirewall"><TT>=firewall</TT></A>
	    <TD> list packages with any field matching "firewall" (2)
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=section%3D?"><TT>section=?</TT></A>
	    <TD> list available package sections
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=task%3D?"><TT>task=?</TT></A>
	    <TD> list available tasks
	  <TR><TD>
	    <TD> <A HREF="$SELF?query=recent%3D2"><TT>recent=2</TT></A>
	    <TD> list packages installed in last 2 days
	  <TR><TD>
	    <TD> <TT>
	         <INPUT TYPE="hidden" NAME="packages" VALUE="hello">
		 <INPUT TYPE="submit" NAME="install" VALUE="Install">
		 package <A HREF="$SELF?query=hello">hello</A><TT>
	    <TD> install the package hello (3)
	  <TR><TD>
	    <TD> <TT>
		 <!-- INPUT TYPE="hidden" NAME="packages" VALUE="hello" -->
		 <INPUT TYPE="submit" NAME="remove" VALUE="Remove ">
		 package <A HREF="$SELF?query=hello">hello</A><TT>
	    <TD> remove the package hello (3)
	</TABLE>
	</FORM>
	<P>
	(1) The regexp form can be used to query also packages owning files
	not installed on your system. However this works only if you have
	properly configured the variable <TT>DEBIAN_CONTENTS</TT> in the
	configuration file<EM>&nbsp/etc/dpkg-www.conf</EM>.
	This query can be used to find which package you should install
	in order to get some missing command or file.
	Note that the leading '<TT>/</TT>' is required to distinguish filename
	queries from package queries.
	<P>
	(2) This works only if the
	<A HREF="$SELF?query=grep-dctrl">grep-dctrl</A> package is installed.
	</P>
	(3) This works only if <TT>INSTALL_BUTTON=1</TT> has been set in the
	configuration file<EM>&nbsp/etc/dpkg-www.conf</EM>, your browser has
	been configured to handle WEB installation requests and, of course,
	you know the root password.
	See <A HREF="${MAN}${HELP_PAGE}">dpkg-www(8)</A>
	man page for more information.
	</P>
EOF
}

debug() {
    test_flag "$DEBUG" && echo "<TT>$(escape_html "$*")</TT><BR>"
}

test_flag() {
    case "$1" in
	0|false|n|no|"") return 1 ;;
	*)		 return 0 ;;
    esac
}

# Sanity check on input values
checkInput() {
    if echo "$*" | grep -q '[(){}<>$%#&!|\\;`]'; then
	printError "Error: invalid input"
	exit
    fi
}

# Execute the query
doQuery() {
    debug "doQuery $1"
    local query="$1"
    case "${query%/}" in
	\?|-\?|-h|-help|--help)
	    usage
	    ;;
	" ")
	    return
	    ;;
	-*)
	    printError "Invalid keyword: $query"
	    exit
	    ;;
	/*)
	    queryOwners "$query"
	    ;;
	*=*)
	    queryPackagesByField "$query"
	    ;;
	*)
	    checkInput "$query"
	    queryPackages "$query"
	    ;;
    esac
}

# Query one or more packages
queryPackages() {
    debug "queryPackages $*"
    local packages="$*"
    local package="${packages%% *}"
    if [ ! "$packages" ]; then
	# No argument, show the package list
	debug "no arguments"
	listPackages
    elif [ "$package" != "$packages" -o "$packages" != "${packages#*[\[*?]}" ]
    then
	# Package list or wildcard, list packages concisely
	debug "package list"
	listPackages $packages
    else
	# One package, show package information
	debug "one package"
	listPackage $package
    fi
}

# Query one or more packages by control field value
queryPackagesByField() {
    field="${*%%=*}"; arg="${*#$field}"; arg="${arg#=}"
    field="${field## }"; field="${field%% }"
    arg="${arg## }"; arg="${arg%% }"
    if [ "$field" = section -a "$arg" = "?" ]; then
	listSections
	return
    fi
    if [ "$field" = task -a "$arg" = "?" ]; then
	listTasks
	return
    fi
    if [ "$field" = recent -o "$field" = new ]; then
	listRecentPackages $arg
	return
    fi
    if [ "$field" != "" -a ! -x /usr/bin/grep-dctrl ]; then
	echo "<PRE>"
	echo -n "Error: the <A HREF=\"$SELF?query=/usr/bin/grep-dctrl\">"
	echo -n "grep-dctrl</A>"
	echo " command is not installed on your system"
	echo "</PRE>"
	return
    fi
    echo "<PRE>"
    echo "Packages with $(escape_html "${field:-any field matching} $arg")"
    echo ""
    {
	if [ -x /usr/bin/grep-dctrl ]; then
	    grep-dctrl $GREP_DCTRL_OPTS -s Package -F "$field" "$arg" \
		$DPKG_STATUS $DPKG_AVAILABLE
	else
	    apt-cache search "$arg" | sed 's/ .*//;s/^/Package: /'
	fi
	dpkg -l "*" 2>&1
    } \
    | awk '
	/^Package:/ { p[$2]=1; NR=0; next }
	(NR <= 5) { print }
	(p[$2] != 1) { next }
	{ print }' \
    | listFilter
    echo "</PRE>"
}

# List available sections
listSections() {
    sections=$(
	grep -h "^Section:" $DPKG_STATUS $DPKG_AVAILABLE \
	   | sed 's|.*:[ ]*||; s|.*/||' | sort | uniq | grep -v "^non-"
    )
    echo "<pre>"
    echo "Available sections:"
    echo ""
    for s in $sections; do
	desc=$(
	    echo "$SECTION_DESCRIPTIONS" | sed "/^$s/!d; s/^$s[	 ]*//;q"
	)
	desc=${desc:="no description available"}
	x=$(printf "%$[16-${#s}]s" " ")
	echo "  <a href=\"${SELF}?query=section%3D${s}\">${s}</a>${x} ${desc}"
    done
    echo "</pre>"
}

# List available tasks
listTasks() {
    tasks=$(
	grep -h "^Task:" $DPKG_STATUS $DPKG_AVAILABLE \
	   | sed 's|.*:[ ]*||; s|,[ ]*|\
|g' | sort | uniq
    )
    echo "<pre>"
    echo "Available tasks:"
    echo ""
    for t in $tasks; do
	desc=$(
	    if [ -e "$TASK_DESCRIPTIONS_FILE" ]; then
		cat "$TASK_DESCRIPTIONS_FILE" 2>/dev/null | awk -v t="$t" '
		    /^Task:/ { task = $2 }
		    /^Description:/ && (task == t) {
			sub($1"[ ]*",""); print; exit
		    }'
	    else
		echo "$TASK_DESCRIPTIONS" | sed "/^$t/!d; s/^$t[ 	]*//;q"
	    fi
	)
	desc=${desc:="no description available"}
	x=$(printf "%$[16-${#t}]s" " ")
	echo "  <a href=\"${SELF}?query=task%3D${t}\">${t}</a>${x} ${desc}"
    done
    echo "</pre>"
}

# List recent packages
listRecentPackages() {
    n=${1:-1}
    packages=$(
	find /var/lib/dpkg/info/ -daystart \( -name \*.list -a -mtime -$n \) \
	   | sed 's|.list$||;s|.*/||' \
	   | sort
    )
    if [ "$packages" ]; then
	echo "<PRE>"
	echo "Recent packages in last $n days:"
	echo ""
	dpkg -l $packages 2>&1 | listFilter
	echo "</PRE>"
    else
	echo "<PRE>"
	echo "No recent packages in last $n days"
	echo "</PRE>"
    fi
}

# List one or more packages concisely
listPackages() {
    debug "listPackages $*"
    echo "<PRE>"
    dpkg -l "$@" 2>&1 | listFilter
    echo "</PRE>"
}

# List a single package
#
listPackage() {
    debug "listPackage $*"
    local package="$1"
    local installed
    local status
    local version
    local latest
    local priority
    local essential

    pkg_info="$(dpkg -s $package)"
    if [ ! "$pkg_info" ]; then
	# Unknown package or virtual package
	listVirtualPackage $package || listUnknownPackage $package
	return 1
    fi

    status="$(echo "$pkg_info" | awk '/^Status:/ {print $4}')"
    #   installed		The package is unpacked and configured OK.
    #   half-installed		The installation has started but not completed.
    #   not-installed		The package is not installed on your system.
    #   unpacked		The package is unpacked, but not configured.
    #   half-configured		The package configuration has not completed.
    #   config-files		Only the config files exist on the system.
    # Handle unknown package
    if [ ! "$status" ]; then
	listUnknownPackage $package
	return 1
    fi

    if [ "$status" = not-installed -o "$status" = config-files ]; then
	if [ -x /usr/bin/apt-cache ]; then
	    pkg_info="$(
		echo "$pkg_info" \
		   | grep "^\(Package\|Status\|Priority\|Section\):"
		apt-cache $APT_CACHE_OPTS show $package 2>/dev/null \
		   | sed 's/^installed-size:/Installed-Size:/' \
		   | grep -v -e "^\(Package\|Status\|Priority\|Section\):" \
			     -e "^\(Filename\|MD5sum\|Size\):"
	    )"
	else
	    pkg_info="$(
		echo "$pkg_info" \
		   | grep "^\(Package\|Status\|Priority\|Section\):"
		dpkg --print-avail $package 2>/dev/null \
		   | grep -v -e "^\(Package\|Status\|Priority\|Section\):" \
			     -e "^\(Filename\|MD5sum\|Size\):"
	    )"
	fi
    fi

    # Check for newer version of installed package
    if [ "$status" = installed ]; then
	version="$(echo "$pkg_info" | awk '/^Version:/ {print $2}')"
	priority="$(echo "$pkg_info"  | grep ^Priority:  | awk '{print $2}')"
	essential="$(echo "$pkg_info" | grep ^Essential: | awk '{print $2}')"
	if test_flag "$CHECK_PACKAGE_VERSION"; then
	    latest=$(
		apt-cache $APT_CACHE_OPTS show $package \
		  | grep ^Version: | awk '{print $2}'
	    ); latest=${latest:-$version}
	    if dpkg --compare-versions "$latest" gt "$version"; then
		pkg_info="$(
		    echo "$pkg_info" \
		       | sed "/^Version:/s/\$/ (latest is $latest)/"
		)"
	    fi
	else
	    latest="$version"
	fi
    fi

    echo "<PRE>"

    # Print package info
    echo "$pkg_info" | packageFilter $package

    if [ "$INSTALL_BUTTON" = "top" ]; then
	addButtons
    fi

    # Print documentation and file list
    if [ "$status" != not-installed ]; then
	listPackageDocumentation $package
	listPackageFiles $package
    fi

    if [ "$INSTALL_BUTTON" != "top" ]; then
	addButtons
    fi

    echo "</PRE>"
}

# Add "Install", "Update" and "Remove" buttons if enabled.
addButtons() {
    if [ "$status" = installed ]; then
	if [ "$priority" = required -o "$essential" = yes ]; then
	    if [ "$version" != "$latest" ]; then
		addUpgradeButton $package $latest
	    fi
	else
	    if [ "$version" != "$latest" ]; then
		addRemoveUpgradeButtons $package $latest
	    else
		addRemoveButton $package
	    fi
	fi
    else
	addInstallButton $package
    fi
}

# Print a "Package not installed" message and add the "Install" button
# if enabled in the configuration file.
#
addInstallButton() {
    debug "addInstallButton $*"
    local package="$1"
    local pkgname="$package"
    if test_flag "$INSTALL_BUTTON"; then
	echo "<FORM METHOD=\"GET\" ACTION=\"$SELF\">" \
	     "<INPUT TYPE=\"hidden\" NAME=\"packages\" VALUE=\"$pkgname\">"
	echo -n \
	     "Package $(escape_html "$package") is not installed." \
	     "<INPUT TYPE=\"submit\" NAME=\"install\" VALUE=\"Install\">" \
	     "</FORM>"
    else
	echo ""
	echo "Package $(escape_html "$package") is not installed."
    fi
}

# Add the "Remove" button if enabled in the configuration file.
#
addRemoveButton() {
    debug "addRemoveButton $*"
    local package="$1"
    local pkgname="$package"
    if test_flag "$INSTALL_BUTTON"; then
	echo "<FORM METHOD=\"GET\" ACTION=\"$SELF\">" \
	     "<INPUT TYPE=\"hidden\" NAME=\"packages\" VALUE=\"$pkgname\">"
	echo -n \
	     "<INPUT TYPE=\"submit\" NAME=\"remove\" VALUE=\"Remove\">" \
	     "package $package" \
	     "</FORM>"
    fi
}

# Add the "Upgrade" button if enabled in the configuration file.
#
addUpgradeButton() {
    debug "addUpgradeButton $*"
    local package="$1"
    local latest="$2"
    local pkgname="$package"
    if test_flag "$INSTALL_BUTTON"; then
	echo "<FORM METHOD=\"GET\" ACTION=\"$SELF\">" \
	     "<INPUT TYPE=\"hidden\" NAME=\"packages\" VALUE=\"$pkgname\">"
	echo -n \
	     "<INPUT TYPE=\"submit\" NAME=\"install\" VALUE=\"Upgrade\">" \
	     "package $package to version $latest" \
	     "</FORM>"
    fi
}

# Add the "Remove" and "Upgrade" buttons if enabled in the configuration file.
#
addRemoveUpgradeButtons() {
    debug "addRemoveUpgradeButtons $*"
    local package="$1"
    local latest="$2"
    local pkgname="$package"
    if test_flag "$INSTALL_BUTTON"; then
	echo "<FORM METHOD=\"GET\" ACTION=\"$SELF\">" \
	     "<INPUT TYPE=\"hidden\" NAME=\"packages\" VALUE=\"$pkgname\">"
	echo -n \
	     "<INPUT TYPE=\"submit\" NAME=\"remove\" VALUE=\"Remove\">" \
	     "package $package. " \
	     "<INPUT TYPE=\"submit\" NAME=\"install\" VALUE=\"Upgrade\">" \
	     "to version $latest" \
	     "</FORM>"
    fi
}

# List all packages providing a virtual package.
#
listVirtualPackage() {
    debug "listVirtualPackage $*"
    local package="$1"
    local packages=$(
	cat $DPKG_STATUS $DPKG_AVAILABLE \
	  | awk -v package=$package '
	    /^Package:/ { owner = $2 }
	    /^Provides:/ {
		gsub(",","")
		for (i=2; i<=NF; i++) { if ($i == package) { print owner } }
	    }' \
	  | sort | uniq
    )
    test "$packages" || return 1
    debug "is virtual package"
    echo "<PRE>"
    echo "Virtual package $(escape_html "$package") is provided by:"
    echo "</PRE>"
    listPackages $(echo $packages)
}

listUnknownPackage() {
    debug "listUnknownPackage $*"
    local package="$(escape_html "$1")"
    echo "<PRE>"
    echo "Package $package is not installed and no info is available."
    echo "</PRE>"
}

listPackageFiles() {
    debug "listPackageFiles $*"
    local package="$1"
    echo
    echo "Files owned by package $(escape_html "$package"):"
    echo
    $DPKG -L $package | sort | fileFilter
}

# This is really ugly but I don't know a better way since install-docs
# doesn't record package ownership of documents.
#
listPackageDocumentation() {
    debug "listPackageDocumentation $*"
    local package="$1"
    local installdocs_files
    local document_id
    local f
    test_flag "$LIST_DOCUMENTATION" || return
    test -x /usr/sbin/install-docs || return
    test -e /var/lib/dpkg/info/$package.postinst || return
    installdocs_files=$(
	cat /var/lib/dpkg/info/$package.postinst \
	  | grep 'install-docs[ 	]*-i' \
	  | sed 's/.*install-docs[ 	]*-i[ 	]*//;s/[ 	].*//' \
	  | sort | uniq
    )
    debug "installdocs_files=$installdocs_files"
    test "$installdocs_files" || return
    echo
    echo "Documentation:"
    echo
    for f in $installdocs_files; do
	# Hack for postinsts with `$package' in filenames
	test ! -e $f && f="$(echo "$f" | sed "s/\$package/$package/i")"
	# Get document_id (this can be different than file basename)
	if [ -e $f ]; then
	    document_id="$(grep "^Document:" "$f" | sed 's/.*: //' | head -1)"
	else
	    document_id="${f##*/}"
	fi
	debug "document_id=$document_id"
	title="$(
	    /usr/sbin/install-docs -s "$document_id" 2>/dev/null \
	      | awk '/^Title: / { sub("^Title: *",""); print $0 }'
	)"
	/usr/sbin/install-docs -s "$document_id" 2>/dev/null \
	  | awk '
	    /^Format:/ { format = $2 }
	    /^Index: / { print $2 "\t" format }
	    /^Files: / { if (!match($2,"\\*")) { print $2 "\t" format } }' \
	  | sort | uniq | fileFilter "$title" | sed 's/^/ /'
    done
}

# Find all packages owning a file. Must be able to handle things like:
#
#   /usr/lib/games/maelstrom/Maelstrom Fonts
#   /usr/share/ucblogo/logolib/#
#   /usr/share/ucblogo/logolib/`
#   /etc/diskless-image/config.sh
#   /bin/bash
#   /.*/postfix
#   /.*pbm.*
#
queryOwners() {
    debug "queryOwners $*"
    local file="${1%/}"
    local owners=$(
	# Must grep the exact filename when using dlocate
	$DPKG -S "$file" 2>/dev/null | grep ": $file$" | cut -f1 -d: \
	    | grep -v diversion | tr -d ','
    )
    if [ "$owners" ]; then
	echo "<PRE>"
	echo "Packages owning $(escape_html "$file"):"
	echo "</PRE>"
	listPackages $owners
	return
    fi

    if [ "$DEBIAN_CONTENTS" ]; then
	owners=$(
	    zcat $DEBIAN_CONTENTS 2>/dev/null \
	       | grep "^${file#/}[	 ][ 	]*[^ ]*\$" \
	       | sed "s^${file#/}[	 ][ 	]*; s/,/\\
/"	       | sed 's|.*/||' \
	       | sort | uniq
	)
    fi
    if [ "$owners" ]; then
	echo "<PRE>"
	echo "Packages owning $(escape_html "$file"):"
	echo "</PRE>"
	listPackages $owners
	return
    fi

    echo "<PRE>"
    echo "No package is owning $(escape_html "$file")"
    echo "</PRE>"
}

# Convert package info to html by adding hrefs to other packages.
#
packageFilter() {
    local package="$1"
    local uri=""
    if [ "$DPKG_WWW_BROWSER_HACK" = true -a "$SHOW_LOCAL_FILES" ]; then
	uri="$(packageURI "$package")"
	test "$uri" = "${uri#file:/}" && uri=""
    fi
    awk -v SELF=$SELF -v package="$package" -v uri="$uri" '
	{ gsub("<","\\&lt;"); gsub(">","\\&gt;") }
	/^Package:/ {
	    # Add link to the Debian package page for this package
	    p = s = package; gsub("\\+", "%2B", s)
	    u = "http://packages.debian.org"
	    $2 = sprintf("<a href=\"%s/%s\">%s</a>", u,s,p)
	}
	/^Section:/ {
	    # Add link to package section list
	    s = t = r = $2
	    gsub("\\+", "\\+", r)
	    gsub("\\+", "%2B", s)
	    sub(r, "<a href=\"" SELF "?query=section%3D" s "\">" t "</a>", $2)
	}
	/^Task:/ {
	    # Add links to package tasks list
	    for (f=2; f<=NF; f++) {
		r = $f;
		gsub(",", "", r)
		s = t = r
		gsub("\\+", "\\+", r)
		gsub("\\+", "%2B", s)
		sub(r, "<a href=\"" SELF "?query=task%3D" s "\">" t "</a>", $f)
            }
	}
	/^Maintainer:/ {
	    # Add link to maintainer packages overview
	    m = $0; sub(".*: ","",m);sub(" &lt;.*","",m);gsub("[()]","\\\\&",m)
	    e = $0; sub(".*&lt;","",e);sub("&gt;.*","",e)
	    u = "http://qa.debian.org/developer.php/developer.php?login=" e
	    sub(m, "<a href=\"" u "\">&</a>")

	    # Add link to Debian BTS page for this package
	    s = package; gsub("\\+", "%2B", s)
	    d = "[Debian Bug Report]"
	    u = "http://bugs.debian.org/cgi-bin/pkgreport.cgi"
	    h = sprintf("<a href=\"%s?repeatmerged=no&pkg=%s\">%s</a>", u,s,d)
	    $0 = $0 " " h
	}
	/^(Depends|Pre-Depends|Conflicts|Replaces|Provides|Suggests|\
	  |Recommends|Enhances):/\
	{
	    # Workaround for apt-cache missing space:
	    #   Depends: python-slang(>= 0.2.0), python (>= 2.1)
	    gsub("\\(", " (")
	    for (f=2; f<=NF; f++) {
		if ($f == "|") {
		    continue 
		}
		if (match($f,"^\\(")) {
		    f++;
		    continue 
		}
		r = $f;
		gsub(",", "", r)
		s = t = r
		gsub("\\+", "\\+", r)
		gsub("\\+", "%2B", s)
		sub(r, "<a href=\"" SELF "?query=" s "\">" t "</a>", $f)
            }
        }
	/^Description:/ {
	    if (uri != "") {
		s = package
		t = uri
		gsub("\\+", "%2B", s)
		gsub("^file:", "", t)
		h =  SELF "?browse=Browse&packages=" s
		printf("Filename: <a href=\"%s\">%s</a>\n", h, t)
	    }
	}
	/^Version:/ {
	    if (match($0, "latest is")) {
		# Show latest version in bold
		sub("\\(latest is", "<B>(latest is"); sub("\\)", ")</B>")
	    }
	}
	/^$/ { next}
	/^ .$/ { $0 = " " }
	{ print }'
}

# Convert a dpkg package list to html adding hrefs to package names.
# This is used for both file list and documentation list.
#
listFilter() {
    awk -v SELF=$SELF \
	-v CHECK_BUTTONS=$CHECK_BUTTONS \
	-v DPKG_AVAILABLE=$DPKG_AVAILABLE \
	-v LIST_UNAVAILABLE=$LIST_UNAVAILABLE \
	-v INSTALLED_ONLY=$INSTALLED_ONLY '
	BEGIN {
	    is_hdr  = 1
	    statMax = 3
	    nameMax = 10
	    versMax = 10
	    descMax = 10
	    if (CHECK_BUTTONS) {
		pkg_button = \
		    "<INPUT TYPE=\"checkbox\" NAME=\"packages\" VALUE=\"%s\">"
		install_button = \
		    "<INPUT TYPE=\"submit\" NAME=\"install\" " \
			"VALUE=\"Install selected packages\">"
		reset_button = \
		    "<INPUT TYPE=\"reset\">"
		buttonW = 2
	    }
	}
	/^No packages found matching/ {
	    print $0
	    missing++
	    NR--
	    next
	}
	(is_hdr) {
	    header[NR] = $0
	}
	(is_hdr == 1) && ($1 == "||/") {
	    # ||/ Name           Version        Description
	    hstat  = $1
	    hname  = $2
	    hvers  = $3
	    hdesc  = $4
	    is_hdr = 2
	    next
	}
	(is_hdr == 2) {
	    # +++-==============-==============-===============================
	    split($0,A,"-")
	    statLen = length(A[1])
	    nameLen = length(A[2])
	    versLen = length(A[3])
	    hsep    = A[4] A[4] A[4]
	    is_hdr  = 0
	    next
	}
	(!is_hdr) {
	    # ii  dpkg-www       2.52           package description ...
	    stat = substr($0,1,statLen)
	    name = substr($0,5,nameLen)
	    vers = substr($0,6+nameLen,versLen)
	    desc = substr($0,7+nameLen+versLen)
	    sub("[ ]+$","",name)
	    sub("[ ]+$","",vers)
	    sub("[ ]+$","",desc)
	    if (INSTALLED_ONLY && !match(stat,"^(i.|.c)")) {
	        next
	    }
	    # if (vers == "<none>") { vers = "-" }
	    nameMax = max(nameMax, length(name))
	    versMax = max(versMax, length(vers))
	    descMax = max(descMax, length(desc))
	    packages[NR,"stat"] = stat
	    packages[NR,"name"] = name
	    packages[NR,"vers"] = vers
	    packages[NR,"desc"] = desc
	    if (desc == "(no description available)") {
		needs_desc = missing_desc[name] = NR
	    }
	}
	END {
	    if (needs_desc) {
		update_descs()
	    }
	    if (NR < 6) { 
		if (!missing) {
		    print "no packages found"
		}
		exit
	    }
	    if (missing) {
		print ""
	    }
	    if (CHECK_BUTTONS) {
		printf "<FORM METHOD=\"GET\" ACTION=\"" SELF "\">"
	    }
	    for (i=1; i<=3; i++) {
		print header[i]
	    }
	    descMax = max(descMax,80-statMax-nameMax-versMax-3)
	    printf("%s %s %s %s\n", \
		   pad(hstat,statMax+buttonW), pad(hname,nameMax), \
		   pad(hvers,versMax), hdesc)
	    printf("+++%s-%s-%s-%s\n", substr("--",1,buttonW), \
		   pad(hsep,nameMax), pad(hsep,versMax), pad(hsep,descMax))
	    nr = NR
	    npackages = 0
	    for (i=6; i<=nr; i++) {
		name = packages[i,"name"]
		if (!name) { continue }
		if (CHECK_BUTTONS) {
		    if (substr(packages[i,"stat"],2,1) == "n") {
			stat = pad(packages[i,"stat"],statMax)
			button = sprintf(pkg_button, name",")
			install = 1
		    } else {
			stat = pad(packages[i,"stat"],statMax)
			button = "  "
		    }
		} else {
		    stat = pad(packages[i,"stat"],statMax)
		}
		npad = pad("", nameMax-length(name))
		vers = escape(pad(packages[i,"vers"],versMax))
		desc = escape(packages[i,"desc"])
		if ((LIST_UNAVAILABLE == 0) &&
		    (desc == "(no description available)")) {
		    NR--
		    continue
		}
		href = link = name
		gsub("\\+", "%2B", href)
		href = "<A HREF=\"" SELF "?query=" href "\">" link "</A>" npad
		printf("%s%s %s %s %s\n", stat, button, href, vers, desc)
		npackages++
	    }
	    if (npackages > 0) {
		npad = statMax+2+buttonW+nameMax
		print ""
		printf(pad(sprintf("%d packages", npackages), npad))
	    }
	    if (CHECK_BUTTONS) {
		if (install) {
		    printf reset_button "  " install_button "\n"
		}
		printf "</FORM>"
	    }
	}
	function escape(s) {
	    gsub("&","\\&amp;",s)
	    gsub("<","\\&lt;",s)
	    gsub(">","\\&gt;",s)
	    return s
	}
	function pad(s,l) {
	    while (length(s) < l) {
		s = s "          "
	    }
	    return substr(s,1,l)
	}
	function max(x,y) {
	    return ((x > y) ? x : y)
	}
	function update_descs() {
	    while (getline < DPKG_AVAILABLE) {
		if ($1 == "Package:") {
		    if ((nr=missing_desc[$2])) {
			package = $2
		    } else {
			package = ""
		    }
		}
		if (package && ($1 == "Version:")) {
		    sub("Version: ",""); vers = $0
		    packages[nr,"vers"] = vers
		    versMax = max(versMax, length(vers))
		}
		if (package && ($1 == "Description:")) {
		    sub("Description: ",""); desc = $0
		    packages[nr,"desc"] = desc
		    descMax = max(descMax, length(desc))
		}
	    }
	}'
}

# Convert a file list to html adding hrefs to files in /usr/{doc,info,man}.
#
fileFilter() {
    awk -v title="$*" -v show_local_files="$SHOW_LOCAL_FILES" \
	-v man="$MAN" -v info2www="$INFO2WWW_PATH" -v dwww="$DWWW_PATH" '
	/\/usr\/(share\/)?([a-zA-Z0-9_+-]+\/)?man\/.*\..*/ {
	    if (title) { t = title } else { t = $1 }
	    if ($2) { t = t " [" $2 "]"; NF=1 }
	    r = s = $1
	    gsub("\\+", "\\+", r);
	    gsub("\\+", "%2B", s);
	    gsub("\\$","\\$",r)
	    sub(r, "<A HREF=\"" man s "\">" t "</A>")
	    print
	    next
	}
	/\/usr\/(share\/)?info\/.*\.gz/ {
	    if (title) { t = title } else { t = $1 }
	    if ($2) { t = t " [" $2 "]"; NF=1 }
	    r = s = $1
	    gsub("\\+", "\\+", r);
	    gsub("\\+", "%2B", s);
	    gsub("\\$","\\$",r)
	    sub(r, "<A HREF=\"" info2www "?" s "\">" t "</A>")
	    print
	    next
	}
	/\/usr\/(doc|share\/(doc|gtk-doc|gnome\/(html|help)|RFC))\// {
	    if (title) { t = title } else { t = $1 }
	    if ($2) { t = t " [" $2 "]"; NF=1 }
	    r = s = $1
	    gsub("\\+", "\\+", r);
	    gsub("\\+", "%2B", s);
	    gsub("\\$","\\$",r)
	    if (show_local_files == "true") {
		sub(r, "<A HREF=\"file:" s "\">" t "</A>")
	    } else {
		sub(r, "<A HREF=\"" dwww "?type=file\\&location=" s "\">"\
		       t "</A>")
	    }
	    print
	    next
	}
	/^\/.$/ { next }
	/^$/ { next }
	/^[^\/]/ { sub("^","  ") }
	{ print }'
}

packageURI() {
    local package="$1"
    local pkgcache=$(tempfile)
    local srcpkgcache=$(tempfile)
    trap "rm -f $pkgcache $srcpkgcache" 0
    apt-get -o "Debug::NoLocking=1" \
	    -o "Dir::Cache=/tmp" \
	    -o "Dir::Cache::pkgcache=$pkgcache" \
	    -o "Dir::Cache::srcpkgcache=$srcpkgcache" \
	    -o "Dir::Cache::archives=/var/cache/apt/archives" \
	    install --reinstall --print-uris -y "$package" \
      | grep -F "/${package}_" \
      | sed "/^'/!d; s/^'//; s/'.*//"
    rm -f $pkgcache $srcpkgcache 2>/dev/null
}

printInstallRequest() {
    local packages="$1"
    local host="$HOSTNAME_FULL"
    cat <<-EOF
	<!--
	Install-host: $host
	Install-packages: $packages
	-->
	<H1>dpkg-www WEB Installation</H1>
	<P>
	If you are reading this document on your browser or you have
	been asked to save it to disk instead of running the installation
	of the required packages this means that your browser has not
	been set up correctly for dpkg-www WEB installation.
	See <A HREF="${MAN}${HELP_PAGE}">dpkg-www(8)</A>
	man page for more information.
	</P>
EOF
}

printRemoveRequest() {
    local packages="$1"
    local host="$HOSTNAME_FULL"
    cat <<-EOF
	<!--
	Remove-host: $host
	Remove-packages: $packages
	-->
	<H1>dpkg-www WEB Deinstallation</H1>
	<P>
	If you are reading this document on your browser or you have
	been asked to save it to disk instead of running the installation
	of the required packages this means that your browser has not
	been set up correctly for dpkg-www WEB installation.
	See <A HREF="${MAN}${HELP_PAGE}">dpkg-www(8)</A>
	man page for more information.
	</P>
EOF
}

printBrowseRequest() {
    local package="$1"
    local host="$HOSTNAME_FULL"
    cat <<-EOF
	<!--
	Browse-package: $package
	-->
	<H1>dpkg-www Browse Request</H1>
	<P>
	If you are reading this document on your browser or you have
	been asked to save it to disk instead of running a browser
	on the required packages this means that your browser has not
	been set up correctly for dpkg-www WEB installation.
	See <A HREF="${MAN}${HELP_PAGE}">dpkg-www(8)</A>
	man page for more information.
	</P>
EOF
}

# Get the value of a query argument
getValue() {
    local key="$1"
    test -x /usr/bin/perl || return
    perl -e "use CGI; my \$q = new CGI(\$_); print scalar \$q->param('$key')" "$1"
}

escape_html() {
    echo "$*" | sed 's/&/\&amp;/g; s/\"/\&quot;/g; s/</\&lt;/g; s/>/\&gt;/g'
}

printHtmlHeader() {
    local title="${1:-Debian Packages}"
    local content_type="${2:-text/html; charset=UTF-8}"
    test "$html_header_done" && return
    html_header_done=true
    echo "Content-type: $content_type"
    echo "Connection: close"
    echo ""
    if [ "$content_type" ]; then
	echo "<HTML>"
	echo "<HEAD>"
	echo "<TITLE>$title</TITLE>"
	echo "</HEAD>"
	echo "<BODY BGCOLOR=\"${BGCOLOR:-#c0c0c0}\">"
    fi
}

# Generate an input form or the <isindex> tag
printInputForm() {
    local default
    local installed
    test "$noheader" = true && return
    test "$input_form_done" = true  && return
    test "$KEEP_INPUT" = 1 && default="$query"
    test "$INSTALLED_ONLY" = 1 && installed="checked"
    echo "<H1>Debian packages on $HOSTNAME</H1>"
    if [ -x /usr/bin/perl ]; then
	echo '<P>'
	if [ "${HTTP_USER_AGENT#Lynx}" != "$HTTP_USER_AGENT" ]; then
	   echo "<FORM METHOD=\"GET\" ACTION=\"$SELF\">"
	    echo "Search:"
	    echo '<INPUT NAME="query" SIZE="42" VALUE="'"$default"'">'
	   echo '<INPUT NAME="search" TYPE="submit" VALUE="Submit">'
	   echo '(?&nbsp;for&nbsp;help)<BR>'
	   echo '<INPUT NAME="installed" TYPE="checkbox"' $installed '>' \
		'only installed packages' '<br>&nbsp;<br>'
	   echo '</FORM>'
	else
	   echo "<FORM METHOD=\"GET\" ACTION=\"$SELF\">"
	   echo '<INPUT NAME="search" TYPE="submit" VALUE="  Search  ">'
	    echo '<INPUT NAME="query" SIZE="50" VALUE="'"$default"'">'
	   echo '<INPUT NAME="help" TYPE="submit" VALUE="   Help   ">'
	   echo '<INPUT NAME="installed" TYPE="checkbox"' $installed '>' \
		'only installed packages'
	   echo '</FORM>'
	   test "$query" != " " && echo '<HR WIDTH="100%">'
	fi
	echo '</P>'
    else
	echo '<P>'
	echo '<ISINDEX>'
	echo '</P>'
    fi
    input_form_done=true
}

# Print bottom info
printHtmlBottom() {
    test "$noheader" = true && return
    cat <<- EOF
	<P>
	<HR WIDTH="100%">
	<ADDRESS>
	Generated by <A HREF="$SELF?query=dpkg-www">
	dpkg-www $PROG_VERSION</A> - Copyright (C) 1999-2006
	<A HREF="http://people.debian.org/~dz/">Massimo Dal Zotto</A>
	<A HREF="mailto:dz@debian.org">&lt;dz@debian.org&gt;</A>
	</ADDRESS>
	<BR>
	</P>
        $(test "${tempfile}" && echo "<PRE>" && cat $tempfile | sed 's/</\&lt;/g' && echo "</PRE>")
	</BODY>
	</HTML>
EOF
}

printError() {
    local message="$*"
    printHtmlHeader "$title"
    printInputForm
    echo "<P>"
    echo "$(escape_html "$message")"
    echo "<P>"
    usage
    printHtmlBottom
}

#------------------------------------------------------------------------------

# Get the query arguments
if [ "$*" ]; then
    # ISINDEX or interactive invocation
    debug=""
    query="$*"
    help=""
    install=""
    remove=""
    browse=""
    packages=""
    bgcolor=""
    noheader=""
    installed=""
else
    # GET method
    debug="$(getValue debug)"
    query="$(getValue query)"
    help="$(getValue help)"
    install="$(getValue install)"
    remove="$(getValue remove)"
    browse="$(getValue browse)"
    packages="$(getValue packages)"
    bgcolor="$(getValue bgcolor)"
    noheader="$(getValue noheader)"
    installed="$(getValue installed)"
fi
test "$help"    != "" && query="?"
test "$debug"   != "" && DEBUG="$debug"
test "$bgcolor" != "" && BGCOLOR="$bgcolor"

# Print debugging info
if test_flag "$DEBUG"; then
    printHtmlHeader "$title"
    cat <<- EOF
	<PRE>
	$(env | sort)
	SELF=$SELF
	DPKG=$DPKG
	SHOW_LOCAL_FILES=$SHOW_LOCAL_FILES
	LIST_DOCUMENTATION=$LIST_DOCUMENTATION
	CHECK_BUTTONS=$CHECK_BUTTONS
	INSTALL_BUTTON=$INSTALL_BUTTON
	PROG=$0
	ARGC=$#
	ARGV="$(escape_html "$*")"
	query="$(escape_html "$query")"
	install="$(escape_html "$install")"
	remove="$(escape_html "$remove")"
	browse="$(escape_html "$browse")"
	packages="$(escape_html "$packages")"
	bgcolor="$(escape_html "$bgcolor")"
	noheader="$(escape_html "$noheader")"
	installed="$(escape_html "$installed")"
	debug="$(escape_html "$debug")"
	</PRE>
EOF
    tempfile=$(tempfile); trap "rm -f $tempfile" 0
    exec 2>$tempfile
    set -x
fi

# Sanity check on input values. $query is checked in doQuery.
checkInput "$install$remove$packages$debug"

if [ "$install" -a "$remove" ]; then
    printError "Error: invalid request"
    exit
fi
if [ "$install$remove$browse" -a "$packages" = "" ]; then
    printError "Error: invalid request"
    exit
fi
if [ "$install$remove$browse" -a "$query" != "" ]; then
    printError "Error: invalid request"
    exit
fi
if [ "$install" -a "$install" != "Install" -a "$install" != "Upgrade" ]; then
    printError "Error: invalid request"
    exit
fi
if [ "$remove" -a "$remove" != "Remove" ]; then
    printError "Error: invalid request"
    exit
fi
if [ "$browse" -a "$browse" != "Browse" ]; then
    printError "Error: invalid request"
    exit
fi
if [ "$installed" != "" ]; then
    if test_flag "$installed"; then
       INSTALLED_ONLY=1
    else
       INSTALLED_ONLY=0
    fi
fi

title="Debian Packages${query:+: $query}"

if [ "$install" ]; then
    printHtmlHeader "Package Installation" application/dpkg-www-installer
    printInstallRequest "$packages"
    printHtmlBottom
elif [ "$remove" ]; then
    printHtmlHeader "Package Deinstallation" application/dpkg-www-installer
    printRemoveRequest "$packages"
    printHtmlBottom
elif [ "$browse" ]; then
    printHtmlHeader "Package Browser" application/dpkg-www-browser
    printBrowseRequest "$packages"
    printHtmlBottom 
else
    printHtmlHeader "$title"
    printInputForm
    doQuery "$query"
    printHtmlBottom
fi

# end of file
