#!/usr/bin/perl -w

##############################################################################
#
# Print billing management system - admin tools, version 4.1.2
#
# Copyright (C) 2000, 2001, 2002, 2003 Daniel Franklin
#
# This program is distributed under the terms of the GNU General Public
# License Version 2.
#
# This is the admin tool for managing the various printbill databases.
#
##############################################################################

use Printbill::PTDB_File;
use Printbill::printbill_pcfg;
use Getopt::Long;
use POSIX;
use Fcntl qw(:DEFAULT :flock);
use strict;

my (%params, $config, %printers, %opt);
my ($INCUSER, $DECUSER, $SETUSER, $INFINITISEUSER, $DEINFINITISEUSER, $AMOUNT, $ADDUSER, $DELUSER, $DISPLAYUSER, $PAGESUSER, $PAIDUSER, $SPENTUSER, $USEDPRINTER, $PPAGESPRINTER, $COLOURSPACEPRINTER, $CLEARBLACKPRINTER, $CLEARCOLOURPRINTER, $CLEARPAGESPRINTER, $WEBADDUSER, $WEBPASSWDUSER, $WEBDELUSER, $WEBNONINTUSER, $WEBNONINTPASSWD, %locks);

sub usage {
	print "
pqm - manage printbill database file. Options:

	--inc user --amount N
		increments quota by $params{'currency_symbol'}N (N need not be an integer)
		
	--dec user --amount N
		decrements quota by $params{'currency_symbol'}N (N need not be an integer)

	--set user --amount N
		sets user's quota to $params{'currency_symbol'}N (N need not be an integer)
	
	--add user
		adds user to database
	
	--del user
		removes user from database
		
	--infinitise user
		gives user an infinite print quota (god mode for admins)
		
	--deinfinitise user
		returns user to status of ordinary mortal
	
	--init
		remove all users, reset page counter (DANGER!)

	--display [user]
		displays info for all users [or for user]
	
	--pages [user]
		shows total pages printed [or those by user]
	
	--spent [user]
		shows total spent [or spent by user]

	--paid [user]
		shows total paid [or paid by user]

	--used [printer]
		shows estimated \% of black [and colour] used

	--ppages [printer]
		shows total pages printed on 'printer'

	--colourspace [printer]
		shows colourspace of 'printer'

	--resetpages [printer]
		 resets printer page count for all or just for 'printer'

	--resetblack [printer]
		you bought a new black ink/toner cartridge for 'printer'

	--resetcolour [printer]
		you bought a new colour ink/toner cartridge for 'printer'

	--updateprinters
		Create printer databases for any new printers in printcap

	--webadduser user
		add a webpqadmin user

	--webdeluser user
		remove a webpqadmin user
	
	--webpasswd user
		interactively set password for a webpqadmin user

	--webnoninter user --passwd password
		non-interactively set password for a webpqadmin user

	--web
		(in combination with some of the print commands) print out a
		bare minimum, for the CGI script (does nothing on its own)

	--ask_no_questions
		don't prompt before doing something dangerous

	--sloppy
		don't check that an account is real

	--help
		print out this help

	--version
		print out the version number

";

}

$SIG{TERM} = \&catch_zap;
$SIG{HUP} = \&catch_zap;
$SIG{INT} = \&catch_zap;

$config = '/etc/printbill/printbillrc';

%params = pcfg ($config);

die "$0: problems parsing configuration file\n" if (! scalar (%params));

%printers = pcfg_printers ($config);

die "$0: problems parsing printer-specific part of configuration file\n" if (! scalar (%printers));

unless (GetOptions (\%opt, # all non-linked options go into %opt
#
#	option			linkage
#
	'inc=s',		\$INCUSER,
	'dec=s',		\$DECUSER,
	'set=s',		\$SETUSER,
	'amount=f',		\$AMOUNT,
	'dollars=f',		\$AMOUNT,
	'add=s',		\$ADDUSER,
	'del=s',		\$DELUSER,
	'infinitise=s',		\$INFINITISEUSER,
	'deinfinitise=s',	\$DEINFINITISEUSER,
	'init!',

	'display:s',		\$DISPLAYUSER,
	'pages:s',		\$PAGESUSER,
	'paid:s',		\$PAIDUSER,
	'spent:s',		\$SPENTUSER,

	'used:s',		\$USEDPRINTER,
	'ppages:s',		\$PPAGESPRINTER,
	'colourspace:s',	\$COLOURSPACEPRINTER,
	'resetpages:s',		\$CLEARPAGESPRINTER,
	'resetblack:s',		\$CLEARBLACKPRINTER,
	'resetcolour:s',	\$CLEARCOLOURPRINTER,
	'updateprinters!',
	
	'webadduser=s',		\$WEBADDUSER,
	'webdeluser=s',		\$WEBDELUSER,
	'webpasswd=s',		\$WEBPASSWDUSER,
	'webnonintpasswd=s',	\$WEBNONINTUSER,
	'passwd=s',		\$WEBNONINTPASSWD,

	'web!',
	'ask_no_questions!',
	'sloppy!',
	'help!',
	'version!'
)) {
	&usage;
}

if ($opt{help}) {
	&usage;
} elsif ($opt{version}) {
	print "pqm version 4.1.2\n";
} elsif ($opt{updateprinters}) {
	&updateprinters;
} elsif (defined ($INCUSER) && !defined ($DECUSER) && !defined ($SETUSER) && defined ($AMOUNT)) {
	&incuser ($INCUSER, $AMOUNT);
} elsif (defined ($DECUSER) && !defined ($INCUSER) && !defined ($SETUSER) && defined ($AMOUNT)) {
	&decuser ($DECUSER, $AMOUNT);
} elsif (defined ($SETUSER) && !defined ($INCUSER) && !defined ($DECUSER) && defined ($AMOUNT)) {
	&setuser ($SETUSER, $AMOUNT);
} elsif (defined ($ADDUSER)) {
	&adduser ($ADDUSER);
} elsif (defined ($DELUSER)) {
	&deluser ($DELUSER);
} elsif (defined ($INFINITISEUSER)) {
	&infinitise ($INFINITISEUSER);
} elsif (defined ($DEINFINITISEUSER)) {
	&deinfinitise ($DEINFINITISEUSER);
} elsif (defined ($DISPLAYUSER)) {
	&display ($DISPLAYUSER);
} elsif (defined ($PAGESUSER)) {
	&pages ($PAGESUSER);
} elsif (defined ($SPENTUSER)) {
	&spent ($SPENTUSER);
} elsif (defined ($PAIDUSER)) {
	&paid ($PAIDUSER);
} elsif (defined ($USEDPRINTER)) {
	&used ($USEDPRINTER);
} elsif (defined ($CLEARBLACKPRINTER)) {
	&resetblack ($CLEARBLACKPRINTER);
} elsif (defined ($CLEARCOLOURPRINTER)) {
	&resetcolour ($CLEARCOLOURPRINTER);
} elsif (defined ($PPAGESPRINTER)) {
	&ppages ($PPAGESPRINTER);
} elsif (defined ($CLEARPAGESPRINTER)) {
	&resetpages ($CLEARPAGESPRINTER);
} elsif (defined ($COLOURSPACEPRINTER)) {
	&colourspace ($COLOURSPACEPRINTER);
} elsif ($opt{init}) {
	&init;
} elsif (defined $WEBADDUSER) {
	&webadduser ($WEBADDUSER);
} elsif (defined $WEBDELUSER) {
	&webdeluser ($WEBDELUSER);
} elsif (defined $WEBPASSWDUSER) {
	&webpasswd ($WEBPASSWDUSER);
} elsif (defined $WEBNONINTUSER && defined $WEBNONINTPASSWD) {
	&webnonintpasswd ($WEBNONINTUSER, $WEBNONINTPASSWD);
} else {
	&usage;
}

sub updateprinters
{
	my (@printers, @pparams, $pparam, %pparamhash, $printer, $printer_name, @line, $var, $val, %printerhash, $uid, $gid);
	my $printcap = `cat $params{'printcap'}`;
	
	if ($? >> 8) {
		die_cleanup (-1, "$0: cannot read $params{'printcap'}\n");
	}

	$printcap =~ s/#.*//g;
	$printcap =~ s/\n+\s*:\s*/:/g;
	$printcap =~ s/\n+//;
	$printcap =~ s/\n+/\n/g;
	$printcap =~ s/:\\*:/:/g;
	$printcap =~ s/:\n/\n/g;

	@printers = split ("\n", $printcap);
	
	foreach $printer (@printers) {
		@pparams = split (":", $printer);
		$printer_name = $pparams [0];

# Get rid of alternative names
		$printer_name =~ s/\|.*//;
		
		print "Checking printer \"$printer_name\"... ";

		%pparamhash = ();

		foreach $pparam (@pparams) {
			@line = split ("=", $pparam);

			if ($#line == 1) {
				$var = $line [0];
				$val = $line [1];
				$pparamhash{$var} = $val;
			}	
		}

# Add any new printers which don't currently have a database entry.

		if (defined ($pparamhash{"achk"}) && ($pparamhash{"achk"} =~ /true/i)
			&& defined ($pparamhash{"as"}) && ($pparamhash{"as"} =~ /--type/) && ($pparamhash{"as"} =~ /bill|lazybill|accountonly/)) {

			if (-e "$params{'db_home'}/printers/$printer_name.db") {
				print "database already exists - ignoring.\n";
			} else {
				print "adding new printer \"$printer_name\".\n";
			
				if (!defined ($printers{$printer_name})) {
					%{$printers{$printer_name}} = %{$printers{'default'}};
				}

				&lock ("printer_$printer_name");
	
				tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer_name.db", "FALSE" or
					die_cleanup (-1, "$0: Looks like $params{'db_home'}/printers/$printer_name.db is not writable!\n");
			
				%printerhash = ();
			
				$printerhash{'total pages'} = 0;
			
				$printerhash{'colourspace'} = $printers{$printer_name}{'colourspace'};
			
				if ($printers{$printer_name}{'colourspace'} ne 'mono') {
					$printerhash{'estimated cyan'} = 0;
					$printerhash{'estimated magenta'} = 0;
					$printerhash{'estimated yellow'} = 0;
				}
				
				if ($printers{$printer_name}{'colourspace'} ne 'cmy') {
					$printerhash{'estimated black'} = 0;
				}

				untie %printerhash;
			
				$uid = (getpwnam ($params{'printbilld_user'}))[2];
		
				if (defined $params{'printbilld_group'}) {
					$gid = getgrnam ($params{'printbilld_group'});
				} else {
					$gid = (getpwnam ($params{'printbilld_user'}))[3];
				}

				chown $uid, $gid, "$params{'db_home'}/printers/$printer_name.db";

				chmod 0664, "$params{'db_home'}/printers/$printer_name.db";

				&unlock ("printer_$printer_name");
			}
		} else {
			print "not accounting - ignoring.\n";
		}
	}

}

sub incuser
{
	my ($user, $amount) = @_;
	my (%userhash, %mischash, $identity, $gid, $uid, $t, $lt);
	
	&lock ("user_$user");

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "FALSE" or
		&die_cleanup (-1, "$0: cannot open $params{'db_home'}/users/$user.db: $!\n");
	
	&lock ("misc");	
	
	tie %mischash, "Printbill::PTDB_File", "$params{'db_home'}/misc.db", "FALSE" or
		&die_cleanup (-1, "$0: cannot open $params{'db_home'}/misc.db: $!\n");
	
	if (!defined ($amount)) {
		print "$0: you need to specify user and an amount with --amount\n";
	}

# If the user doesn't exist, create him/her
	
	if (!defined ($userhash{'quota'})) {
		if (!$opt{sloppy}) {
			$identity = (getpwnam ($user)) [0];
		
			if (!defined ($identity) || $identity ne $user) {
				untie %userhash;
# Delete the bogus user's database
				`/bin/rm $params{'db_home'}/users/$user.db`;
# Don't bother testing $? - we'd just have to do the same anyway :)
				untie %mischash;
				die_cleanup (-1, "$0: user $user does not have a valid UNIX account - see the system administrator!\n")
			}
		}

		$userhash{'quota'} = 0;
		$userhash{'spent'} = 0;
		$userhash{'pages'} = 0;
		$userhash{'cyan'} = 0;
		$userhash{'magenta'} = 0;
		$userhash{'yellow'} = 0;

		$uid = (getpwnam ($params{'printbilld_user'}))[2];
		
		if (defined $params{'printbilld_group'}) {
			$gid = getgrnam ($params{'printbilld_group'});
		} else {
			$gid = (getpwnam ($params{'printbilld_user'}))[3];
		}

		chown $uid, $gid, "$params{'db_home'}/users/$user.db";
		chmod 0664, "$params{'db_home'}/users/$user.db";
	}

	$userhash{'quota'} += $amount;
	$mischash{'total paid'} += $amount;
	
	printf ("Incrementing %s\'s quota by $params{'currency_symbol'}%.2f to $params{'currency_symbol'}%.2f.\n", $user, $amount, $userhash{'quota'});

	untie %userhash;
	untie %mischash;
	
	if (defined ($params{'financelog'})) {
		open FINLOG, ">>$params{'financelog'}"
			or die_cleanup (-1, "$0: Cannot open financial transaction log $params{'financelog'}.\n");

		$t = time;
		$lt = localtime (time);

		print FINLOG "$lt	$t	$amount\n";

		close FINLOG;
	}
	
	&unlock ("user_$user");
	&unlock ("misc");
}

sub decuser
{
	my ($user, $amount) = @_;
	my (%userhash, %mischash, $identity, $t, $lt);
	
	&lock ("user_$user");

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "FALSE" or
		&die_cleanup (-1, "$0: cannot open $params{'db_home'}/users/$user.db: $!\n");
	
	&lock ("misc");	
	
	tie %mischash, "Printbill::PTDB_File", "$params{'db_home'}/misc.db", "FALSE" or
			&die_cleanup (-1, "$0: cannot open $params{'db_home'}/misc.db: $!\n");

# If the user doesn't exist, create him/her

	if (!defined ($userhash{'quota'})) {
		if (!$opt{sloppy}) {
			$identity = (getpwnam ($user)) [0];
		
			if (!defined ($identity) || $identity ne $user) {
				untie %userhash;
# Delete the bogus user's database
				`/bin/rm $params{'db_home'}/users/$user.db`;
# Don't bother testing $? - we'd just have to do the same anyway :)
				untie %mischash;
				die_cleanup (-1, "$0: user $user does not have a valid UNIX account - see the system administrator!\n");
			}
		}

		$userhash{'quota'} = 0;
		$userhash{'spent'} = 0;
		$userhash{'pages'} = 0;
		$userhash{'cyan'} = 0;
		$userhash{'magenta'} = 0;
		$userhash{'yellow'} = 0;
		$userhash{'black'} = 0;
	}

	$userhash{'quota'} -= $amount;
	$mischash{'total paid'} -= $amount;

	printf ("Decrementing %s\'s quota by $params{'currency_symbol'}%.2f to $params{'currency_symbol'}%.2f.\n", $user, $amount, $userhash{'quota'});

	untie %userhash;
	untie %mischash;

	if (defined ($params{'financelog'})) {
		open FINLOG, ">>$params{'financelog'}"
			or die_cleanup (-1, "$0: Cannot open financial transaction log $params{'financelog'}.\n");

		$t = time;
		$lt = localtime (time);

		print FINLOG "$lt	$t	-$amount\n";

		close FINLOG;
	}
	
	&unlock ("user_$user");
	&unlock ("misc");
}

sub setuser
{
	my ($user, $amount) = @_;
	my (%userhash, %mischash, $identity, $delta, $t, $lt);
	
	&lock ("user_$user");

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "FALSE" or
		&die_cleanup (-1, "$0: cannot open $params{'db_home'}/users/$user.db: $!\n");
	
	&lock ("misc");	
	
	tie %mischash, "Printbill::PTDB_File", "$params{'db_home'}/misc.db", "FALSE" or
			&die_cleanup (-1, "$0: cannot open $params{'db_home'}/misc.db: $!\n");

# If the user doesn't exist, create him/her

	if (!defined ($userhash{'quota'})) {
		if (!$opt{sloppy}) {
			$identity = (getpwnam ($user)) [0];
		
			if (!defined ($identity) || $identity ne $user) {
				untie %userhash;
# Delete the bogus user's database
				`/bin/rm $params{'db_home'}/users/$user.db`;
# Don't bother testing $? - we'd just have to do the same anyway :)
				untie %mischash;
				die_cleanup (-1, "$0: user $user does not have a valid UNIX account - see the system administrator!\n");
			}
		}

		$userhash{'quota'} = 0;
		$userhash{'spent'} = 0;
		$userhash{'pages'} = 0;
		$userhash{'cyan'} = 0;
		$userhash{'magenta'} = 0;
		$userhash{'yellow'} = 0;
		$userhash{'black'} = 0;
	}

	$delta = $amount - $userhash{'quota'};
	$mischash{'total paid'} += $delta;

	if (defined ($params{'financelog'})) {
		open FINLOG, ">>$params{'financelog'}"
			or die_cleanup (-1, "$0: Cannot open financial transaction log $params{'financelog'}.\n");

		$t = time;
		$lt = localtime (time);

		print FINLOG "$lt	$t	$delta\n";

		close FINLOG;
	}
	
	$userhash{'quota'} = $amount;

	printf ("Setting %s\'s quota to $params{'currency_symbol'}%.2f.\n", $user, $userhash{'quota'});

	untie %userhash;
	untie %mischash;

	&unlock ("user_$user");
	&unlock ("misc");
}

sub adduser
{
	my ($user) = @_;
	my (%userhash, $identity, $uid, $gid);
	
	&lock ("user_$user");

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "FALSE"
		or die_cleanup (-1, "$0: cannot open $params{'db_home'}/users/$user.db for writing (check permissions).\n");

	if (defined ($userhash{'quota'})) {
		untie %userhash;
		die_cleanup (-2, "$0: user $user already exists in the database!\n");
	}

	if (! $opt{sloppy}) {
		$identity = (getpwnam ($user)) [0];
	
		if (!defined ($identity) || $identity ne $user) {
			untie %userhash;
# Delete the bogus user's database
			`/bin/rm $params{'db_home'}/users/$user.db`;
# Don't bother testing $? - we'd just have to do the same anyway :)
			die_cleanup (-1, "$0: user $user does not have a valid UNIX account - see the system administrator!\n");
		}
	}
	
	print "User $user added to database, initial credit = $params{'currency_symbol'}0.00\n";
	
	$userhash{'quota'} = 0;
	$userhash{'spent'} = 0;
	$userhash{'pages'} = 0;
	$userhash{'cyan'} = 0;
	$userhash{'magenta'} = 0;
	$userhash{'yellow'} = 0;
	$userhash{'black'} = 0;
	
	untie %userhash;

# Are we running as the web user or as root?
	
	if ($< == (getpwnam ($params{'web_user'}))[2]) {
		$uid = $<;
	} else {
		$uid = (getpwnam ($params{'printbilld_user'}))[2];
	}
	
	if (defined $params{'printbilld_group'}) {
		$gid = getgrnam ($params{'printbilld_group'});
	} else {
		$gid = (getpwnam ($params{'printbilld_user'}))[3];
	}
	
	chown $uid, $gid, "$params{'db_home'}/users/$user.db"
		or die_cleanup (-1, "$0: Running as $<, but not a member of group $gid!\n");
		
	chmod 0664, "$params{'db_home'}/users/$user.db";

	&unlock ("user_$user");
}

sub deluser
{
	my ($user) = @_;
	my (%userhash);
	
	&lock ("user_$user");

# Open read-only to see if the user actually exists...

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "TRUE" or
		die_cleanup (-1, "$0: no such user $user\n");

	untie %userhash;

	unlink "$params{'db_home'}/users/$user.db" 
		or die_cleanup (-1, "$0: cannot unlink $params{'db_home'}/users/$user.db: $!\n");
		
	print "Deleted $user from the database.\n";

	&unlock ("user_$user");
}

sub infinitise
{
	my ($user) = @_;
	my (%userhash);
	
	&lock ("user_$user");

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "FALSE" or
		die_cleanup (-1, "$0: no such user $user\n");
		
	$userhash{'infinitism'} = "YES";

	untie %userhash;

	&unlock ("user_$user");

	print "Granted $user god-like super-powers to print as much as desired.\n";
}

sub deinfinitise
{
	my ($user) = @_;
	my (%userhash);
	
	&lock ("user_$user");

	tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "FALSE" or
		die_cleanup (-1, "$0: no such user $user\n");
		
	$userhash{'infinitism'} = "NO";

	untie %userhash;

	&unlock ("user_$user");

	print "Withdrawn god-like unlimited-printing super-powers from $user.\n";
}

sub display
{
	my ($user) = @_;
	my (@users, %userhash, %mischash);
	
	if ($user eq "") {
		opendir USERS_DIR, "$params{'db_home'}/users" 
			or die_cleanup (-1, "$0: cannot open directory $params{'db_home'}/users: $!\n");

		@users = readdir USERS_DIR
			or @users = ();
			
		@users = sort grep {! /^\./} @users;
			
		closedir USERS_DIR
			or die_cleanup (-1, "$0: cannot close directory $params{'db_home'}/users: $!\n");

		if ($opt{web}) {
			foreach $user (@users) {
				$user =~ s/\.db//;

				tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "TRUE"
					or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/users/$user.db: $!\n");
				
				if (!defined ($userhash{'quota'})) {
					$userhash{'quota'} = 0;
				}
				
				if (!defined ($userhash{'spent'})) {
					$userhash{'spent'} = 0;
				}
				
				if (!defined ($userhash{'pages'})) {
					$userhash{'pages'} = 0;
				}

				if (!defined ($userhash{'cyan'})) {
					$userhash{'cyan'} = 0;
				}

				if (!defined ($userhash{'magenta'})) {
					$userhash{'magenta'} = 0;
				}

				if (!defined ($userhash{'yellow'})) {
					$userhash{'yellow'} = 0;
				}
				
				if (!defined ($userhash{'black'})) {
					$userhash{'black'} = 0;
				}

				if (!defined ($userhash{'infinitism'})) {
					$userhash{'infinitism'} = "NO";
				}
				
				printf ("%s&$params{'currency_symbol'}%.2f%s&$params{'currency_symbol'}%.2f&%i&%f&%f&%f&%f\n", $user, $userhash{'quota'}, ($userhash{'infinitism'} eq 'YES') ? " (infinite)" : "", $userhash{'spent'}, $userhash{'pages'}, $userhash{'cyan'}, $userhash{'magenta'}, $userhash{'yellow'}, $userhash{'black'});
				
				untie %userhash;
			}
		} else {
			print "Current database:\n";

			foreach $user (@users) {
				$user =~ s/\.db//;

				tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "TRUE"
					or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/users/$user.db: $!\n");
				
				if (!defined ($userhash{'quota'})) {
					$userhash{'quota'} = 0;
				}
				
				if (!defined ($userhash{'spent'})) {
					$userhash{'spent'} = 0;
				}
				
				if (!defined ($userhash{'pages'})) {
					$userhash{'pages'} = 0;
				}

				if (!defined ($userhash{'cyan'})) {
					$userhash{'cyan'} = 0;
				}

				if (!defined ($userhash{'magenta'})) {
					$userhash{'magenta'} = 0;
				}

				if (!defined ($userhash{'yellow'})) {
					$userhash{'yellow'} = 0;
				}
				
				if (!defined ($userhash{'black'})) {
					$userhash{'black'} = 0;
				}
		
				if (!defined ($userhash{'infinitism'})) {
					$userhash{'infinitism'} = "NO";
				}
				
				printf ("%s: Credit: $params{'currency_symbol'}%.2f%s, Spent: $params{'currency_symbol'}%.2f, Pages: %i, CMYK: %.2f, %.2f, %.2f, %.2f\n", $user, $userhash{'quota'}, ($userhash{'infinitism'} eq 'YES') ? " (infinite)" : "", $userhash{'spent'}, $userhash{'pages'}, $userhash{'cyan'}, $userhash{'magenta'}, $userhash{'yellow'}, $userhash{'black'});
				
				untie %userhash;
			}
	
			tie %mischash, "Printbill::PTDB_File", "$params{'db_home'}/misc.db", "TRUE"
				or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/misc.db: $!\n");
	
			print "A total of $mischash{'total pages'} " . (($mischash{'total pages'} == 1) ? "page has" : "pages have") . " been printed.\n";
			printf ("A total of $params{'currency_symbol'}%.2f has been paid for print quota.\n", $mischash{'total paid'});
			printf ("A total of $params{'currency_symbol'}%.2f has actually been spent on printing.\n", $mischash{'total spent'});

			untie %mischash;
		}
	} else {
		tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "TRUE"
			or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/users/$user.db: $!\n");
			
		if (!defined ($userhash{'quota'})) {
			$userhash{'quota'} = 0;
		}
		
		if (!defined ($userhash{'spent'})) {
			$userhash{'spent'} = 0;
		}
		
		if (!defined ($userhash{'pages'})) {
			$userhash{'pages'} = 0;
		}

		if (!defined ($userhash{'cyan'})) {
			$userhash{'cyan'} = 0;
		}

		if (!defined ($userhash{'magenta'})) {
			$userhash{'magenta'} = 0;
		}

		if (!defined ($userhash{'yellow'})) {
			$userhash{'yellow'} = 0;
		}
				
		if (!defined ($userhash{'black'})) {
			$userhash{'black'} = 0;
		}
						
		if (!defined ($userhash{'infinitism'})) {
			$userhash{'infinitism'} = "NO";
		}
		
		if ($opt{web}) {
			printf ("$params{'currency_symbol'}%.2f%s\n", $userhash{'quota'}, , ($userhash{'infinitism'} eq 'YES') ? " (infinite)" : "");
		} else {
			printf ("%s: Credit: $params{'currency_symbol'}%.2f%s, Spent: $params{'currency_symbol'}%.2f, Pages: %i, CMYK: %.2f, %.2f, %.2f, %.2f\n", $user, $userhash{'quota'}, ($userhash{'infinitism'} eq 'YES') ? " (infinite)" : "", $userhash{'spent'}, $userhash{'pages'}, $userhash{'cyan'}, $userhash{'magenta'}, $userhash{'yellow'}, $userhash{'black'});
		}
	
		untie %userhash;
	}
}

sub pages
{
	my ($user) = @_;
	my (%mischash, %userhash);

	if ($user eq "") {
		tie %mischash, "Printbill::PTDB_File", "$params{'db_home'}/misc.db", "TRUE"
			or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/misc.db: $!\n");
	
		if ($opt{web}) {
			print "$mischash{'total pages'}\n";
		} else {
			print "A total of $mischash{'total pages'} pages has been printed.\n";
		}
		
		untie %mischash;
	} else {
		tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "TRUE"
			or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/users/$user.db: $!\n");
	
		if ($opt{web}) {
			print "$userhash{'pages'}\n";
		} else {
			print "User $user has printed $userhash{'pages'} pages\n";
		}
		
		untie %userhash;
	}
}

sub spent
{
	my ($user) = @_;
	my (%mischash, %userhash);
	
	if ($user eq "") {
		tie %mischash, "Printbill::PTDB_File", "$params{'db_home'}/misc.db", "TRUE"
			or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/misc.db: $!\n");
	
		if ($opt{web}) {
			printf ("$params{'currency_symbol'}%.2f\n", $mischash{'total spent'});
		} else {
			printf ("A total of $params{'currency_symbol'}%.2f has been spent on printing.\n", $mischash{'total spent'});
		}

		untie %mischash;
	} else {
		tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "TRUE"
			or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/users/$user.db: $!\n");
	
		if ($opt{web}) {
			printf ("$params{'currency_symbol'}%.2f\n", $userhash{'spent'});
		} else {
			printf ("A total of $params{'currency_symbol'}%.2f has been spent on printing by $user.\n", $userhash{'spent'});
		}
		
		untie %userhash;
	}
}

sub paid
{
	my ($user) = @_;
	my (%mischash, %userhash);
	
	if ($user eq "") {
		tie %mischash, "Printbill::PTDB_File", "$params{'db_home'}/misc.db", "TRUE"
			or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/misc.db: $!\n");
	
		if ($opt{web}) {
			printf ("$params{'currency_symbol'}%.2f\n", $mischash{'total paid'});
		} else {
			printf ("A total of $params{'currency_symbol'}%.2f has been paid for print quota.\n", $mischash{'total paid'});
		}
		
		untie %mischash;
	} else {
		tie %userhash, "Printbill::PTDB_File", "$params{'db_home'}/users/$user.db", "TRUE"
			or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/users/$user.db: $!\n");

		if ($opt{web}) {
			printf ("$params{'currency_symbol'}%.2f\n", $userhash{'spent'} + $userhash{'quota'});
		} else {
			printf ("A total of $params{'currency_symbol'}%.2f has been paid for print quota by $user.\n", $userhash{'spent'} + $userhash{'quota'});
		}	
		
		untie %userhash;
	}
}

sub used
{
	my ($printer) = @_;
	my (@printers, %printerhash, $etpb, $etpc, $cyanleft, $magentaleft, $yellowleft, $blackleft);
	
	if ($printer eq "") {
		opendir PRINTERS_DIR, "$params{'db_home'}/printers"
			or die_cleanup (-1, "$0: cannot open directory $params{'db_home'}/printers: $!\n");

		@printers = readdir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot read directory $params{'db_home'}/printers: $!\n");

		@printers = sort grep { !/^\./ } @printers; 

		closedir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot close directory $params{'db_home'}/printers: $!\n");
	
		if ($#printers == -1) {
			print "$0: no printers listed in $params{'db_home'}/printers - run $0 --updateprinters\n";
		} else {
			foreach $printer (@printers) {
				$printer =~ s/\.db//;

# If the parameters are taken from global defaults, create an entry in the
# printers hash for this printer which is just a copy of the defaults.

				if (!defined ($printers{$printer})) {
					%{$printers{$printer}} = %{$printers{'default'}};
				}

				tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer.db", "TRUE"
					or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/printers/$printer.db: $!\n");

				if ($printers{$printer}{'colourspace'} ne 'cmy') {
					$etpb = $printers{$printer}{'estimated_total_percent_black'};
				}

				if ($printers{$printer}{'colourspace'} ne 'mono') {
					$etpc = $printers{$printer}{'estimated_total_percent_colour'};
				}

				if ($opt{'web'}) {
					if ($printers{$printer}{'colourspace'} ne 'mono') {
						$cyanleft = 100 * $printerhash{'estimated cyan'} / $etpc;
						$magentaleft = 100 * $printerhash{'estimated magenta'} / $etpc;
						$yellowleft = 100 * $printerhash{'estimated yellow'} / $etpc;
					} else {
						$cyanleft = 0;
						$magentaleft = 0;
						$yellowleft = 0;
					}
					
					if ($printers{$printer}{'colourspace'} ne 'cmy') {
						$blackleft = 100 * $printerhash{'estimated black'} / $etpb;
					} else {
						$blackleft = 0;
					}
					
					printf "$printer&%.2f:%.2f:%.2f:%.2f\n", $cyanleft, $magentaleft, $yellowleft, $blackleft;
				} else {
					print "Printer \"$printer\":\n\n";

					if ($printers{$printer}{'colourspace'} ne 'mono') {
						$cyanleft = 100 * $printerhash{'estimated cyan'} / $etpc;
						printf "cyan: %.2f", $cyanleft;
						print "\% used\n";

						$magentaleft = 100 * $printerhash{'estimated magenta'} / $etpc;
						printf "magenta: %.2f", $magentaleft;
						print "\% used\n";

						$yellowleft = 100 * $printerhash{'estimated yellow'} / $etpc;
						printf "yellow: %.2f", $yellowleft;
						print "\% used\n";
					}

					if ($printers{$printer}{'colourspace'} ne 'cmy') {
						$blackleft = 100 * $printerhash{'estimated black'} / $etpb;
						printf "black: %.2f", $blackleft;
						print "\% used\n\n";
					}
				}
			
				untie %printerhash;
			}
		}
	} else {
		if (-r "$params{'db_home'}/printers/$printer.db") {
			if (!defined ($printers{$printer})) {
				%{$printers{$printer}} = %{$printers{'default'}};
			}
	
			tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer.db", "TRUE"
				or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/printers/$printer.db: $!\n");

			if ($printerhash{'colourspace'} ne 'cmy') {
				$etpb = $printers{$printer}{'estimated_total_percent_black'};
			}

			if ($printerhash{'colourspace'} ne 'mono') {
				$etpc = $printers{$printer}{'estimated_total_percent_colour'};
			}

			if ($opt{'web'}) {
				if ($printers{$printer}{'colourspace'} ne 'mono') {
					$cyanleft = 100 * $printerhash{'estimated cyan'} / $etpc;
					$magentaleft = 100 * $printerhash{'estimated magenta'} / $etpc;
					$yellowleft = 100 * $printerhash{'estimated yellow'} / $etpc;
				} else {
					$cyanleft = 0;
					$magentaleft = 0;
					$yellowleft = 0;
				}
				
				if ($printers{$printer}{'colourspace'} ne 'cmy') {
					$blackleft = 100 * $printerhash{'estimated black'} / $etpb;
				} else {
					$blackleft = 0;
				}
				
				printf "$printer&%.2f:%.2f:%.2f:%.2f\n", $cyanleft, $magentaleft, $yellowleft, $blackleft;
			} else {
				print "Printer \"$printer\":\n\n";

				if ($printers{$printer}{'colourspace'} ne 'mono') {
					$cyanleft = 100 * $printerhash{'estimated cyan'} / $etpc;
					printf "cyan: %.2f", $cyanleft;
					print "\% used\n";

					$magentaleft = 100 * $printerhash{'estimated magenta'} / $etpc;
					printf "magenta: %.2f", $magentaleft;
					print "\% used\n";

					$yellowleft = 100 * $printerhash{'estimated yellow'} / $etpc;
					printf "yellow: %.2f", $yellowleft;
					print "\% used\n";
				}

				if ($printers{$printer}{'colourspace'} ne 'cmy') {
					$blackleft = 100 * $printerhash{'estimated black'} / $etpb;
					printf "black: %.2f", $blackleft;
					print "\% used\n\n";
				}
			}
			
			untie %printerhash;
		} else {
			die_cleanup (-1, "$0: $params{'db_home'}/printers/$printer.db is not readable.\n");
		}
	}
}

sub resetblack
{
	my ($printer) = @_;
	my (@printers, %printerhash);

	if ($printer eq "") {
		opendir PRINTERS_DIR, "$params{'db_home'}/printers"
			or die_cleanup (-1, "$0: cannot open directory $params{'db_home'}/printers: $!\n");

		@printers = readdir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot read directory $params{'db_home'}/printers: $!\n");

		@printers = sort grep { !/^\./ } @printers; 

		closedir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot close directory $params{'db_home'}/printers: $!\n");
		
		if ($#printers == -1) {
			print "$0: no printers listed in $params{'db_home'}/printers - run $0 --updateprinters\n";
		} else {
			foreach $printer (@printers) {
				$printer =~ s/\.db//;
				print "$printer\n";

				if ($printers{$printer}{'colourspace'} ne 'cmy') {
					&lock ("printer_$printer");
			
					tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer.db", "FALSE"
						or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/printers/$printer.db: $!\n");

					$printerhash{'estimated black'} = 0;

					untie %printerhash;
			
					&unlock ("printer_$printer");

					print "Black ink levels for printer \"$printer\" reset.\n";
				} else {
					print "$printer is CMY (3-colour), ignoring.\n";
				}
			}
		}
	} else {
		if ($printers{$printer}{'colourspace'} ne 'cmy') {
			&lock ("printer_$printer");

			tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer.db", "FALSE"
				or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/printers/$printer.db: $!\n");

			$printerhash{'estimated black'} = 0;
		
			untie %printerhash;

			&unlock ("printer_$printer");
		
			print "Black ink levels for printer \"$printer\" reset.\n";
		} else {
			die_cleanup (-1, "$0: printer $printer is CMY (3-colour) - can't reset black levels!\n");
		}
	}
}

sub resetcolour
{
	my ($printer) = @_;
	my (@printers, %printerhash);
	
	if ($printer eq "") {
		opendir PRINTERS_DIR, "$params{'db_home'}/printers"
			or die_cleanup (-1, "$0: cannot open directory $params{'db_home'}/printers: $!\n");
		
		@printers = readdir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot read directory $params{'db_home'}/printers: $!\n");

		@printers = sort grep { !/^\./ } @printers; 
			
		closedir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot close directory $params{'db_home'}/printers: $!\n");

		if ($#printers == -1) {
			print "$0: no printers listed in $params{'db_home'}/printers - run $0 --updateprinters\n";
		} else {
			foreach $printer (@printers) {
				$printer =~ s/\.db//;
			
				if ($printers{$printer}{'colourspace'} ne 'mono') {
					&lock ("printer_$printer");
				
					tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer.db", "FALSE"
						or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/printers/$printer.db: $!\n");
					
					$printerhash{'estimated cyan'} = 0;
					$printerhash{'estimated magenta'} = 0;
					$printerhash{'estimated yellow'} = 0;
					
					untie %printerhash;
	
					&unlock ("printer_$printer");
					
					print "Colour ink levels for printer \"$printer\" reset.\n";
				} else {
					print "$printer is mono, ignoring.\n";
				}
			}
		}
	} else {
		if ($printers{$printer}{'colourspace'} ne 'mono') {
			&lock ("printer_$printer");
			
			tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer.db", "FALSE"
				or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/printers/$printer.db: $!\n");

			$printerhash{'estimated cyan'} = 0;
			$printerhash{'estimated magenta'} = 0;
			$printerhash{'estimated yellow'} = 0;
				
			untie %printerhash;
		
			&unlock ("printer_$printer");

			print "Colour ink levels for printer \"$printer\" reset.\n";
		} else {
			die_cleanup (-1, "$0: printer $printer is mono - can't reset colour levels!\n");
		}
	}
}

sub ppages
{
	my ($printer) = @_;
	my (@printers, %printerhash);

	if ($printer eq "") {
		opendir PRINTERS_DIR, "$params{'db_home'}/printers"
			or die_cleanup (-1, "$0: cannot open directory $params{'db_home'}/printers: $!\n");

		@printers = readdir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot read directory $params{'db_home'}/printers: $!\n");

		@printers = sort grep { !/^\./ } @printers; 

		closedir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot close directory $params{'db_home'}/printers: $!\n");

		if ($#printers == -1) {
			print "$0: no printers listed in $params{'db_home'}/printers - run $0 --updateprinters\n";
		} else {
			foreach $printer (@printers) {
				$printer =~ s/\.db//;
			
				tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer.db", "TRUE"
					or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/printers/$printer.db: $!\n");
				
				if (!defined ($printerhash{'total pages'})) {
					$printerhash{'total pages'} = 0;
				}

				if ($opt{'web'}) {
					print $printerhash{'total pages'} . "\n";
				} else {
					print "A total of " . $printerhash{'total pages'} . " pages has been printed on printer \"$printer\"\n";
				}

				untie %printerhash;
			}
		}
	} else {
		tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer.db", "TRUE"
			or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/printers/$printer.db: $!\n");

		if (!defined ($printerhash{'total pages'})) {
			$printerhash{'total pages'} = 0;
		}

		if ($opt{'web'}) {
			print $printerhash{'total pages'} . "\n";
		} else {
			print "A total of " . $printerhash{'total pages'} . " pages has been printed on printer \"$printer\"\n";
		}
		
		untie %printerhash;
	}
}

sub resetpages
{
	my ($printer) = @_;
	my (@printers, %printerhash);

	if ($printer eq "") {
		opendir PRINTERS_DIR, "$params{'db_home'}/printers"
			or die_cleanup (-1, "$0: cannot open directory $params{'db_home'}/printers: $!\n");

		@printers = readdir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot read directory $params{'db_home'}/printers: $!\n");

		@printers = sort grep { !/^\./ } @printers; 

		closedir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot close directory $params{'db_home'}/printers: $!\n");
		
		if ($#printers == -1) {
			print "$0: no printers listed in $params{'db_home'}/printers - run $0 --updateprinters\n";
		} else {
			foreach $printer (@printers) {
				$printer =~ s/\.db//;
			
				&lock ("printer_$printer");
			
				tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer.db", "FALSE"
					or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/printers/$printer.db: $!\n");

				$printerhash{'total pages'} = 0;

				untie %printerhash;

				&unlock ("printer_$printer");
			
				print "Page count for printer \"$printer\" reset.\n";
			}
		}
	} else {
		&lock ("printer_$printer");

		tie %printerhash, "Printbill::PTDB_File", "$params{'db_home'}/printers/$printer.db", "FALSE"
			or die_cleanup (-1, "$0: cannot open file $params{'db_home'}/printers/$printer.db: $!\n");

		$printerhash{'total pages'} = 0;
		
		untie %printerhash;
		
		&unlock ("printer_$printer");
		
		print "Page count for printer \"$printer\" reset.\n";
	}
}

sub colourspace
{
	my ($printer) = @_;
	my (@printers);

	if ($printer eq "") {
		opendir PRINTERS_DIR, "$params{'db_home'}/printers"
			or die_cleanup (-1, "$0: cannot open directory $params{'db_home'}/printers: $!\n");
		
		@printers = readdir PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot read directory $params{'db_home'}/printers: $!\n");

		@printers = sort grep { !/^\./ } @printers; 
		
		closedr PRINTERS_DIR
			or die_cleanup (-1, "$0: cannot close directory $params{'db_home'}/printers: $!\n");

		if ($#printers == -1) {
			print "$0: no printers listed in $params{'db_home'}/printers - run $0 --updateprinters\n";
		} else {
			foreach $printer (@printers) {
				$printer =~ s/\.db//;
			
				if (!defined ($printers{$printer})) {
					%{$printers{$printer}} = %{$printers{'default'}};
				}

				if ($opt{'web'}) {
					print $printers{$printer}{'colourspace'} . "\n";
				} else {
					print "Printer $printer is " . $printers{$printer}{'colourspace'} . "\n";
				}
			}
		}
	} else {
		if (!defined ($printers{$printer})) {
			%{$printers{$printer}} = %{$printers{'default'}};
		}
			
		if ($opt{'web'}) {
			print $printers{$printer}{'colourspace'} . "\n";
		} else {
			print "Printer \"$printer\" is " . $printers{$printer}{'colourspace'} . "\n";
		}
	}
}

sub init
{
	my ($confirm, $uid, $web_uid, %mischash, %adminhash, $gid);
	
	if (! $opt{ask_no_questions}) {
		print "About to WIPE everything in ALL databases!

*** WARNING ***

You only get one more chance, and this is it. If you press enter, any
existing databases under $params{db_home} will be destroyed.

Press enter to confirm or CTRL-C to abort.";

		<STDIN>;
	}

	`/bin/rm -rf $params{'db_home'} &> /dev/null`;
	
	if ($? >> 8) {
		die_cleanup (-1, "$0: could not remove $params{'db_home'}\n");
	}

	umask 0;

# No main directory? Start by creating one!

	mkdir "$params{'db_home'}", 0755
		or die_cleanup (-1, "$0: could not create $params{'db_home'}: $!\n");

	mkdir "$params{'db_home'}/users", 0775
		or die_cleanup (-1, "$0: could not create $params{'db_home'}/users/: $!\n");

	$uid = (getpwnam ($params{'printbilld_user'}))[2];

	if (defined $params{'printbilld_group'}) {
		$gid = getgrnam ($params{'printbilld_group'});
	} else {
		$gid = (getpwnam ($params{'printbilld_user'}))[3];
	}

	chown $uid, $gid, "$params{'db_home'}/users"
		or die_cleanup (-1, "$0: could not set ownership on $params{'db_home'}/users/ to $uid: $!\n");
	
# Set directory permissions and ownership to appropriate values

	mkdir "$params{'db_home'}/stats", 0755
		or die_cleanup (-1, "$0: could not create $params{'db_home'}/stats/: $!\n");

	chown $uid, -1, "$params{'db_home'}/stats"
		or die_cleanup (-1, "$0: could not set ownership on $params{'db_home'}/stats/ to $uid: $!\n");

	mkdir "$params{'db_home'}/printers", 0755
		or die_cleanup (-1, "$0: could not create $params{'db_home'}/printers/: $!\n");

	chown $uid, -1, "$params{'db_home'}/printers"
		or die_cleanup (-1, "$0: could not set ownership on $params{'db_home'}/printers/ to $uid: $!\n");

	mkdir "$params{'db_home'}/tmp", 0770
		or die_cleanup (-1, "$0: could not create $params{'db_home'}/tmp/: $!\n");

	chown $uid, $gid, "$params{'db_home'}/tmp"
		or die_cleanup (-1, "$0: could not set ownership on $params{'db_home'}/stats/ to $uid: $!\n");
		
	if (defined $params{'stats_path'} && defined $params{'graph_png_output_dir'}) {
# Be slightly more forgiving with this directory - if it exists, just
# chown/chmod it.
		if (-d $params{'graph_png_output_dir'}) {
			chmod 0755, $params{'graph_png_output_dir'};
		} else {
			mkdir $params{'graph_png_output_dir'}, 0755
				or die_cleanup (-1, "$0: could not create $params{'graph_png_output_dir'}: $!\n");
		}
			
		$web_uid = getpwnam ($params{'web_user'});
		chown $web_uid, -1, $params{'graph_png_output_dir'};
	}

	&lock ("misc");

	tie %mischash, "Printbill::PTDB_File", "$params{'db_home'}/misc.db", "FALSE" or
		die_cleanup (-1, "$0: cannot open file $params{'db_home'}/misc.db: $!\n");

	$mischash{'total pages'} = 0;
	$mischash{'total paid'} = 0;
	$mischash{'total spent'} = 0;

	untie %mischash;

	chmod 0664, "$params{'db_home'}/misc.db"
		or die_cleanup (-1, "$0: could not set mode on $params{'db_home'}/misc.db to 0644: $!\n");

	chown $uid, $gid, "$params{'db_home'}/misc.db"
		or die_cleanup (-1, "$0: could not set ownership on $params{'db_home'}/misc.db to $uid: $!\n");

	&unlock ("misc");

	if (defined $params{'web_user'}) {
		$web_uid = getpwnam ($params{'web_user'});

		&lock ("admin");

		tie %adminhash, "Printbill::PTDB_File", "$params{'db_home'}/web_admin.db", "FALSE" or
			die_cleanup (-1, "$0: cannot open file $params{'db_home'}/web_admin.db: $!\n");

		%adminhash = ();

		untie %adminhash;

		chmod 0600, "$params{'db_home'}/web_admin.db"
			or die_cleanup (-1, "$0: could not set mode on $params{'db_home'}/web_admin.db to 0600: $!\n");

		chown $web_uid, -1, "$params{'db_home'}/web_admin.db"
			or die_cleanup (-1, "$0: could not set ownership on $params{'db_home'}/web_admin.db to $uid: $!\n");

		&unlock ("admin");

		mkdir "$params{'db_home'}/cookies", 0755
			or die_cleanup (-1, "$0: could not create $params{'db_home'}/cookies/: $!\n");

		chown $web_uid, -1, "$params{'db_home'}/cookies"
			or die_cleanup (-1, "$0: could not set ownership on $params{'db_home'}/cookies to $uid: $!\n");
	}
	
	print "Re-constructed $params{'db_home'} and cleared the databases.\n";
}

sub webadduser
{
	my ($user) = @_;
	my (%adminhash, $password1, $password2, $salt);

	&lock ("admin");

	tie %adminhash, "Printbill::PTDB_File", "$params{'db_home'}/web_admin.db", "FALSE" or
		die_cleanup (-1, "$0: cannot open file $params{'db_home'}/web_admin.db: $!\n");

	if ($user eq "") {
		print "$0: (null) is not a valid username\n";
	} elsif (defined ($adminhash{$user})) {
		print "$0: $user already has an admin account.\n";
	} else {
		print "Adding new web print administration user $user\n";
		system "stty -echo";
		print "Password for $user: ";
		chomp ($password1 = <STDIN>);
		print "\n";
		print "Re-enter password for $user: ";
		chomp ($password2 = <STDIN>);
		print "\n";
		system "stty echo";

		$salt = &random_salt;

		if ($password1 eq $password2) {
			$adminhash{$user} = crypt ($password1, $salt);
			print "Print administrator $user now exists.\n";
		} else {
			print "Butterfingers! Passwords don't match.\n"
		}
	}

	untie %adminhash;

	&unlock ("admin");
}

sub webdeluser
{
	my ($user) = @_;
	my (%adminhash);

	&lock ("admin");

	tie %adminhash, "Printbill::PTDB_File", "$params{'db_home'}/web_admin.db", "FALSE" or
		die_cleanup (-1, "$0: cannot open file $params{'db_home'}/web_admin.db: $!\n");

	if (defined ($adminhash{$user})) {
		delete $adminhash{$user};
		print "Print administrator $user no longer exists.\n";
	} else {
		print "$0: $user does not appear to have an admin account.\n";
	}
	
	untie %adminhash;

	&unlock ("admin");
}

sub webpasswd
{
	my ($user) = @_;
	my (%adminhash, $password1, $password2, $salt);
	
	print "Changing administrative password for $user\n";
	print "New password for $user: ";
	system "stty -echo";
	chomp ($password1 = <STDIN>);
	print "\n";
	print "Re-enter password for $user: ";
	chomp ($password2 = <STDIN>);
	print "\n";
	system "stty echo";

	if ($password1 eq $password2) {
		$salt = &random_salt;

		&lock ("admin");
	
		tie %adminhash, "Printbill::PTDB_File", "$params{'db_home'}/web_admin.db", "FALSE" or
			die_cleanup (-1, "$0: cannot open file $params{'db_home'}/web_admin.db: $!\n");

		if (defined ($adminhash{$user})) {
			$adminhash{$user} = crypt ($password1, $salt);
			untie %adminhash;

			print "Password for $user changed.\n";
		} else {
			untie %adminhash;

			print "Print administrator $user does not exist.\n";
			print "Please use --webadduser first.\n";
		}

		&unlock ("admin");
	} else {
		print "Butterfingers! Passwords don't match.\n"
	}
	
}

sub webnonintpasswd
{
	my ($user, $pass) = @_;
	my (%adminhash, $salt);
	
	&lock ("admin");
	
	tie %adminhash, "Printbill::PTDB_File", "$params{'db_home'}/web_admin.db", "FALSE" or
		die_cleanup (-1, "$0: cannot open file $params{'db_home'}/web_admin.db: $!\n");

	if (defined ($adminhash{$user})) {
		$salt = &random_salt;
		$adminhash{$user} = crypt ($pass, $salt);
		print "Password for $user changed.\n";
	} else {
		print "Print administrator $user does not exist.\n";
		print "Please use --webadduser first.\n";
	}
	
	untie %adminhash;

	&unlock ("admin");
}

# Generate a random salt

sub random_salt
{
	my ($C1, $C2);
	$C1 = &salt_char;
	$C2 = &salt_char;
	
	return $C1 . $C2;
}

sub salt_char
{
	my $R = floor (rand() * 64);
	my ($C);
	
	if ($R < 26) {
		$C = chr ($R + ord ("A")); 
	} elsif ($R < 52) {
		$C = chr ($R - 26 + ord ("a"));
	} elsif ($R < 62) {
		$C = chr ($R - 52 + ord ("0"));
	} elsif ($R == 62) {
		$C = '.';
	} else {
		$C = '/';
	}

	return $C;
}

# We don't use proper flock() or fcntl() because we would need to keep track
# of file descriptors. This way is simple - we just need to keep track of
# the filename.

sub lock
{
	my ($text) = @_;
	my ($lockpid);

	while (-e "$params{'db_home'}/tmp/.printbill_$text.lock") {
		open (LOCKFILE, "<$params{'db_home'}/tmp/.printbill_$text.lock") or
			&die_cleanup (-1, "$0: cannot open lockfile $params{'db_home'}/tmp/.printbill_$text.lock: $!\n");
		
		flock LOCKFILE, LOCK_EX or
			&die_cleanup (-1, "$0: cannot lock lockfile $params{'db_home'}/tmp/.printbill_$text.lock: $!\n");

		$lockpid = <LOCKFILE>;
		chomp $lockpid;
		
# Is the locking process still running? If not, we can safely nuke the file
# and lock it ourselves.

		last if (! -d "/proc/$lockpid");
		
		if (!$opt{web}) {
			&die_cleanup (-1, "$0: $params{'db_home'}/tmp/.printbill_$text.lock is held by another active process.\n");
		}

# Otherwise, we have to wait.

		close LOCKFILE or
			&die_cleanup (-1, "$0: cannot close lockfile: $!\n");

		sleep $params{'retry_interval'};
	}
	
	open (LOCKFILE, ">$params{'db_home'}/tmp/.printbill_$text.lock")
		or &die_cleanup (-1, "$0: cannot open lockfile $params{'db_home'}/tmp/.printbill_$text.lock: $!\n");
	
	flock LOCKFILE, LOCK_EX
		or &die_cleanup (-1, "$0: cannot lock lockfile $params{'db_home'}/tmp/.printbill_$text.lock: $!\n");

	print LOCKFILE $$
		or &die_cleanup (-1, "$0: cannot write to lockfile: $!\n");

	$locks{$text} = 1;

	close LOCKFILE
		or &die_cleanup (-1, "$0: cannot close lockfile: $!\n");
}

sub unlock
{
	my ($text) = @_;

	if (-e "$params{'db_home'}/tmp/.printbill_$text.lock") {
		unlink "$params{'db_home'}/tmp/.printbill_$text.lock"
			or &die_cleanup (-1, "$0: cannot remove lockfile $params{'db_home'}/tmp/.printbill_$text.lock: $!\n");
		delete $locks{$text};
	} else {
		print "$params{'db_home'}/tmp/.printbill_$text.lock doesn't exist...\n";
	}
	
}

# Wash our hands of the whole affair

sub cleanup {
	my $key;
	
	if (%locks) {
		for $key (keys %locks) {
			&unlock ($key);
		}
	}
}

# Cleanup and die gracefully...

sub die_cleanup
{
	my ($the_return_val, $msg) = @_;

	&cleanup;

	print STDERR $msg;
	exit $the_return_val;
}

# Deal with signals. Die, but clean up our mess first.

sub catch_zap {
	&cleanup;
	exit 0;
}
