#!/usr/bin/perl
#
# $Revision: 1.31 $
# Copyright 2009-2011 Teleflora
#
# Apply a "patch" to a "Daisy" POS environment.
#
# Intended for use with files produced by the "makepatch.pl" script
# for use in an Altiris deployment model.
#
use strict;
use warnings;
use Getopt::Long;
use POSIX;
use English;
use Digest::MD5;
use File::Basename;

my $CVS_REVISION = '$Revision: 1.31 $';
my $PROGNAME = basename($0);

#
# Command line options.
#
my $VERSION = 0;
my $HELP = 0;
my $RESTART = 1;
my $DAISY = 0;
my $DAISY_DIR = '/d/daisy';
my $LOG_STDERR = 0;
my $CONVERT_TO_ELAVON = 0;
my $INSTALL_FTD_FILES = 0;
my $HARDEN_LINUX = 0;
my $DEBUG = 0;

#
# Alternate os and root for testing.
#
my $ALTROOT = "";
my $ALTOS = "";

#
# Global variables.
#
my $POSDIR = "";
my $TIMESTAMP = strftime("%Y%m%d%H%M%S", localtime());
my $OS;

# Exit status values
my $EXIT_OK = 0;
my $EXIT_ERROR = -1;
my $EXIT_COMMAND_LINE = 1;
my $EXIT_MUST_BE_ROOT = 2;
my $EXIT_PATCH_VALIDATION = 3;
my $EXIT_SETTLE_RECS_NOT_ZERO = 4;
my $EXIT_POSTPATCH_CLEANUP = 8;
my $EXIT_STOP_DAISY = 9;
my $EXIT_START_DAISY = 10;
my $EXIT_PATCH_FAILED = 11;
my $EXIT_POSTPATCH = 12;
my $EXIT_PERMS = 15;
my $EXIT_HARDEN_LINUX = 16;
my $EXIT_ELAVON_CONTROL_FILE = 17;
my $EXIT_CARD_CONTROL_FILE = 18;
my $EXIT_CARD_INPUT_FILE = 19;
my $EXIT_NO_ELIGIBLE_DIRS = 20;

#
# The patch type can be either "Patch" or "Revision".
#
my $PATCH_TYPE = "Patch";
my @PATCHFILES = ();

#
# The list of Daisy dirs that a patch is going to be applied to.
my @DaisyDirs = ();

GetOptions(
	"version" => \$VERSION,
	"help" => \$HELP,
	"restart!" => \$RESTART,
	"daisy" => \$DAISY,
	"daisydir=s" => \$DAISY_DIR,
	"log-stderror" => \$LOG_STDERR,
	"convert-to-elavon" => \$CONVERT_TO_ELAVON,
	"install-ftd-files" => \$INSTALL_FTD_FILES,
	"harden-linux" => \$HARDEN_LINUX,
	"debug" => \$DEBUG,
	"altroot=s" => \$ALTROOT,
	"altos=s" => \$ALTOS,
) || die "Error: invalid command line option, exiting...\n";


# --version
if ($VERSION) {
	print("$PROGNAME $CVS_REVISION\n");
	exit($EXIT_OK);
}

# --help
if ($HELP) {
	print("$PROGNAME $CVS_REVISION\n");
	usage();
	exit($EXIT_OK);
}

#
# If there are no names of patch file, output usage and exit.
#
if ($#ARGV < 0) {
	usage();
	exit($EXIT_COMMAND_LINE);
}

#
# If daisy not specificed on the command line, then verify the existence
# of either the default daisy database dir or one specified on the command
# line.
#
if ($DAISY == 0) {
	unless (-d $DAISY_DIR) {
		print("Can not detect that Daisy has been installed... exiting.");
		exit($EXIT_COMMAND_LINE);
	}
	$DAISY = 1;
}

$POSDIR = $DAISY_DIR;
unless (-d $POSDIR) {
	print("$PROGNAME: $POSDIR does not exist");
	exit($EXIT_COMMAND_LINE);
}

#
# Gotta be root or exit.
#
if($EUID != 0) {
	usage();
	logerror("Error: You need root privileges to run this script.\n");
	loginfo("No patches applied.");
	loginfo("<===== End $PROGNAME $CVS_REVISION =====>");
	exit($EXIT_MUST_BE_ROOT);
}


sub main
{
    my $exit_status = $EXIT_OK;
    my $include_crd_dirs = 0;
    my $thisfile = "";

    if ($CONVERT_TO_ELAVON) {
	$include_crd_dirs = 1;

	# Show stopper:
	#   If there are any any settle files with non-zero record counts,
	#   then conversion should not be performed.
	#
	if (verify_settle_rec_count() != 0) {
	    logerror("Conversion to Elavon can't proceed until all settlements complete");
	    return($EXIT_SETTLE_RECS_NOT_ZERO);
	}
    }

    loginfo("");
    loginfo("");
    loginfo("<===== $PROGNAME $CVS_REVISION =====>");
    loginfo("Applying patch files: @PATCHFILES");


    #
    # Scrub the list of patch file names to avoid shell injection attacks.
    #
    foreach $thisfile (@ARGV) {
	loginfo("Scrubbing patch file name: $thisfile");
	push(@PATCHFILES, validate_input($thisfile));
    }


    #
    # Apply various criteria to validate the list of patch files...
    # if all patch files do not pass all criteria, then exit.
    #
    if (validate_patchfiles(\@PATCHFILES) != 0) {
	logerror("Patch file validation errors: @PATCHFILES");
	logerror("Please refer to log file for specific reasons");
	loginfo("<===== End $PROGNAME $CVS_REVISION =====>");
	return($EXIT_PATCH_VALIDATION);
    }

    #
    # If there are any post patch scripts left from a previous application,
    # remove them.
    #
    if (cleanup_postpatch_scripts($include_crd_dirs) != 0) {
	logerror("Cleanup of previous postpatch scripts failed.");
	loginfo("<===== End $PROGNAME $CVS_REVISION =====>");
	return($EXIT_POSTPATCH_CLEANUP);
    }

    #
    # Stop Daisy.
    #
    if ($RESTART) {
	if (stop_daisy() != 0) {
	    logerror("Can't stop daisy... exiting");
	    loginfo("<===== End $PROGNAME $CVS_REVISION =====>");
	    return($EXIT_STOP_DAISY);
	}
    }
    else {
	loginfo("Daisy not stopped: --norestart specified");
    }


    #
    # Step through each patch file, and apply it; if unsuccessful,
    # log a message and stop applying patches.
    #
    my $applypatch_status = 0;
    foreach $thisfile (@PATCHFILES) {

	next if ($thisfile eq "");

	$applypatch_status = applypatch($thisfile, $include_crd_dirs);
	if ($applypatch_status != $EXIT_OK) {

	    if ($applypatch_status == $EXIT_NO_ELIGIBLE_DIRS) {
		$exit_status = $EXIT_OK;
	    }
	    else {
		$exit_status = $EXIT_PATCH_FAILED;
	    }

	    last;
	}
    }

    #
    # If there were any problems with applypatch, back everything out.
    #
    if ($exit_status != $EXIT_OK) {
	foreach $thisfile (@PATCHFILES) {
	    reverse_patch($thisfile);
	}
    }

    #
    # One of the patches may have created a "postpatch.pl" script.
    # If so, run that after all ".patch" files have been extracted.
    #
    # If there were no errors from the apply patch, then
    #	proceed with applying any post patch scripts.
    #
    if ($exit_status == $EXIT_OK && $applypatch_status != $EXIT_NO_ELIGIBLE_DIRS) {
	if (apply_postpatch(\@PATCHFILES, $TIMESTAMP, $include_crd_dirs) != 0) {
	    $exit_status = $EXIT_POSTPATCH;
	}
    }

    #
    # Only run perms fixer if everything OK.
    #
    if ($exit_status == $EXIT_OK && $applypatch_status != $EXIT_NO_ELIGIBLE_DIRS) {
	if (run_daisy_perms() != 0) {
	    $exit_status = $EXIT_PERMS;
	}
    }

    #
    # Only run the harden script if everything OK and it's asked for.
    #
    if ($exit_status == $EXIT_OK && $applypatch_status != $EXIT_NO_ELIGIBLE_DIRS) {
	if ($HARDEN_LINUX) {
	    if (run_harden_linux() != 0) {
		$exit_status = $EXIT_HARDEN_LINUX;
	    }
	}
    }

    #
    # Restart the POS.
    #
    if ($RESTART) {
	if (start_daisy() != 0) {
	    logerror("Can't start daisy...");
	    loginfo("<===== End $PROGNAME $CVS_REVISION =====>");
	    return($EXIT_START_DAISY);
	}
    }
    else {
	loginfo("Daisy not restarted: --norestart specified");
    }


    #
    # All that's left to do is write a log message and report the exit status.
    #
    loginfo("$PROGNAME is terminating with exit status $exit_status");
    loginfo("<===== End $PROGNAME $CVS_REVISION =====>");

    return($exit_status);
}


################################################################
################################################################
################################################################


sub usage
{
	print("Usage:\n");
	print("$PROGNAME --version\n");
	print("$PROGNAME --help\n");
	print("$PROGNAME --daisy\n");
	print("$PROGNAME [options] daisy-x.x.x_A.patch daisy-x.x.x_B.patch ...\n");
	print("\n");
	print("Options:\n");
	print("--daisydir=path       use 'path' for the default daisy db dir path\n");
	print("--convert-to-elavon   perform a conversion to Elavon payment system\n");
	print("--harden-linux        run harden_linux.pl after successfully applying patch\n");
	print("--log-stderror        write output to stderr to the log file\n");
	print("--norestart           prevents stopping and then restarting Daisy\n");
	print("--install-ftd-files   force installation of FTD files\n");
	print("\n");
}


#
# Determines if settelment record count is zero
#
# Returns 1 if true else 0
#
sub is_settle_rec_count_zero
{
    my $settle_file_path = $_[0];
    my $buffer = "";
    my $bufsize = 2; # first two bytes
    my $rc = 0; # assume false

    if (open(SF, $settle_file_path)) {
	my $readsize = sysread(SF, $buffer, $bufsize);
	if (defined($readsize) && $readsize == $bufsize) {
	    my $byte = ord(substr($buffer, 0, 1));
	    if ($byte == 0) {
		$byte = ord(substr($buffer, 1, 1));
		if ($byte == 0) {
		    $rc = 1;
		}
	    }
	}
	else {
	    logerror("Error reading settle file: $settle_file_path\n");
	    logerror("Error: $!\n");
	}
	close(SF);
    }
    else {
	logerror("Open of settle file failed: $settle_file_path");
	logerror("Error: $!\n");
    }

    return($rc);
}


#
# Verify that there are no outstanding settlement records.
#
# Returns:
#   0 --> there are no settle files with record count non-zero
#   1 --> there is at least one settle file with record count non-zero
#
sub verify_settle_rec_count
{
    my @posdirs = get_daisy_dirs();
    my @crd_dirs = get_crd_dirs(@posdirs);
    my $settle_file = "settfile.pos";

    # include card processing dirs
    foreach my $ccrd_dir (@crd_dirs) {
	push(@posdirs, $ccrd_dir);
    }

    foreach my $posdir (@posdirs) {
	my $settle_file_path = "$ALTROOT/$posdir/$settle_file";
	if (-e $settle_file_path) {
	    if (!is_settle_rec_count_zero($settle_file_path)) {
		loginfo("Non-zero settlements in directory: $posdir");
		return(1);
	    }
	}
    }

    return(0);
}

#
# Get a field from a control file
#
# Input
#   arg[0]: full path to the control file
#   arg[1]: buffer size
#   arg[2]: index of field begin
#   arg[3]: field length
# 
# Return
#   field on success, empty string on error
#
sub get_control_file_field
{
    my $ctrl_file = $_[0];
    my $bufsize = $_[1];
    my $field_begin = $_[2];
    my $field_size = $_[3];
    my $field = "";
    my $buffer = "";

    if (open(DF, $ctrl_file)) {
	my $rc = sysread(DF, $buffer, $bufsize);
	if (defined($rc) && $rc == $bufsize) {
	    $field = substr($buffer, $field_begin, $field_size);
	}
	else {
	    showerror("Error reading control file: $ctrl_file\n");
	    showerror("Error: $!\n");
	}
	close(DF);
    }
    else {
	showerror("Open of control file failed: $ctrl_file");
    }

    return($field);
}


#
# Determine whether this system is configured to use Elavon
# for credit card payments.
#
# If the terminal id field in the file elavctrl.pos is
# 16 ascii chars long, then the system is assumed to be
# configured to use Elavon.
#
# Input
#   arg[0]: daisy database dir or card dir
#
# Returns 1 for yes, 0 for no
#
sub is_configured_for_elavon
{
    my $daisy_dir = $_[0];
    my $ctrl_file = "$daisy_dir/elavctrl.pos";

    if (-e $ctrl_file) {
	my $terminal_id = get_control_file_field($ctrl_file, 124, 8, 16);
	if ($terminal_id ne "") {
	    if (length($terminal_id) == 16) {
		if ($terminal_id =~ /[[:ascii:]]{16}/) {
		    return(1);
		}
	    }
	}
    }

    return(0);
}


#
# Function to determine if an arbitrary path is a path to a
# daisy databse directory.
#
sub is_daisy_db_dir
{
	my $path = $_[0];

	# must begin with '/d/'
	return(0) unless ($path =~ /^\/d\//);

	# must be a directory
	return(0) unless (-d $path);

	# skip old daisy dirs
	return(0) if ($path =~ /^\/d\/.+-\d{12}$/);

	# must contain the magic files
	return(0) unless (-e "$path/flordat.tel");
	return(0) unless (-e "$path/control.dsy");

	# must be daisy 8.0+
	return(0) unless (-d "$path/bin");

	return(1);
}


#
# If there are any credit card processing directories in the specified
# daisy database dir, add it to the list.  A credit card processing
# directory is defined to be any directory named 'crd[2-9]' that is
# located within a daisy database directory.
#
sub get_crd_dirs
{
    my @posdirs = shift;
    my @crd_dirs = ();

    foreach my $posdir (@posdirs) {

	my @ccrd_files = glob("$posdir/crd[2-9]");

	foreach my $ccrd_file (@ccrd_files) {
	    if (-d $ccrd_file) {
		push(@crd_dirs, $ccrd_file);
	    }
	}
    }

    return(@crd_dirs);
}


#
# Return a list of verified POS dirs.  For Daisy, remember, there can be
# more than one daisy database dir!
#
sub get_daisy_dirs
{
	my @daisy_dirs = ();
	my $daisy_top_dir = '/d';

	# Daisy POS can have one or more database dirs
	my @daisy_db_dirs = glob("$daisy_top_dir/*");
	for my $daisy_db_dir (@daisy_db_dirs) {

		# must be a directory
		next unless (-d $daisy_db_dir);

		# skip old daisy dirs
		next if ($daisy_db_dir =~ /.+-\d{12}$/);

		# must contain the magic files
		next unless(-e "$daisy_db_dir/flordat.tel");
		next unless(-e "$daisy_db_dir/control.dsy");

		# must be daisy 8.0+
		next unless (-d "$daisy_db_dir/bin");

		push(@daisy_dirs, $daisy_db_dir);
	}

	return(@daisy_dirs);
}


#
# Validate each patchfile from the list of patchfiles
#
# Returns 0 on success, -1 on error
#
sub validate_patchfiles
{
	my @patchfiles = @{$_[0]};
	my @posdirs = get_daisy_dirs();
	my $thisfile = "";

	foreach $thisfile (@patchfiles) {

		determine_patch_type($thisfile);

		# Verify patch file is a file.
		unless (-f "$thisfile") {
			logerror("\"$thisfile\" is not a file.\n");
			return(-1);
		}

		# Check for a zero length file.
		if (-s "$thisfile" == 0) {
			logerror("\"$thisfile\" is a zero length file.\n");
			return(-1);
		}

		# Even if patch file is not zero length, check for empty patch file.
		my @patchfiles = get_patchfile_catalog($thisfile);
		if ($#patchfiles < 0) {
			logerror("Patchfile \"$thisfile\" Contains no Files Within it.\n");
			return(-1);
		}

		# If a Daisy patch file, verify there exists valid daisy db dirs.
		if ($thisfile =~ /daisy-.*.\.patch/) {

			# Make sure one or more Daisy database dirs exist
			unless (@posdirs) {
				usage();
				logerror("No Daisy Directories Found.\n");
				return(-1);
			}
		}

		# If a RTI patch file, unsupported.
		if ($thisfile =~ /RTI-.*.\.patch/) {

			logerror("RTI patch unsupported: $thisfile\n");
			return(-1);
		}

		# Verify file system with the daisy database dir is not 100% full.
		# The code must respect that the possible various daisy db dirs
		# can be on any file system.
		foreach my $posdir (@posdirs) {
			if (get_diskused($posdir) == 100) {
				logerror("filesystem is 100% full: \"$posdir\"\n");
				return(-1);
			}
		}

		loginfo("Patch validated: $thisfile\n");
	}

	return(0);
}


#
# Clean up old post patch script from previous patches...
# Remember, there can be more than one daisy database dir.
#
# A script named "postpatch.pl" might be in the bin directory
# of the POS... if it exists, remove it.
#
# Returns 0 on success, -1 on error
#
sub cleanup_postpatch_scripts
{
    my $include_crd_dirs = $_[0];
    my @posdirs = ();
    my $post_patch_path;

    @posdirs = get_daisy_dirs();

    if ($include_crd_dirs) {
	my @crd_dirs = get_crd_dirs(@posdirs);
	foreach my $ccrd_dir (@crd_dirs) {
	    push(@posdirs, $ccrd_dir);
	}
    }

    foreach my $posdir (@posdirs) {

	$post_patch_path = "$posdir/bin/postpatch.pl";

	if (-f $post_patch_path) {
	    loginfo("Removing previous post patch script: $post_patch_path");
	    mysystem("rm -f $post_patch_path");
	    if (-f $post_patch_path) {
		logerror("Can not remove previous post patch script: $post_patch_path");
		return(-1);
	    }
	}
    }

    return(0);
}


#
# Make a backup of existing files, the ones to be replaced
# by the patch.  First arg is a list of the possible POS dirs -
# remember, that for daisy, there could be more than one.
#
# Returns:
#   $EXIT_OK --> success
#   $EXIT_ERROR --> copy of original file to backup dir failed
#   $EXIT_ERROR --> checksum difference between original and backup
#
sub backup_files
{
    my @posdirs = @{$_[0]};
    my @patchfile_catalog = @{$_[1]};

    # Need to know os type in this function
    determine_os();

    #
    # First, loop through all the POS dirs...
    #
    foreach my $posdir (@posdirs) {

	#
	# First, make sure there is a place to put the backups.
	#
	unless (-d "$posdir/backups") {
	    mysystem("mkdir $posdir/backups");
	    mysystem("chmod 750 $posdir/backups");
	    mysystem("chown tfsupport:daisy $posdir/backups");
	}

	#
	# Now, loop through the list of all the files in the patchfile and
	# copy the originals (if there is one) to the backup directory.
	#

	foreach my $thisfile (@patchfile_catalog) {

	    my $src_file = "$posdir/$thisfile";

	    #
	    # Handle platform specific files - like those that have a suffix
	    # of "-FC3" or "-FC5" or "-RHWS5" or "-RHEL5".
	    #
	    if ($src_file =~ /(.*)-$OS$/) {
		$src_file = $1;
	    }

	    #
	    # FIXME: This strategy does not handle files of the same name
	    # under subdirs in $posdir.
	    #

	    my $dst_file = "$posdir/backups/" . basename($thisfile) . ".prepatch.$TIMESTAMP";
	    my $oldmd5 = "";
	    my $newmd5 = "";

	    if (! -f $src_file) {
		loginfo("\"$src_file\" will be a new file from patch. Backup Not Possible.");
		next;
	    }

	    $oldmd5 = get_md5sum("$src_file");

	    mysystem("cp -f $src_file $dst_file");
	    if ($? != 0) {
		logerror("File Backup Failed ($?): \"$src_file\" -> \"$dst_file\"");
		return($EXIT_ERROR);
	    }

	    $newmd5 = get_md5sum("$dst_file");

	    # Verify MD5sum of our "old" and "new" files. They should be the same.
	    if ($oldmd5 ne $newmd5) {
		logerror("File Backup Failed: MD5 sums do not match");
		logerror("\"$src_file\" ($oldmd5) -> \"$dst_file\" ($newmd5)");
		return($EXIT_ERROR);
	    }

	    loginfo("Backed Up... $src_file -> $dst_file (MD5=$newmd5)");
	}
    }

    return($EXIT_OK);
}


#
# Apply a patch to a set of daisy dirs.
#
# Returns:
#   $EXIT_OK --> success
#   $EXIT_ERROR --> no files in patchfile
#   $EXIT_ERROR --> could not backup files to be patched
#   $EXIT_ERROR --> could not extract file from patch tar archive
#
sub applypatch
{
    my $patchfile = $_[0];
    my $include_crd_dirs = $_[1];

    my @patchfile_catalog = ();
    my $old_patch_id = "";
    my $new_patch_id = "";

    determine_patch_type($patchfile);

    log_patch_info($patchfile);

    my @all_posdirs = get_daisy_dirs();

    if ($include_crd_dirs) {
	my @all_crd_dirs = get_crd_dirs(@all_posdirs);
	foreach my $ccrd_dir (@all_crd_dirs) {
	    push(@all_posdirs, $ccrd_dir);
	}
    }

    #
    # Now that the list of all daisy processing dirs is gathered,
    # if doing a "Global to Elavon Conversion", verify which
    # of the daisy dirs actually need converting.
    #
    if ($CONVERT_TO_ELAVON) {
	foreach my $posdir (@all_posdirs) {
	    if (is_configured_for_elavon($posdir)) {
		loginfo("Skipping directory already converted to Elavon: $posdir");
	    }
	    elsif (! -e "$posdir/settfile.pos") {
		loginfo("Skipping directory with no settfile.pos: $posdir");
	    }
	    elsif (! -e "$posdir/crdctrl.pos") {
		loginfo("Skipping directory with no crdctrl.pos: $posdir");
	    }
	    elsif (! -e "$posdir/ndc2ctrl.pos") {
		loginfo("Skipping directory with no ndc2ctrl.pos: $posdir");
	    }
	    elsif (! -e "$posdir/crdinet.pos") {
		loginfo("Skipping directory with no crdinet.pos: $posdir");
	    }
	    elsif (! -e "$posdir/crdmodem.pos") {
		loginfo("Skipping directory with no crdmodem.pos: $posdir");
	    }
	    else {
		push(@DaisyDirs, $posdir);
	    }
	}
    }
    else {
	@DaisyDirs = @all_posdirs;
    }

    # if there are no daisy database dirs, then nothing to do
    if (@DaisyDirs == 0) {
	loginfo("Skipping patch application: no eligible daisy database dirs");
	return($EXIT_NO_ELIGIBLE_DIRS);
    }

    loginfo("Begin applying Daisy patch: $patchfile");
    loginfo("Applying patch type: $PATCH_TYPE");

    #
    # The patch file contains a set of files to be replaced -
    # get a list of those files from the patch file.
    #
    @patchfile_catalog = get_patchfile_catalog($patchfile);
    if ($#patchfile_catalog < 0) {
	logerror("There are no files in patchfile: $patchfile\n");
	return($EXIT_ERROR);
    }

    # Backup the files to be replaced
    if (backup_files(\@DaisyDirs, \@patchfile_catalog) != 0) {
	return($EXIT_ERROR);
    }

    #
    # Unzip the patch file before heading into the loop... otherwise, the
    # patch file has to be uncompressed for each file extraction which takes
    # too much time when dealing with a edir update.
    #
    # And don't use the "mysystem()" function since that puts the output
    # of the command into the log file.
    #
    my $unzipped_patchfile = "";

    $unzipped_patchfile = qx(mktemp /tmp/applypatch.XXXXXXX);
    chomp($unzipped_patchfile);
    $unzipped_patchfile .= ".tar";
    system("gzip -dc $patchfile > $unzipped_patchfile");

    #
    # Outer loop through all the Daisy database dirs...
    #
    foreach my $posdir (@DaisyDirs) {

	#
	# Inner loop through the list of all the files in the patchfile and
	# extract the files from patch file
	#
	foreach my $thisfile (@patchfile_catalog) {

	    my $newmd5 = "";

	    # Extract this file.
	    mysystem("tar -C \"$posdir\" -xvf $unzipped_patchfile $thisfile");
	    if($? != 0) {
		logerror("\"$patchfile: $thisfile\": tar exit status $?\n");
		mysystem("rm -f $unzipped_patchfile");
		return($EXIT_ERROR);
	    }

	    if(! -f "$posdir/$thisfile") {
		logerror("\"$patchfile: $thisfile\": file not present after extract");
		mysystem("rm -f $unzipped_patchfile");
		return($EXIT_ERROR);
	    }

	    if(-s "$posdir/$thisfile" == 0) {
		logerror("\"$patchfile: $thisfile\": zero byte file after extract");
		mysystem("rm -f $unzipped_patchfile");
		return($EXIT_ERROR);
	    }

	    $newmd5 = get_md5sum("$posdir/$thisfile");

	    loginfo("Patched \"$posdir/$thisfile\" (New MD5=$newmd5)");
	}
    }

    loginfo("End applying Daisy patch: $patchfile");

    mysystem("rm -f $unzipped_patchfile");

    return($EXIT_OK);
}


#
# Back out a previously applied patch, if the patch was even previously applied.
#
sub reverse_patch
{
    my $patchfile = $_[0];

    my @patchfile_catalog = get_patchfile_catalog($patchfile);
    if ($#patchfile_catalog < 0) {
	logerror("There are no files in patchfile: $patchfile\n");
	return(0);
    }

    loginfo("!!!!!!!!!!!!!!! Begin Reverse Patch \"$patchfile\" !!!!!!!!!!!!!!!");


    foreach my $posdir (@DaisyDirs) {	# outer loop: relevant daisy dirs

	foreach my $thisfile (@patchfile_catalog) {	# inner loop: files in the patch

	    my $src_file = "$posdir/backups/" . basename($thisfile) . ".prepatch.$TIMESTAMP";
	    my $dst_file = "$posdir/$thisfile";

	    #
	    # No file to be backed out.
	    #
	    # This could happen if, for example, we were supposed to apply three
	    # ".patch" files, and, the first ".patch" file hosed before the subsequent
	    # two "backup" processes started. In short, this is a fairly harmless
	    # situation. If there is no backup file, then, we didn't touch anything
	    # to begin with.
	    #
	    if (! -f $src_file) {
		loginfo("Warning: $src_file does not exist. Cannot back-out change.");
		next;
	    }

	    loginfo("Backing out $src_file -> $dst_file");
	    mysystem("cp -f $src_file $dst_file 2> /dev/null");
	    if($? != 0) {
		# We're pretty much hosed at this point.
		logerror("Backout of $src_file $dst_file Failed ($?)");
	    }
	}
    }

    loginfo("!!!!!!!!!!!!!!! End Reverse Patch \"$patchfile\" !!!!!!!!!!!!!!!");

    return(0);
}


#
# Run any "postscript.pl" script left in a daisy database directory by a
# patch file.
#
# The name of the post patch script is "postpatch.pl" and if it exists,
# it will be in the "bin" directory of a daisy database directory.
#
# Remember, for Daisy, there can be one or more daisy database directories.
#
# Note that, we assume that post patch script "cleans up" if it fails along the
# way. We are giving the post patch script the "TIMESTAMP", which tells the
# script where our "backup" files are located; we are also giving it the path
# to our ".patch" files, so that it knows which files to "reverse out".
#
sub apply_postpatch
{
    my @patchfiles = @{$_[0]};
    my $timestamp = $_[1];
    my $include_crd_dirs = $_[2];
    my $rc = 0;

    determine_os();

    loginfo("PostPatch: processing list of Daisy dirs to post patch: @DaisyDirs");

    foreach my $posdir (@DaisyDirs) {

	my $script_path = "$posdir/bin/postpatch.pl";
	my $script_options = "";

	if (-f $script_path) {
	    my $md5sum = "";

	    $md5sum = get_md5sum($script_path); #Do this *before* dos2unix.
	    unless ($OS eq "RH72") {
		mysystem("dos2unix $script_path");
	    }
	    mysystem("chown root:root $script_path");
	    mysystem("chmod 555 $script_path");

	    if ($CONVERT_TO_ELAVON) {
		$script_options .= "--convert-to-elavon";
	    }

	    if ($INSTALL_FTD_FILES) {
		$script_options .= "--install-ftd-files";
	    }

	    loginfo("PostPatch: running $script_path $script_options '$posdir' '$timestamp' @patchfiles");
	    loginfo("PostPatch: $script_path MD5sum: $md5sum");
	    if ($DEBUG) {
		loginfo("PostPatch: DEBUG: command which runs postpatch.pl:");
		loginfo("PostPatch: DEBUG: cd $posdir && $script_path $script_options $posdir \"$timestamp\" @patchfiles");
	    }
	    else {
		mysystem("cd $posdir && $script_path $script_options $posdir \"$timestamp\" @patchfiles");
		if ($? != 0) {
		    logerror("PostPatch: $script_path returned $?");
		    $rc = 1;
		}
	    }

	}

	else {
	    loginfo("PostPatch: post patch script does not exist: $script_path");
	}
    }

    loginfo("PostPatch: processing done");

    return($rc);
}

#
# Look in the patch file and get a list of files which will be added.
#
sub get_patchfile_catalog
{
	my $patchfile = $_[0];
	my @files = ();

	open(PIPE, "tar -tzvf $patchfile |");
	while(<PIPE>) {
		chomp;
		my @tarline = split(/\s+/);

		# Listing the contents of the tar archive, the filename
		# is at index 5; be aware that the "filename" might include
		# directories; after splitting, the list should contain:
		#
		#	index 0: file perms		(-rwxr-xr-x)
		#	index 1: file owner/group	(tfsupport/daisy)
		#	index 2: file size		(75856)
		#	index 3: file date		(2010-12-20)
		#	index 4: file time		(10:29:26)
		#	index 5: file name		(updateos.pl)

		my $fileperms = $tarline[0];
		my $filename = $tarline[5];

		# skip a tar header - don't think tar does this anymore
		next if($filename =~ /^(\d+\.\d+\.\d+)(\s+)(Patch)$/);

		# skip a directory within the tar file
		next if ($fileperms =~ /^d/);

		# skip if the file in the tar archive exists as a directory
		# in any of the daisy database dirs
		my @posdirs = get_daisy_dirs();
		foreach my $posdir (@posdirs) {
			next if (-d "$posdir/$filename");
		}

		# ok, put this one on the list
		push(@files, $filename);
	}
	close(PIPE);

	return(@files);
}


#
# Create a log entry indicating what has changed.
#
sub log_patch_info
{
	my $patchfile = $_[0];
	my @patchfile_catalog = ();
	my $patchsize = "";


	loginfo("");
	loginfo("$PROGNAME  " . '$Revision: 1.31 $');
	loginfo("USER: $ENV{'USER'}");
	loginfo("UID: $UID");
	loginfo("EUID: $EUID");
	loginfo("CWD: " . getcwd());
	loginfo("POSDIR: $POSDIR");

	$patchsize = (-s $patchfile);
	loginfo("$PATCH_TYPE File: $patchfile (" . get_md5sum($patchfile) . ")");
	loginfo("$PATCH_TYPE File Size: $patchsize");
	loginfo("$PATCH_TYPE File MD5: " . get_md5sum($patchfile));
	loginfo("$PATCH_TYPE Timestamp: $TIMESTAMP");

	# Log the actual files in the patch.
	@patchfile_catalog = get_patchfile_catalog($patchfile);
	loginfo("$PATCH_TYPE Contents: @patchfile_catalog");
}


sub run_daisy_perms
{
    my @posdirs = ();
    my $perms_cmd = "$POSDIR/bin/dsyperms.pl";
    my $rc = 0;

    @posdirs = get_daisy_dirs();

    loginfo("PostPatch: processing list of Daisy dirs to post patch: @posdirs");

    foreach my $posdir (@posdirs) {

	if (-e $perms_cmd) {
	    loginfo("Running Daisy perms script: $perms_cmd");
	    $rc = mysystem("$perms_cmd $posdir");
	    if ($rc != 0) {
		logerror("Daisy perms script returns non-zero exit status: $rc");
	    }
	}
	else {
	    logerror("Daisy perms script not found: $perms_cmd");
	    $rc = 1;
	}
    }

    return($rc);
}


sub run_harden_linux
{
    my $harden_cmd = "$POSDIR/bin/harden_linux.pl";
    my $rc = 0;

    if (-e $harden_cmd) {
	loginfo("Running harden Linux script: $harden_cmd");
	$rc = mysystem("$harden_cmd");
	if ($rc != 0) {
	    logerror("Harden Linux script returns non-zero exit status: $rc");
	}
    }
    else {
	logerror("Harden Linux script not found: $harden_cmd");
	$rc = 1;
    }

    return($rc);
}


#
# Return the percentage of disk being used.
# This gives us a clue as to whether the disk is already "at capacity".
#
sub get_diskused
{
	my $directory = $_[0];
	my $returnval = -1;


	if(! -d "$directory") {
		return(-1);
	}


	open(PIPE, "df -h $directory |");
	while(<PIPE>) {
		next until(/(\s+)(\d+)(\%)(\s+)/);
		$returnval = $2;
		last;
	}
	close(PIPE);

	return($returnval);
}


sub get_md5sum
{
	my $filename = $_[0];
	my $ctx;
	my $md5sum = "";

	if(! -f "$filename") {
		return("");
	}

	open(FILE, "< $filename");
	$ctx = Digest::MD5->new;
	$ctx->addfile(*FILE);
	$md5sum = $ctx->hexdigest;
	close(FILE);

	return($md5sum);
}


sub determine_os
{
	$OS = "";

	# We could also use /usr/bin/lsb-release
	open(FILE, "< /etc/redhat-release");
	while(<FILE>) {

		# Fedora Core 3
		if(/(Fedora)(\s+)(Core)(\s+)(release)(\s+)(3)/) {
			$OS = "FC3";
			last;
		}

		# Fedora Core 4
		if(/(Fedora)(\s+)(Core)(\s+)(release)(\s+)(4)/) {
			$OS = "FC4";
			last;
		}

		# Fedora Core 5
		if(/(Fedora)(\s+)(Core)(\s+)(release)(\s+)(5)/) {
			$OS = "FC5";
			last;
		}

		# Redhat Enterprise Linux Client Workstation 5
		if( /(Client release)(\s+)(5)/ ) {
			$OS = "RHWS5";
			last;
		}

		# ES 6
		# Redhat Enterprise Linux Server 6
		if( /(release)(\s+)(6)/ ) {
			$OS = "RHEL6";
			last;
		}

		# ES 5
		# Redhat Enterprise Linux Server 5
		if( (/(release)(\s+)(5)/)
		||  (/(CentOS)([[:print:]]+)(\s)(5)/) ) {
			$OS = "RHEL5";
			last;
		}

		# EL 4
		# Redhat Enterprise Linux Server 4
		if( (/(release)(\s+)(4)/) 
		||  (/(CentOS)([[:print:]]+)(\s)(4)/) ) {
			$OS = "RHEL4";
			last;
		}

		# Redhat 7.2
		if(/(release)(\s+)(7\.2)/) {
			$OS = "RH72";
			last;
		}
	}
	close(FILE);

	return($OS);
}


sub loginfo
{
	my $message = $_[0];
	my $logtime = strftime("%Y-%m-%d %H:%M:%S", localtime());

	chomp($message);
	open LOGFILE, ">> $POSDIR/log/RTI-Patches.log";
		print LOGFILE "$logtime ($PROGNAME-$$) <I> $message\n";
	close LOGFILE;

	return "";
}


sub logerror
{
	my $message = $_[0];
	my $logtime = strftime("%Y-%m-%d %H:%M:%S", localtime());

	chomp($message);
	open LOGFILE, ">> $POSDIR/log/RTI-Patches.log";
		print LOGFILE "$logtime ($PROGNAME-$$) <E> $message\n";
	close LOGFILE;

	return "";
}


# PCI 6.5.6
# "some string; `cat /etc/passwd`" -> "some string cat /etc/passwd"
# "`echo $ENV; $(sudo ls)`" -> "echo ENV sudo ls"
# etc.
sub validate_input
{
	my $var = $_[0];
	my $temp = "";

	if(! $var) {
		return "";
	}

	$temp = $var;

	# "`bad command`" -> "bad command"
	$temp =~ s/\`//g;

	# "$(bad command)" -> "bad command"
	$temp =~ s/(\$\()(.*.)(\))/$2/g;


	# "stuff ; bad command" -> "stuff bad command"
	$temp =~ s/\;//g;

	# "stuff && bad command" -> "stuff bad command"
	$temp =~ s/\&//g;

	# "stuff | bad command" -> "stuff bad command"
	$temp =~ s/\|//g;

	# "stuff > bad command" -> "stuff bad command"
	$temp =~ s/\>//g;

	# "stuff < bad command" -> "stuff bad command"
	$temp =~ s/\<//g;

	# Filter non printables
	$temp =~ s/[[:cntrl:]]//g;


	return($temp);
}

#
# Subroutine to run system commands.  This logs an entry.
#
sub mysystem
{
	my $command = $_[0];
	my $returnval = "";
	my $log_file = "$POSDIR/log/RTI-Patches.log";

	my $stderr_dest = ($LOG_STDERR) ? "$log_file" : "/dev/null";

	system("$command >> $log_file 2>> $stderr_dest");
	$returnval = WEXITSTATUS($?);
	loginfo("$command Returned: $returnval");

	return ($returnval)
}

#
# Currently, a file is classified as a "Patch" or a "Revision" solely
# by whether it has the substring "revision" *anywhere* in it's name!
#
sub determine_patch_type
{
	$PATCH_TYPE = "Patch";
	if ($_[0] =~ m/revision/) {
		$PATCH_TYPE = "Revision";
	}
}


#
# Start Daisy.
#
# Return number less than 0 if can't start Daisy.
#
sub start_daisy
{
	loginfo("Changing to run level 3");
	mysystem("/sbin/init 3");
	if ($? != 0) {
		logerror("Unable to change run levels");
		return(-1);
	}

	return(0);
}

#
# Stop Daisy POS.
#
# Returns 0 on success, -1 on error
#
sub stop_daisy
{
	loginfo("Changing to run level 4");
	mysystem("/sbin/init 4");
	if ($? != 0) {
		logerror("Unable to change run levels");
		return(-1);
	}

	loginfo("Stopping all Daisy processes");
	if (-f "$POSDIR/utils/killemall") {
		mysystem("$POSDIR/utils/killemall");
		if ($? != 0) {
			logerror("Unable to stop Daisy processes");
			return(-1);
		}
	}

	sleep(5);	# give the system a chance to settle down

	return(0);
}


exit(main());

