#!/bin/bash
#
# CGI interface to dpkg. Show information about debian packages.
#
# Copyright © 1999-2006 Massimo Dal Zotto <dz@debian.org>
# Copyright © 2017-2022 Guillem Jover <guillem@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, see <https://www.gnu.org/licenses/>.

PROG_VERSION=2.x
SELF=${0##*/}; export SELF
HELP_PAGE="/usr/share/man/man8/dpkg-www.8.gz"
COLUMNS=256; export COLUMNS
SECTION_DESCRIPTIONS_FILE="/usr/share/dpkg-www/debian-sections"

# Configuration defaults, can be overridden 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
TASK_DESCRIPTIONS_FILE=/usr/share/tasksel/descs/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 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 command -v dlocate >/dev/null; then
	DPKG=dlocate
    else
	DPKG=dpkg-query
    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>wildcard 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=dpkg"><tt>dpkg<tt></a>
	    <td> list verbosely the package dpkg
	  <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=/.*/README"><tt>/.*/README</tt></a>
	    <td> list packages owning any file named "README" (1)
	  <tr><td>
	    <td> <a href="$SELF?query=/.*txt.*"><tt>/.*txt.*</tt></a>
	    <td> list packages owning any file matching *txt* (1)
	  <tr><td>
	    <td> <a href="$SELF?query=maintainer%3DDpkg+Developers"
	         ><tt>maintainer=Dpkg Developers</tt></a>
	    <td> list packages with maintainer Dpkg Developers (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 the 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() {
    local field="${*%%=*}"
    local 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" != "" ] && ! command -v grep-dctrl >/dev/null; 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 command -v grep-dctrl >/dev/null; then
	    ( dpkg-query -s; dpkg-query -p ) \
	       | grep-dctrl $GREP_DCTRL_OPTS -s Package -F "$field" "$arg"
	else
	    apt-cache search "$arg" | sed 's/ .*//;s/^/Package: /'
	fi
	dpkg-query -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() {
    local sections
    sections=$(
	( dpkg-query -s; dpkg-query -p ) | grep -h "^Section:" \
	   | sed 's|.*:[ ]*||; s|.*/||' | sort | uniq | grep -v "^non-"
    )
    echo "<pre>"
    echo "Available sections:"
    echo ""
    for s in $sections; do
        local desc
	desc=$(
	    sed -e "/^$s/!d; s/^$s[	 ]*//;q" "$SECTION_DESCRIPTIONS_FILE"
	)
	desc=${desc:="no description available"}
        local x
	x=$(printf "%$[16-${#s}]s" " ")
	echo "  <a href=\"${SELF}?query=section%3D${s}\">${s}</a>${x} ${desc}"
    done
    echo "</pre>"
}

# List available tasks
listTasks() {
    echo "<pre>"
    echo "Available tasks:"
    echo ""
    ( dpkg-query -s; dpkg-query -p ) \
      | TASK_DESCRIPTIONS_FILE=$TASK_DESCRIPTIONS_FILE \
        perl -MDpkg::Control -MDpkg::Index -E '
        my $tasks = Dpkg::Index->new(get_key_func => sub { return $_[0]->{Task} });
        $tasks->load($ENV{TASK_DESCRIPTIONS_FILE});
        my %pkg2task;
        foreach my $task ($tasks->get()) {
          next unless exists $task->{Key};
          my $name = $task->{Key} =~ s/\s*//gr;
          $pkg2task{$name} = $task->{Task};
        }
        my $idx = Dpkg::Index->new(type => CTRL_FILE_STATUS);
        $idx->load("-");
        foreach my $ctrl ($idx->get(Section => "tasks")) {
          my $pkg = $ctrl->{Package};
          my $task = $pkg2task{$pkg};
          my $desc = (split "\n", $ctrl->{Description})[0];
          my $pad = 16 - length $task;
          say "  <a href=\"$ENV{SELF}?query=$pkg\">$task</a>" . " " x $pad . " ${desc}";
        }
      '
    echo "</pre>"
}

# List recent packages
listRecentPackages() {
    local n=${1:-1}
    local s
    s=$(
	N=$n perl -MTime::Piece -MTime::Seconds -E '
	my $o = localtime->truncate(to => "day") -
	Time::Seconds->new($ENV{N} * ONE_DAY);
	print $o->epoch;'
    )
    local packages
    packages=$(
	dpkg-query -f '${db-fsys:Last-Modified} ${binary:Package}\n' -W \
	   | S=$s perl -n -E '(@field) = split " ";
		print "$field[1] " if $field[0] > $ENV{S};'
    )
    if [ "$packages" ]; then
	echo "<pre>"
	echo "Recent packages in last $n days:"
	echo ""
	dpkg-query -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-query -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

    local pkg_info
    pkg_info="$(dpkg-query -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 command -v apt-cache >/dev/null; 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-query --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=$(
	( dpkg-query -s; dpkg-query -p ) \
	  | 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
    dpkg-query --control-list $package | grep -q postinst || return
    installdocs_files=$(
	dpkg-query --control-show $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 = "https://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 = "https://qa.debian.org/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 = "https://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"
    command -v perl >/dev/null || 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 command -v perl >/dev/null; 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>
	</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
