package common;

# common.pm - these are common web server routines
# 
# ==============================================================
# 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.
# 
# Original source:
#	Copyright (c) 1999, 2000 SAIC (Bruce W. Mohler)
# 
# Contributed source:
#	Copyright (c) 2000 Ken Monville
#	Copyright (c) 1999, 2000 Paul Farrall
# 
# ==============================================================
# 
# $Header: /export/home/mohlerb/syssumm/web.server/RCS/common.pm,v 1.13 2000/04/27 17:38:31 mohlerb Exp mohlerb $
# 
# $Revision: 1.13 $
#
# $Log: common.pm,v $
# Revision 1.13  2000/04/27 17:38:31  mohlerb
# changes from Ken Monville including:
# These patches enable the System Handle to become a link
# if $SystemHandleUrl is set; this is similar to the System
# Handle being a link in sysquery.pl?target=showall
#
# Revision 1.12  2000/04/22 23:18:07  mohlerb
# changes from Ken Monville including:
# includes an addtional field to the sysquery.pl showall page;
# now displays the System Handle in the table; includes support
# to make a link from the System Handle; If $ServerHandleUrl
# in common.pm is left empty, the system handles are just
# printed without being a link, however, if $ServerHandleUrl
# contains a value, the Server Handle is appended to it and made
# into an <a href=""> tag
#
# Revision 1.11  2000/04/18 03:37:29  mohlerb
# changes from Ken Monville including:
# include a separate footer file (syssumm.footer) similar
# to the syssumm.header file
#
# Revision 1.10  2000/04/17 04:38:43  mohlerb
# changes from Bruce Mohler including:
# fixed PromptForUrl to allow "https...";
# this defect was found by Michael Seeger <michael.seeger@mchh.siemens.de>
#
# Revision 1.9  2000/03/16 23:01:10  mohlerb
# changes from Bruce Mohler including:
# changes to support re-write LoadConfigurationFile function;
# note that the CreateConfigurationFile function no longer
# exists; a single argument to LoadConfigurationFile function
# (boolean) which states whether to try to fix problems if
# one or more configuration tags are missing; this is only
# true in install.pl, false for all other scripts; therefore
# if tags are added to the configuration file in later versions,
# it will automatically prompt for new values and add them to
# the file
#
# Revision 1.8  2000/02/23 19:27:01  mohlerb
# changes from Bruce Mohler including:
# s/profile/summary/g for consistency on all generated HTML
# and perl source; added comment in EnterSystemName to remind
# people that they can enter a pattern, not just a nodename;
# added border to GenerateShowall table output
#
# Revision 1.7  2000/02/04 20:54:49  mohlerb
# changes from Bruce Mohler including:
# added variable $HostNameType to common.pm; it has 2 possible
# values: "FQDN" or "hostname"; "FQDN" is the default value which
# emulates the original behavior of creating the subdirectories
# under <docroot>/syssumm with the fully qualified domain name;
# "hostname" merely uses the hostname for the subdirectory name
# (and query searches)
#
# Revision 1.6  1999/10/23 23:34:01  mohlerb
# changes from Bruce Mohler including:
# added PromptForUrl for WebServerUrl field in configuration file
# to check for proper URL pattern and to strip any trailing '/'
# off of the field - this is related to Revision 1.23 of sysquery.pl
#
# Revision 1.5  1999/08/31 15:13:54  mohlerb
# changes from Paul Farrall including:
# The email address for the webmaster had a \ inserted before
# the @.
#
# Revision 1.4  1999/08/21 01:24:26  mohlerb
# changes from Paul Farrall including:
# fix to function which was creating blank lines in the *List files
#
# Revision 1.3  1999/08/04 17:03:34  mohlerb
# changes from Bruce Mohler including:
# when the syssumm home directory and web.server.info files are
# created, make sure that they are created with permissions that
# make them globally readable, otherwise processes like the
# sysquery.pl script (running under the web server account) may
# not get access to the common.pm and configuration files.
#
# Revision 1.2  1999/07/02 21:59:26  mohlerb
# changes from Bruce Mohler including:
# fixes after testing sysquery.pl
#
# Revision 1.1  1999/07/02 16:06:51  mohlerb
# Initial revision
#


require Exporter;
@ISA = qw(Exporter);
###########################################################################
# the following sub-routines (&) are variables ($) will be 
# visible in a program using this module...
@EXPORT = qw(
	&LoadConfigurationFile

	&LoadSummary
	&UnloadSummary

	&GenerateWebPage

	&UpdateLists
	&UpdateListFile
	&WriteListFile

	$SyssummUserid
	$SyssummHomeDirectory
	$SyssummUid
	$SyssummGid
	$SyssummDirectory
	$SystemHandleUrl
	$CGIScriptName
	$FooterFileName
	$FormatFileName
	$HeaderFileName
	$SummaryFileName
	$ConfigurationFileName
	$ProtectedMessage
	$HostNameType
	$PreviousFileSuffix

	%FieldToList
	$UpdateFileNow
	$DeferFileUpdate
);
###########################################################################


###########################################################################
# Public Global Variables (must be declared above)
###########################################################################

$SyssummUserid = "syssumm";
$SyssummHomeDirectory = "";
$SyssummUid = 0;
$SyssummGid = 0;
$SyssummDirectory = "syssumm";		# under $DocrootDirectory
$CGIScriptName = "sysquery.pl";
$FooterFileName = "syssumm.footer";
$FormatFileName = "syssumm.format";
$HeaderFileName = "syssumm.header";
$SummaryFileName = "syssumm.info";
$ConfigurationFileName = "web.server.info";

$ProtectedMessage = "To be provided.";

# CUSTOMIZE - only comment one of the choices - default to original behavior
$HostNameType = "FQDN";
# $HostNameType = "hostname";

# In the summary list, you can optionally have the System Handle appended
# to the end of the following link..
# If its left blank, it will not be a link.
# $SystemHandleUrl = "/cgi-bin/server-log-util.pl?action=view&nodename=";
$SystemHandleUrl = "";

# standard suffix for prior versions of files
$PreviousFileSuffix = ".bak";

###########################################################################
# Data for list files

# map profile field name to list
%FieldToList = (
	"Software:OsName" => "OSList",
	"General:Vendor" => "VendorList",
	"General:Location" => "LocationList",
	"General:Organization" => "OrganizationList",
);

%ListsLoaded = ( );

$UpdateFileNow = 1;
$DeferFileUpdate = 0;


###########################################################################
# Private Global Variables
###########################################################################

$PathToConfigFile = "";
$FilePermissions = 0644;

$CheckForFile = 1;
$DontCheckForFile = 0;

###########################################################################
# Data for persistent configuration file

# 
# You can use the following comment as boiler plate to copy
# into Perl scripts using the functions in the Perl module:
# 

# 
# At run-time, certain global variables are defined from a
# persistent configuration file by the function LoadConfigurationFile
# in common.pm.  Refer to the source in that Perl module to 
# determine which variables are defined.
# 
# In addition, certain variables are exported into the calling
# scripts main:: name space including the syssumm home directory,
# uid, and gid.
# 

%ConfigFieldPrompts = (
	AdminEmailAddress => 
		"Enter e-mail address for your local System Administrator",
	WebServer => "Enter type of web server (e.g., apache, netscape)",
	CGIDirectory => "Enter path to your CGI directory",
	DocrootDirectory => 
		"Enter path to your web server document root directory",
	Webmaster => "Enter \"name\" for webmaster on bottom of web page",
	WebmasterEmailAddress => "Enter e-mail address for Webmaster",
	WebServerUrl => "Enter the URL for the web server",
	MailCommand => "Enter the path to your local mail command",
);

%ConfigFieldTypes = (
	DocrootDirectory => "directory",
	WebServer => "name",
	CGIDirectory => "directory",
	AdminEmailAddress => "email",
	Webmaster => "name",
	WebmasterEmailAddress => "email",
	WebServerUrl => "url",
	MailCommand=> "path",
);


###########################################################################
# Subroutines
###########################################################################

###########################################################################
# Functions for persistent configuration file
###########################################################################

# 
# The "flow of control" in the next function is controlled
# by 2 hashes, %ConfigFieldPrompts # and %ConfigFieldTypes, 
# which are defined above...
# 

# 
# The following function creates a "persistant" configuration 
# file for the scripts that reside on the web server computer.
# 
# The format of this file is
# 
#	tab=value
# 
# This is a sample of the file (stale example!):
# 
#	AdminEmailAddress=root\@stegasaurus.saic.com
#	CGIDirectory=/home/httpd/cgi-bin
#	HtdocDirectory=/home/httpd/html
#	Webmaster=Webmaster
#	WebmasterEmailAddress=root\@stegasaurus.saic.com
# 

sub LoadConfigurationFile ($)				# PUBLIC
{	my $TakeCareOfProblems = shift;

	DeterminePathToConfigFile($CheckForFile) if ( $PathToConfigFile eq "" );

	if ( ( ! -f $PathToConfigFile ) or -z $PathToConfigFile ) {
		PromptForConfigurationValues();
		my @ConfigInfo = GlobalVarsToArray();
		WriteConfigurationFile($PathToConfigFile, @ConfigInfo);
		DumpGlobalVars();
	}

	my @ConfigInfo = ReadConfigurationFile($PathToConfigFile);
	my @Problems = ArrayToGlobalVars(@ConfigInfo);
	DumpGlobalVars();
	# note that @Problems represents tags that should've
	# been in the configuration file but were not found

	if ( @Problems ) {
		if ( $TakeCareOfProblems ) {
			# prompt for the missing values
			PromptForConfigurationValues();
			my @ConfigInfo = GlobalVarsToArray();
			DumpGlobalVars();
			WriteConfigurationFile($PathToConfigFile, @ConfigInfo);
		}
		else {
			die "There were problems with the configuration file.";
		}
	}

	return;
}


sub ReadConfigurationFile ($)				# PRIVATE
{	my $PathToConfigFile = shift;

	# slurp config file into memory
	open CONFIG, "<$PathToConfigFile" or
		die "Couldn't open $PathToConfigFile for input: $!";
	my @ConfigInfo = <CONFIG>;
	chomp(@ConfigInfo);
	close(CONFIG);

	if ( $main::Debug ) {
		print STDERR "Lines read from $PathToConfigFile:\n";
		foreach $Line (@ConfigInfo) {
			print STDERR "\t$Line\n";
		}
	}

	return @ConfigInfo;
}


sub WriteConfigurationFile ($@)				# PRIVATE
{	my $PathToConfigFile = shift;
	my @ConfigInfo = @_;

	# write them out to the file
	open CONFIG, ">$PathToConfigFile" or
		die "Couldn't open $PathToConfigFile for output: $!";
	foreach $Line (@ConfigInfo) {
		print CONFIG "$Line\n";
	}
	close(CONFIG);

	# make sure ownerships and permissions are correct
	chown $SyssummUid, $SyssummGid, $PathToConfigFile;
	chmod $FilePermissions, $PathToConfigFile;

	return;
}


# 
# Input: array of ASCII lines from configuration file
# Output: list of tags that were *NOT* found in the input array
# 
# This function should recognize lines in any order that they
# appear in the configuration file even though it writes them
# out in alphabetical order.
# 
# The Perl variables that it defines are in the main:: name
# space (ie., they are GLOBAL).
# 
# It's up to the calling function to look at the returned array
# to determine whether there was a problem.
# 

sub ArrayToGlobalVars (@)				# PRIVATE
{	my @ConfigInfo = @_;

	my @RemainingTags = (sort keys %ConfigFieldPrompts);
	LINE: foreach $Line (@ConfigInfo) {

		print STDERR "\nProcessing line: $Line...\n" if $main::Debug;

		my @Tags = @RemainingTags;
		@RemainingTags = ( );
		print STDERR "\tRemaining tags: @Tags\n" if $main::Debug;
		while ( @Tags ) {
			$Tag = shift @Tags;
			print STDERR "\tLooking for tag /$Tag/...\n" if $main::Debug;
			if ( $Line =~ /^$Tag=/ ) {
				print STDERR "\tmatched!\n" if $main::Debug;
				$Line =~ /$Tag=(.*$)/;

				my $Value = $1;
				my $VarName = "main::$Tag";
				${$VarName} = $Value;

				push @RemainingTags, @Tags;

				next LINE;
			}
			else {
				push @RemainingTags, $Tag;
			}
		}
	}

	return @RemainingTags;
}


sub GlobalVarsToArray (@)				# PRIVATE
{	my @ConfigInfo = ( );

	foreach $Tag (sort keys %ConfigFieldPrompts) {
		my $VarName = "main::$Tag";
		push @ConfigInfo, "$Tag=${$VarName}";
	}

	return @ConfigInfo;
}


sub DumpGlobalVars ()					# PRIVATE
{
	return if ( ! $main::Debug );

	foreach $Tag (sort keys %ConfigFieldPrompts) {
		my $VarName = "main::$Tag";
		if ( defined ${$VarName} ) {
			print STDERR "$Tag is ${$VarName}...\n";
		}
		else {
			print STDERR "$Tag is missing...\n";
		}
	}

	return;
}


sub PromptForConfigurationValues ()			# PRIVATE
{
	# prompt for each of the values and store in global variables
	# if the Tag ("Key") already has a value, it is skipped
	foreach $Tag (sort keys %ConfigFieldPrompts) {
		my $Value = "";
		my $VarName = "main::$Tag";

		# if it's defined *and* has a non-null value
		next if ( defined ${$VarName} );

		if ( $ConfigFieldTypes{$Tag} eq "directory" ) {
			$Value = PromptForDirectoryPath($ConfigFieldPrompts{$Tag});
		}
		elsif ( $ConfigFieldTypes{$Tag} eq "email" ) {
			$Value = PromptForEmailAddress($ConfigFieldPrompts{$Tag});
		}
		elsif ( $ConfigFieldTypes{$Tag} eq "name" ) {
			$Value = PromptForName($ConfigFieldPrompts{$Tag});
		}
		elsif ( $ConfigFieldTypes{$Tag} eq "path" ) {
			$Value = PromptForPath($ConfigFieldPrompts{$Tag});
		}
		elsif ( $ConfigFieldTypes{$Tag} eq "url" ) {
			$Value = PromptForUrl($ConfigFieldPrompts{$Tag});
		}
		else {
			$Value = PromptFor($ConfigFieldPrompts{$Tag});
		}

		# store variables globally
		${$VarName} = $Value;
	}

	return;
}


sub DeterminePathToConfigFile ($)			# PRIVATE
{	my $CheckForFile = shift;

	# 
	# The configuration file is stored in the home directory
	# of the syssumm account.  Is that defined?
	# 
	$SyssummHomeDirectory = (getpwnam($SyssummUserid))[7];
	if ( $SyssummHomeDirectory eq "" ) {
		die "Can't determine location of syssumm configuration file: $!";
	}

	$PathToConfigFile = "$SyssummHomeDirectory/$ConfigurationFileName";

	# okay, so we know where it's supposed to be...  is it there?
	if ( $CheckForFile and ! -f $PathToConfigFile ) {
		die "Can't access syssumm configuration file $PathToConfigFile: $!";
	}

	# get uid and gid for setting permissions later on...
	$SyssummUid = (getpwnam($SyssummUserid))[2];
	$SyssummGid = (getpwnam($SyssummUserid))[3];

	print STDERR "Path to config file is $PathToConfigFile...\n" if $main::Debug;

	return;
}


sub PromptForDirectoryPath ($$)				# PRIVATE
{	my $Prompt = shift;
	my $DirectoryPath = "";

	while ( 1 == 1 ) {
		$DirectoryPath = PromptFor($Prompt);
		last if ( -d "$DirectoryPath" );
		print STDERR "That directory doesn't exist...\n";
	}

	return $DirectoryPath;
}


sub PromptForEmailAddress ($$)				# PRIVATE
{	my $Prompt = shift;
	my $EmailAddress = "";

	while ( 1 == 1 ) {
		$EmailAddress = PromptFor($Prompt);
		last if $EmailAddress =~ /\@/;
		print STDERR "Invalid email address...\n";
	}

	return $EmailAddress;
}


sub PromptForName ($$)					# PRIVATE
{	my $Prompt = shift;
	my $Name = "";

	while ( 1 == 1 ) {
		$Name = PromptFor($Prompt);
		last if $Name =~ /\w/;
		print STDERR "Invalid name...\n";
	}

	return $Name;
}


sub PromptForPath ($$)					# PRIVATE
{	my $Prompt = shift;
	my $Path = "";

	while ( 1 == 1 ) {
		$Path = PromptFor($Prompt);
		last if ( -x "$Path" );
		print STDERR "Invalid path...\n";
	}

	return $Path;
}


sub PromptForUrl ($$)					# PRIVATE
{	my $Prompt = shift;
	my $Url = "";

	while ( 1 == 1 ) {
		$Url = PromptFor($Prompt);
		last if $Url =~ /http:\/\/\w*/;
		last if $Url =~ /https:\/\/\w*/;
		print STDERR "Invalid URL...\n";
	}

	# strip off trailing slash (sysquery.pl will add it...)
	$Url =~ s/\/$//;

	print "URL is $Url...\n" if $main::Debug;

	return $Url;
}


sub PromptFor ($$)					# PRIVATE
{	my $Prompt = shift;
	my $Result = "";

	print STDOUT "$Prompt:\n-> ";
	$Result = <STDIN>;
	chomp($Result);

	return $Result;
}


###########################################################################
# Functions for profiles
###########################################################################

# 
# This function loads a profile into memory after cleaning
# out any prior profile.  For each category read, it pushes
# the category name onto an array called @ListOfCategories.
# 

sub LoadSummary ($)					# PUBLIC
{	my $SystemName = shift;
	my $NbrOfLines = 0;

	UnloadSummary();

	my $PathToSummary = "$main::DocrootDirectory/$main::SyssummDirectory/$SystemName/$SummaryFileName";
	print STDERR "Path to profile is $PathToSummary...\n" if $main::Debug;

	open PROFILE, "<$PathToSummary" or
		die "Couldn't open format file $PathToSummary: $!";
	my @SystemInfo = <PROFILE>;
	chomp(@SystemInfo);
	close(PROFILE);

	# load information into hashes
	foreach $Line (@SystemInfo) {
		next if ( ! $Line );		# skip blank lines
		# print STDERR "\nProcessing $Line...\n" if $main::Debug;
		my ( $Category, $Subcategory, $Value ) = split(/:/, $Line, 3);

		# keep track of categories
		my $Found = 0;
		foreach $ListCategory (@main::ListOfCategories) {
			if ( $Category eq $ListCategory ) {
				$Found = 1;
				last;
			}
		}
		if ( ! $Found ) {
			push @main::ListOfCategories, $Category;
			# print STDERR "\tAdded $Category to list of categories...\n" if $main::Debug;
		}

		# make sure the category hash is global
		$Category = "main::$Category";
		${$Category}{$Subcategory} = $Value;

		$NbrOfLines += 1;
	}

	return $NbrOfLines;
}


sub UnloadSummary ()					# PUBLIC
{	my $NbrOfCategories = 0;

	if ( defined(@main::ListOfCategories) ) {
		foreach $Hash (@main::ListOfCategories) {
			# make sure the category hash is global
			$Hash = "main::$Hash";
			# empty the hash
			%{$Hash} = ( );

			$NbrOfCategories += 1;
		}
		# empty the array
		@main::ListOfCategories = ( );
	}

	return $NbrOfCategories;
}


###########################################################################
# Functions for HTML generation
###########################################################################

# 
# The purpose of the following function is to create an ASCII 
# file containing HTML text of a system's profile.  The name of 
# the output file should correspond to the name of the default 
# web page in a directory if none is specified.  It was derived 
# from the GenerateSummary function originally in the sysquery.pl 
# script.  This function does not deliver any HTML to a browser.  
# It merely generates the static HTML to be delivered on request.
# 
# This function assumes that it has already been placed in the
# directory where the summary input file will be read from as 
# well as the new web page written to.
# 

sub GenerateWebPage ($)					# PUBLIC
{	my $Target = shift;

	print STDERR "\tGenerateWebPage($Target)...\n" if $main::Debug;

	LoadFormat();
	LoadSummary($Target);

	# open the output file

	# generate the HTML
	# GenerateHeader(OUTPUTFILE, "System Summary - $Target Summary");
	# ProcessFormatInfo(OUTPUTFILE, $Target);
	# GenerateFooter(OUTPUTFILE);

	# close the output file

	return;
}


sub LoadFormat
{
	my $PathToFormatFile = 
		"$main::DocrootDirectory/$main::SyssummDirectory/$FormatFileName";

	open FORMAT, "<$PathToFormatFile" or
		die "Couldn't open format file $PathToFormatFile: $!";
	@FormatInfo = <FORMAT>;
	chomp(@FormatInfo);
	close(FORMAT);

	return;
}


##########################################################################
##########################################################################
###############################  XXX  ####################################
##########################################################################
##########################################################################

sub GenerateSummary
{
	LoadFormat();
	LoadSummary($Target);

	GenerateHeader("System Summary - $Target Summary");
	ProcessFormatInfo($Target);
	GenerateFooter();

	return;
}


# 
# This function will output the start of the <head>
# section including the <title>.  It will then look
# for an external file that will complete the <head>
# section and begin the <body> section so that the
# user can customize the appearance of the web pages
# for their site.
# 
# If the external file doesn't exist, a minimal set
# of lines closing the <head> section and opening the
# <body> section will be emitted.
# 
# See the Install documentation on the web site for
# examples of setting up the external file.
# 

sub GenerateHeader
{	my $Title = shift;

	print "Content-type: text/html\n\n";

	print "<html>\n";
	print "<head>\n";
	print "<title>$Title</title>\n";

	my $PathToHeaderFile = "$PathToSyssummDirectory/$HeaderFileName";

	if ( -f $PathToHeaderFile ) {
		open HEADER, "<$PathToHeaderFile" or
			die "Couldn't open header file $PathToHeaderFile: $!";
		my @HeaderInfo = <HEADER>;
		close(HEADER);
		print @HeaderInfo;
	}
	else {
		# no external file, emit miminal lines to open <body>
		print "<!-- Built-in header information -->\n\n";
		print "</head>\n\n";
		print "<body>\n";
	}

	return;
}


#
# This does the same as sub GenerateHeader(), except it's for the footer.
# If the footer file is not present, it just closes the body of the html, 
# then displays a simple footer.
#

sub GenerateFooter
{
	my $PathToFooterFile = "$PathToSyssummDirectory/$FooterFileName";
 
	if ( -f $PathToFooterFile ) {
		open FOOTER, "<$PathToFooterFile" or
		die "Couldn't open footer file $PathToFooterFile: $!";
		my @FooterInfo = <FOOTER>;
		close(FOOTER);
		print @FooterInfo;
	}
	else {
		# no external file, emit miminal lines to close </html>
		print "<!-- Built-in footer information -->\n\n";
		print "</body>\n";
		print "<br>\n";                 # hard-code vertical spacing
		print "<p align=center>\n";
		print "Send comments about this web page to <a href=\"mailto:$WebmasterEmailAddress\">$Webmaster</a>\n";
		print "</p>\n";
		print "</html>\n";
	}

	return;
}


sub ProcessFormatInfo ($)
{	my $Target = shift;

	print <<END_TEXT;

<h1>System Summary for $Target:</h1>
<p>
END_TEXT

	# 
	# The information in the format file appears like the
	# following example:
	# 
	#	table columns=2 title=General
	#	  General:NodeName General:Organization
	#	  General:Vendor General:Model
	#	  General:Location-*
	#	  General:DateSystemInstalled
	#	endtable
	# 
	# Note that lines that contain more than one "field"...
	# 
	# A field ending in "-*" is a Multiple Value field.
	# 
	# The format file (example above) drives the order in
	# which fields appear in the HTML-ized output.  The
	# values for each field are derived from the summary.
	# 
	# Note that the "value" of a field may be "#include 
	# <file> ...".  In this case, each file on the line,
	# in the order they appear, are sent out to the browser.
	# The data in each file may contain HTML tags.  If the
	# suffix on the data file appears to be a graphics file
	# (.jpg, .jpeg, or .gif), then the file name is wrapped
	# in an <img src...>filename</img> tag.
	# 
	foreach $Line (@FormatInfo) {
		$ThisLineNumber++;

		# general cleanup
		$Line =~ s/^\s*//;		# dike leading whitespace
		next if $Line =~ /^#/;		# bypass comments
		next if ( $Line eq "" );	# bypass blank lines

		# identify and process commands
		my @Field = split(/ /, $Line);
		if ( $Field[0] eq "table" ) {
			shift @Field;
			StartTable(@Field);
			next;
		}
		elsif ( $Field[0] eq "endtable" ) {
			EndTable();
			next;
		}

		# what's left over is data
		print "<tr>\n";

		# make sure we don't exceed number of columns
		my $NbrOfColumns = scalar(@Field);
		if ( $NbrOfColumns gt $ThisManyColumns ) {
			die "Data at line $ThisLineNumber exceeds max columns";
		}

		# are we going to do any column spanning?
		my $ColumnSpanning = ( $NbrOfColumns < $ThisManyColumns );

		# process each "field"
		my $CurrentColumn = 1;
		foreach $Field (@Field) {

			print STDERR "\tProcessing field $Field...\n" if $main::Debug;

			# handle spanning columns
			my $SpanningArg = "";
			if ( $ColumnSpanning and 
			     $CurrentColumn == $NbrOfColumns ) {
				my $ColumnsToSpan = ( ( $ThisManyColumns - $NbrOfColumns) + 1 );
				$SpanningArg = " colspan=\"$ColumnsToSpan\"";
			}

			# handle the actual field
			my ( $Category, $Subcategory ) = split(/:/, $Field, 2);
			# 
			# if the subcategory ends in -* (as in Disks-*),
			# then expand it and find all of the matching
			# subcategory fields
			# 
			if ( $Subcategory =~ /-\*$/ ) {
				my @Value = ExpandWildcard($Category, $Subcategory);
				$Subcategory =~ s/-\*$//;
				my $Label = TransformSubcategory($Subcategory);
				print "<td$SpanningArg> $Label: <br>@Value</b> </td>\n";
			}
			else {
				my $Value = "Unknown";
				if ( ${$Category}{$Subcategory} ) {
					if ( $Subcategory eq "SystemHandle" &&
						$SystemHandleUrl ) {
						$Value = "<a href=\"$SystemHandleUrl" . "${$Category}{$Subcategory}\">${$Category}{$Subcategory}</a>";
					} else {
						$Value = ${$Category}{$Subcategory};
						$Value =~ s/PROTECTED/$ProtectedMessage/;
					}
				}
				my $Label = TransformSubcategory($Subcategory);

				# XXX - DEBUGGING
				# print STDERR "\t\tCategory: $Category, Subcategory: $Subcategory, Value: $Value\n" if $main::Debug;

				# 
				# If the value starts with "#include"
				# assume that the value is one or more
				# files to be included as the value
				# for this field.
				# 
				if ( $Value =~ /^#include/ ) {
					print "<td$SpanningArg> $Label: ";
					SubstituteFileForValue($Value);
					print "</td>\n";
				}
				else {
					print "<td$SpanningArg> $Label: <b>$Value</b> </td>\n";
				}
			}

			$CurrentColumn++;
		}

		print "</tr>\n";
	}

	return;
}


sub StartTable
{	my @TableArgs = @_;

	# process "command line args"
	foreach $Arg (@TableArgs) {
		my ( $Tag, $Value ) = split(/=/, $Arg, 2);
		if ( $Tag eq "columns" ) {
			$ThisManyColumns = $Value;	# global
		}
		elsif ( $Tag eq "title" ) {
			print "<h2>$Value</h2>\n";
		}
		else {
			die "Error processing table command arg at line $ThisLineNumber";
		}
	}

	# so, what are you waiting for?
	print "<table border=\"2\" width=\"$TableWidth\">\n";

	return;
}


sub EndTable
{
	print "</table>\n";

	return;
}


# 
# Take a name like "DateSystemInstalled" and
# transform it to "Date System Installed"
# 

sub TransformSubcategory
{	my $Subcategory = $_[0];

	# XXX This was not working properly in the first
	#     version and has been tweaked since.  Please
	#     check for proper working.
#	$Subcategory =~ s/([A-Z][a-z]+)([A-Z])/$1 $2/g;

	# The regular expression above fails  to split words 2/3 in
	# three word names like "DateSystemInstalled".

	# How about this instead.
	# This one splits any pattern like "bA" into "b A"
	# e.g. "Name" => "Name", "NameaNameb" => "Namea Nameb",
	#      "NameaNamebNamec" => "Namea Nameb Namec"
	# note.  Name must end and begin with alphabet character.
	$Subcategory =~ s/([a-z])([A-Z])/$1 $2/g;

	return $Subcategory;
}


sub ExpandWildcard
{	my $Category = $_[0];
	my $Subcategory = $_[1];
	my @Results = ( );

	$Subcategory =~ s/-\*$/-/;

	foreach $Key ( sort keys %{$Category} ) {
		if ( $Key =~ /$Subcategory/ ) {
			push @Results, "<b>${$Category}{$Key}</b><br>";
		}
	}

	return @Results;
}


# 
# Note that this function could really be called
# SubstituteFile(s)ForValue...
# 

sub SubstituteFileForValue
{	my $Value = $_[0];
	my @FilesToInclude = ( );

	print STDERR "\tIn SubstituteFileForValue()...\n" if $main::Debug;

	# clean up the "value"
	$Value =~ s/\s+/ /g;

	# split the value into sub-fields
	my @SubFields = split(/ /, $Value);

	# build list of files to include
	my $ThisShouldBeFileName = 0;
	foreach $SubField (@SubFields) {
		if ( $ThisShouldBeFileName ) {
			push @FilesToInclude, $SubField;
			$ThisShouldBeFileName = 0;
			next;
		}
		if ( $SubField eq "#include" ) {
			$ThisShouldBeFileName = 1;
			next;
		}
	}

	# calculate where the images are stored (just in case)
	my $PathToSummaryDirectory = "/$main::SyssummDirectory/$Target";
	print STDERR "\tPath to summary directory is $PathToSummaryDirectory...\n" if $main::Debug;

	# include the files
	foreach $FileName ( @FilesToInclude ) {

		# is it an image?
		my $ImageFile = 0;
		foreach $Pattern ( @GraphicFileSuffixes ) {
			if ( $FileName =~ /\.$Pattern$/ ) {
				$ImageFile = 1;
				last;
			}
		}

		if ( $ImageFile ) {
			print STDERR "\t\tincluding image $FileName...\n" if $main::Debug;
			print "<img src=$PathToSummaryDirectory/$FileName>\n";
		}
		else {
			print STDERR "\t\tincluding $FileName...\n" if $main::Debug;
			open INPUT, "<$FileName" or
				print "Couldn't open file $FileName for inline inclusion: $!";
			my @Input = <INPUT>;
			print "@Input";
			close(INPUT);	
		}
	}


	return;
}



###########################################################################
# Functions for list files
###########################################################################

# 
# When the user views the first output from sysquery.pl
# they discover that they can search on 4 parameters:
# OS, Vendor, Location, and Organization.  These values
# are not hard-coded into any of the scripts but are
# derived from the existing profiles.  These "lists"
# are stored in the same directory as the format file
# and are initially created by the updatelists.pl script
# if there are pre-existing profiles and are then
# maintained by this script as incoming profiles are
# stored under the syssumm directory.  That is the
# purpose of this function...
# 

# 
# This function assumes that there is already a loaded
# profile...
# 
# This function is not especially efficient if you're
# going to be calling it to process a series of profiles.
# 

# 
# XXX - There is a known problem with these routines that
# I've seen in testing where where an incoming profile will
# create a blank line in one of the *List files.  I'll bang
# on this one next week.
# 
# XXX - There is another problem as well.  This function is
# driven by the %FieldToList hash.  Two of the four fields 
# are protected which means that there is no value in the
# incoming profile to store into the list file.  These fields
# specifically are: General:Location and General:Organization.
# 

sub UpdateLists ()					# PUBLIC
{
	foreach $Key (keys %FieldToList) {

		my $ListFile = $FieldToList{$Key};

		my ( $Category, $Subcategory ) = split(/:/, $Key, 2);
		$Category = "main::$Category";
		my $Value = ${$Category}{$Subcategory};
		next if ( $Value =~ /to be provided/i );
		next if ( $Value eq "" );

		# CUSTOMIZE
		if ( $Value =~ /linux/i && $ListFile eq "VendorList" ) {
			# in case we have a long vendor string, truncate it
			# For example, change
			#	"Red Hat Linux release 5.1 (Manhattan)"
			# to
			#	"Red Hat Linux"
			$Value =~ /^(.* linux) .*/i;
			$Value = $1;
		}

		UpdateListFile($ListFile, $Value, $UpdateFileNow);
	}

	return;
}


# 
# This function was written in a way that assumed that under
# certain circumstances, it might be called several times to
# update the same file.  Therefore, the first time that it is
# called, it caches the file in memory and doesn't reload it
# on subsequent calls.  You can also defer the writing of the
# file so that you can call the next function down yourself.
# 
# This is an example of calling the function on a one-time
# basis (as you might in incoming.pl):
# 
#	UpdateListFile($dir, $list, $value, $UpdatefileNow);
# 
# This is an example of how you miht call the function if you
# which to defer updating the file (as you might in updatelists.pl):
# 
#	UpdateListfile($dir, $list, $value, $DeferFileUpdate);
#	...
#	WriteListFile($dir, $list);
# 
# These functions assume that LoadConfigurationFile() was called
# prior to them.  It initialized certain global variables required
# for proper functioning.
# 

sub UpdateListFile ($$$)				# PUBLIC
{	my $ListName = shift;
	my $Value = shift;
	my $UpdateFile = shift;

	LoadListFile($ListName);
	my $ListChanged = AddNewUniqueValue($ListName, $Value);
	if ( $ListChanged and $UpdateFile ) {
		WriteListFile($ListName);
	}

	return;
}


sub LoadListFile ($)					# PRIVATE
{	my $ListName = shift;

	# is it already loaded?
	return if ( defined($ListsLoaded{$ListName}) );

	# make sure it's a known list file
	my $Found = 0;
	foreach $Key (sort keys %FieldToList) {
		my $Value = $FieldToList{$Key};
		if ( $ListName eq $Value ) {
			$Found = 1;
			last;
		}
	}
	if ( ! $Found ) {
		die "Attempt to update invalid list: $ListName: $!";
	}


	# load the list - allow for the file not to exist
	my $PathToListFile = 
		"$main::DocrootDirectory/$main::SyssummDirectory/$ListName";
	if ( open LISTFILE, "<$PathToListFile" ) {
		@{$ListName} = <LISTFILE>;
		chomp(@{$ListName});
		close LISTFILE;
	}
	else {
		@{$ListName} = ( );
	}

	# mark it as loaded
	$ListsLoaded{$ListName} = 1;

	return;
}


sub AddNewUniqueValue ($$)				# PRIVATE
{	my $ListName = shift;
	my $Value = shift;
	my $ListChanged = 0;

	my $Found = 0;
	foreach $ListValue (@{$ListName}) {
		if ( $Value eq $ListValue ) {
			$Found = 1;
			last;
		}
	}
	if ( ! $Found ) {
		push @{$ListName}, $Value;
		$ListChanged = 1;
	}

	return $ListChanged;
}


sub WriteListFile ($)					# PUBLIC
{	my $ListName = shift;

	my $PathToListFile = 
		"$main::DocrootDirectory/$main::SyssummDirectory/$ListName";
	open LISTFILE, ">$PathToListFile" or
		die "Couldn't open $PathToListFile for output: $!";
	# keep values in alphabetical order...
	foreach $Value (sort @{$ListName}) {
		print LISTFILE "$Value\n";
	}
	close LISTFILE;

	# make sure ownerships and permissions are correct
	chown $SyssummUid, $SyssummGid, $PathToListFile;
	chmod $FilePermissions, $PathToListFile;

	return;
}


1;
