#!/usr/bin/perl -T

#                                                             02 March 2016
#  submit    assignment-number file(s)
#  unsubmit  assignment-number file(s)
#  check     assignment-number
#  makeit    assignment-number [file]
#  protect   assignment-number file(s)
#  unprotect assignment-number file(s)
#  retrieve  assignment-number [-dDATE] file(s)
#
#  The submit program can be invoked in seven different ways:
#
#      /home/classes/csXYZ/bin/submit  1  Makefile tokenize.c unique.c time.log
#
#  submits the named source files as your solution to Homework #1;
#
#      /home/classes/csXYZ/bin/check  2
#
#  lists the files that you have submitted for Homework #2;
#
#      /home/classes/csXYZ/bin/unsubmit  3  error.submit bogus.solution
#
#  deletes the named files that you had submitted previously for Homework #3
#  (i.e., withdraws them from submission, which is useful if you accidentally
#  submit the wrong file);
#
#      /home/classes/csXYZ/bin/makeit  4  tokenize unique
#
#  runs "make" on the files that you submitted previously for Homework #4;
#
#      /home/classes/csXYZ/bin/protect  5  tokenize.c time.log
#
#  protects the named files that you submitted previously for Homework #5 (so
#  they cannot be deleted accidentally);
#
#      /home/classes/csXYZ/bin/unprotect  6  unique.c time.log
#
#  unprotects the named files that you submitted previously for Homework #6
#  (so they can be deleted); and
#
#      /home/classes/csXYZ/bin/diffit  7  unique.c time.log
#
#  uses /usr/bin/diff to compare the named source files with the versions that
#  you submitted previously for Homework #7; and
#
#      /home/classes/csXYZ/bin/retrieve  8  Csquash.c
#
#  retrieves copies of the named files that you submitted previously for
#  Homework #8 (in case you accidentally delete your own copies).
#
############################################################################
#  NOTES:                                                                  #
#  1) These commands must run with the effective groupid of the course TA  #
#       group, from which the course number itself is derived.             #
#  2) submit and unsubmit are disabled when the directory where the files  #
#       are saved contains a file name ".GRADED".                          #
#  3) submit also checks the files into RCS if the homework directory      #
#       (e.g., /c/cs323/SUBMIT/1) contains a file named ".RCS".  Moreover, #
#           /c/csXYZ/bin/retrieve  7  -d"2011/09/21 20:00" Csquash.c       #
#       will retrieve the latest submission prior to the time specified    #
#       (see the -d flag under "man co" for how to specify the time); and  #
#           /c/csXYZ/bin/submit    7  -nVERSION Csquash.c                  #
#       will name the revision so that                                     #
#           /c/csXYZ/bin/retrieve  7  -nVERSION Csquash.c                  #
#       can retrieve it.                                                   #
#  4) submit skips files that /usr/bin/file reports as type ELF, unless    #
#       the homework directory contains a file named ".ELF".               #
#  5) submit requires that a log file be submitted (or already exist) when #
#       the homework directory contains a file named ".LOG".               #
#  6) The -V<digit> option before the assignment number allows alternate   #
#       versions to be submitted, etc. in a different directory if the     #
#       homework directory contains a file named ".VERSION".               #
############################################################################

$ENV{'PATH'} = "/usr/bin:/bin";                 # Untaint system() calls
$ENV{'LC_CTYPE'} = "en_US.UTF-8";		# Fix strange bug
$ENV{'LC_ALL'} = "";
delete @ENV{qw(IFS ENV BASH_ENV CDPATH)};       # No insecure setgid warnings
umask(002);                                     # Force correct protection

$command = $0;                                  # Extract command name
$command =~ s{^.*/}{};

$course = getgrgid($));                         # Get course number from egid
($course =~ s[^(cs\d\d\d)(ta|ucg)$][$1])
   || die ("$command: insufficient privileges\n");

# Undocumented Feature: The -V<digit> option allows alternate versions to
#  be submitted, unsubmitted, etc. in a different directory.
if ($ARGV[0] =~ /^-V[1-1]$/) {
   ($version = shift @ARGV) =~ s{-}{.};
} elsif ($ARGV[0] =~ /^-V/) {
   die ("$command: invalid version number $ARGV[0]\n");
}

$hwkN = shift;                                  # Homework # begins with digit?

$CLASS = "/home/classes/$course";               # Class directory
$class = "$CLASS/class";                        # Subdirectory for students
$submit = "$CLASS/SUBMIT";                      # Subdirectory for submissions

$OPEN = (-e "$submit/$hwkN/.OPEN");             # skip class membership test

$userid = (Getpwuid($<))[0];                    # Get userid

if ($OPEN) {
    $name = "$userid";
} else {
    $name = "UNKNOWN";                              # Search for name of directory
    opendir (LS, $class)                            #   owned by this student
	|| die ("$command: cannot open $class\n");
    while ($file = readdir (LS)) {
	next if ($file =~ m{^\.});                  # Ignore . files
	@fields = stat ("$class/$file");
	if (Getpwuid ($fields[4]) eq $userid || $file =~ m{\.$userid$}) {
	    $name = $file;
	    last;
	}
    }
    closedir (LS);

    (-d "$class/$name")                  # Student in course?
	|| die ("$command: unknown student $userid\n");
    $name =~ s{\?\?}{n};                            # Convert ?? to n (for pena)
}


($hwkN =~ /^\d/)
   || die ("$command: invalid assignment #$hwkN\n");

(-d "$submit/$hwkN")                            # Assignment being collected?
   || die ("$command: not collecting Homework #$hwkN\n");
					
!defined ($version)                             # Alternate versions allowed?
   || (-e "$submit/$hwkN/.VERSION")
   || die ("$command: -V flag not enabled for Homework #$hwkN\n");

$directory = "$submit/$hwkN/$name$version";     # Create subdirectory to submit
$directory = &untaint ($directory);
(-d $directory)
   || mkdir ($directory, 0770)                  # drwxrwx---
   || die ("$command: cannot create directory for Homework #$hwkN\n");
chmod (02770, $directory);

if ($RCS = (-e "$submit/$hwkN/.RCS")) {         # Use rcs on directory?
    $rcs = "$directory/RCS";                     # Create subdirectory
    $rcs = &untaint ($rcs);
    (-d $rcs)
	|| mkdir ($rcs, 0770)                     # drwxrwx---
	|| die ("$command: cannot create RCS directory for Homework #$hwkN\n");
    chmod (02770, $rcs);
}

$ELF = (-e "$submit/$hwkN/.ELF");               # Allow submission of object

$LOG = (-e "$submit/$hwkN/.LOG");               # Require submission of log file

# locations of staff-supplied files to copy
$hwkN =~ m{^(\d+).*$};
$REQUIRED = "$CLASS/hw$1/Required";
$OPTIONAL = "$CLASS/hw$1/Optional";
$BUILD = "$CLASS/hw$1/Build";

##########
# UNSUBMIT
if ($command =~ /unsubmit/i) {                  # Delete files in holding area
    (! -e "$directory/.GRADED")
	|| die ("$command: submission graded; no deletions allowed\n");
    
    foreach $file (@ARGV) {
	$file = &baseName ($file);                # Reduce to basename
	$hold = "$directory/$file";               # Name in holding area
	&unsubmit ($file, $hold);
    }
    
    # unsubmit (baseName,  holdName)
    sub unsubmit {
	my ($base, $hold) = @_;
	
	(-f $hold)
	    || die ("$command: cannot delete Homework #$hwkN: $base\n");
	(-W $hold || ! -O $hold)
	    || die ("$command: Homework #$hwkN: $base is protected\n");
	print "Deleting $base\n";
	$hold = &untaint ($hold);
	unlink ($hold);
    }
    
    
########
# SUBMIT
} elsif ($command =~ /submit/i) {               # Copy files into holding area
    (! -e "$directory/.GRADED")
	|| die ("$command: submission graded; no additions allowed\n");
    
    if ($RCS && ($ARGV[0] =~ m{^-n})) {          # Save version name (if any)
	$versionName = shift @ARGV;
	$versionName =~ s{\s+}{,}g;               # Change whitespace to commas
	$versionName = &untaint ($versionName);
    }
    
    if ($LOG) {                                  # Require log file?
	@files = Backtick ("/bin/ls $directory"); # Existing log files
	@old = grep (m{log}, @files);
	chomp (@old);
	@new = grep (m{log}, @ARGV);              # Basenames of new log files
	foreach $new (@new) {
	    $new = &baseName ($new);
	}
	if (@old > 1) {
	    die ("$command: multiple log files; please unsubmit all but one\n");
	} elsif (@new > 1) {
	    die ("$command: multiple log files submitted\n");
	} elsif (@old == 0 && @new == 0) {
	    die ("$command: no log file submitted\n");
	} elsif (@old > 0 && @new > 0 && $new[0] ne $old[0]) {
	    die ("$command: log files have different names ($new[0] vs $old[0])\n");
	}
    }

   foreach $file (@ARGV) {
       $file = &baseName ($file);                # Reduce to basename
       $hold = "$directory/$file";               # Name in holding area
       $rcs  = "$directory/RCS/$file,v";         # Name in RCS directory
       &submit ($file, $hold, $rcs);
    }
    
    (! $version)                                 # Reminder for -V flag
	|| (-e "$directory/README")
	|| die ("$command: warning: no README file submitted\n");
    
    # submit (baseName, holdName, rcsName)
    sub submit {
	my ($base, $hold, $rcs) = @_;
	
	(-f $base)
	    || die ("$command: cannot copy Homework #$hwkN: $base\n");
	(-R $base)
	    || die ("$command: cannot read Homework #$hwkN: $base\n");
	(-W $hold)
	    || (! -e $hold)
	    || die ("$command: Homework #$hwkN: $base is protected\n");
	$base = &untaint ($base);
	$hold = &untaint ($hold);
	$rcs  = &untaint ($rcs);
	
	if (&type($base) =~ m{^ELF}i && !$ELF) {
	    print "Skipping object file $base\n";
	    return;
	}
	
	if ($course eq "cs323"                    # CPSC 323 only accepts
	    && $hwkN !~ m{vote}) {              #   source and log files
	    my $type = &type($base);
	    $type =~ s{\s*$}{};
	    if ($type !~ m{C program|perl|python|ruby|bourne}i
		&& $base !~ m{^[Mm]akefile$|\.[ch]$|log|README}) {
		print "Skipping non-source/log $type file $base\n";
		return;
	    }
	}
	
	print "Copying $base\n";
	if ($RCS) {
	    System ("/bin/cp    $base $hold");
	} else {
	    System ("/bin/cp -i $base $hold");
	}
	utime (time(), time(), $hold);
	
	if (&type($hold) =~ m{perl|python|ruby|bourne}i) {
	    chmod (0750, $hold);                    # -rwxr-x--- for Perl scripts
	} elsif (&type($hold) =~ m{^ELF}i && $ELF) {
	    chmod (0750, $hold);                    # -rwxr-x--- for object files
	} else {
	    chmod (0640, $hold);                    # -rw-r-----
	}
	
	chmod (0550, $rcs)                        # Make RCS file -r-xr-x--- if
	    if (-X $hold && -e $rcs);              #   submitted file is executable
	
	System ("/usr/bin/rcs ci -f -l -q -m. -t-. $versionName $hold")
	    if ($RCS);
    }


#######
# CHECK
} elsif ($command =~ /check/i) {                # List files in holding area
    System ("/bin/ls -l $directory");
    

########        # Todo: Run as anonymous user
# MAKEIT
} elsif ($command =~ /makeit/i) {               # Execute make in holding area
    (-f "$directory/Makefile")                   # Cannot run without Makefile
	|| (-f "$directory/makefile")
	|| (-f "$OPTIONAL/makefile")
	|| die ("$command: no makefile\n");
    
    $makearea = &untaint ("/tmp/.$name$$");      # Create temporary directory
    mkdir ($makearea, 0770)                      # -rwxrwx---
	|| die ("$command: cannot create temporary directory\n");
    chmod (02770, $makearea);
    
    $SIG{'HUP'} = $SIG{'INT'} = $SIG{'QUIT'}     # Ignore signals
    = $SIG{'TERM'} = 'IGNORE';
    chdir ($makearea);                           # Copy files
    @ls = Backtick ("/bin/ls $directory");       # Cannot use "cp $directory/* ."
    if (@ls)
    {
	foreach $ls (@ls) {
	    chomp ($ls);
	    next                                      # Skip RCS and *.o
		if ($ls =~ m{^RCS$} || $ls =~ m{\.o$});
	    $ls = &untaint ($ls);
	    $file = "$directory/$ls";
	    System ("/bin/cp $file $makearea");
	    push @cFiles, "$makearea/$file"           # Keep list of .c files
		if ($ls =~ m{\.c$});
	    (System ("/bin/grep /home/accts/$userid $file"))
		|| warn ("$command: possible dependency on private file in $ls\n\n");
	}
    }

    Prepare($makearea, $REQUIRED, $OPTIONAL);
    
    if (!($fork = fork)) {                       # Non-setgid child runs make
	$) = $(;                                  # Run as real group
	$SIG{'HUP'} = $SIG{'INT'} = $SIG{'QUIT'}  # Allow signals
	= $SIG{'TERM'} = 'DEFAULT';
	print "Making @ARGV\n";
	utime (time(), time(), @cFiles);
	if ($course =~ m{.23}) {                  # CPSC ?23 adds cs323/bin/Wall
	    $ENV{PATH} =                           #   to search path to force
		"/c/cs323/bin/Wall:$ENV{PATH}";  #   -std=c99 -Wall -pedantic
	}
	$args = &untaint ("@ARGV");
	exec ("hash gcc cc ; /usr/bin/make -e $args")
	    || die ("$command: cannot execute make -e $args\n");
    }
    waitpid ($fork, 0);

    Build($BUILD);

    System ("/bin/rm -rf $makearea");            # Clean up
    

###########
# UNPROTECT
} elsif ($command =~ /unprotect/i) {            # Unprotect files in holding area
   foreach $file (@ARGV) {
      $file = &baseName ($file);                # Reduce to basename
      $hold = "$directory/$file";               # Name in holding area
      &unprotect ($file, $hold);
   }

   # unprotect (baseName, holdName)
   sub unprotect {
      my ($base, $hold) = @_;

      (-f $hold)
	 || die ("$command: cannot unprotect Homework #$hwkN: $base\n");
      print "Unprotecting $base\n";
      $hold = &untaint ($hold);

      if (&type($hold) =~ m{perl|python|ruby|bourne}i) {
	chmod (0750, $hold);                    # -rwxr-x--- for Perl scripts
      } else {
	chmod (0640, $hold);                    # -rw-r-----
      }
   }


#########
# PROTECT
} elsif ($command =~ /protect/i) {              # Protect files in holding area
   foreach $file (@ARGV) {
      $file = &baseName ($file);                # Reduce to basename
      $hold = "$directory/$file";               # Name in holding area
      &protect ($file, $hold);
   }

   # protect (baseName, holdName)
   sub protect {
      my ($base, $hold) = @_;

      (-f $hold)
	 || die ("$command: cannot protect Homework #$hwkN: $base\n");
      print "Protecting $base\n";
      $hold = &untaint ($hold);

      if (&type($hold) =~ m{perl|python|ruby|bourne}i) {
	chmod (0550, $hold);                    # -r-xr-x--- for Perl scripts
      } else {
	chmod (0440, $hold);                    # -r--r-----
      }
   }


########
# RETRIEVE
} elsif ($command =~ /retrieve/i) {             # Get files from holding area
   if ($RCS && ($ARGV[0] =~ m{^-n})) {          # Save version name (if any)
      $versionName = shift @ARGV;
      $versionName =~ s{^-n}{-r}g;              # Change -n to -r
      $versionName =~ s{\s+}{,}g;               # Change whitespace to commas
      $versionName = &untaint ($versionName);
   }

   if ($ARGV[0] =~ m{^-d}) {                    # Save date (if any)
      $date = shift @ARGV;
      $date =~ s{\s+}{,}g;                      # Change whitespace to commas
      $date = &untaint ($date);
      $RCS ||
	 warn ("$command: date ignored; can only retrieve latest version\n");
   }

   foreach $file (@ARGV) {
      $file = &baseName ($file);                # Reduce to basename
      if ($RCS) {                               # Name in holding area
	 $hold = "$directory/RCS/$file,v";
      } else {
	 $hold = "$directory/$file";
      }
      &retrieve ($file, $hold);
   }

   # retrieve(baseName, holdName)
   sub retrieve {
      my ($base, $hold) = @_;

      (-f $hold)
	 || die ("$command: cannot retrieve Homework #$hwkN: $base\n");
      (-R $hold)
	 || die ("$command: cannot read Homework #$hwkN: $base\n");
      (! -e $base)
	 || die ("$command: Homework #$hwkN: $base already exists\n");
      (-W ".")
	 || die ("$command: Homework #$hwkN: $base is not writable\n");
      print "Copying $base\n";
      $file = &untaint ($file);
      $hold = &untaint ($hold);
      $base = &untaint ($base);
      if ($RCS) {
	 System ("/usr/bin/rcs co -I -zLT $date $versionName $base $hold");
	 if (-f "$directory/$file") {
	    System ("/bin/chmod --reference=$directory/$file $base");
	 }
      } else {
	 System ("/bin/cp -i $hold $base");
      }
   }

########
# VERSIONS
} elsif ($command =~ /versions/i) {             # List versions in RCS
   if ($RCS) {
       foreach $file (@ARGV) {
	   $file = &baseName ($file);                # Reduce to basename
	   $hold = "$directory/RCS/$file";
	   $hold = untaint($hold);
	   system("/usr/bin/rlog $hold");
       }
   } else {
       warn("version control is not enabled");
   }

########
# DIFFIT
} elsif ($command =~ /diffit/i) {               # rcsdiff files in holding area
   foreach $file (@ARGV) {
      $file = &baseName ($file);                # Reduce to basename
      $hold = "$directory/$file";               # Name in holding area
      &diffit ($file, $hold);
   }

   # diffit (baseName, holdName)
   sub diffit {
      my ($base, $hold) = @_;

      (-R $hold)
	 || die ("$command: cannot read Homework #$hwkN: $base\n");
      print "Comparing to $base\n";
      $hold = &untaint ($hold);
      $base = &untaint ($base);
      System ("/usr/bin/diff $hold $base");
   }


########        # Todo: Run as anonymous user
# TESTIT
} elsif ($command =~ /testit/i) {               # Execute test in holding area
   $hwkN =~ m{^(\d+).*$};                       # Find script
   (@ARGV > 0)
      || die ("usage: testit assignment-number program-to-test\n");

   $script = "$CLASS/Hwk$1/test.@ARGV";
   $script = &untaint ($script);

   print "$script\n";
   (-x $script)
      || die ("$command: script $script not found\n");

   $testarea = &untaint ("/tmp/.$name$$");      # Create temporary directory
   mkdir ($testarea, 0770)                      # -rwxrwx---
      || die ("$command: cannot create temporary directory\n");
   chmod (02770, $testarea);

   $SIG{'HUP'} = $SIG{'INT'} = $SIG{'QUIT'}     # Ignore signals
	       = $SIG{'TERM'} = 'IGNORE';
   chdir ($testarea);                           # Copy files
   @ls = Backtick ("/bin/ls $directory");       # Cannot use "cp $directory/* ."
   foreach $ls (@ls) {
      chomp ($ls);
      next                                      # Skip RCS and *.o
	 if ($ls =~ m{^RCS$} || $ls =~ m{\.o$});
      $ls = untaint ($ls);
      $file = "$directory/$ls";
      System ("/bin/cp $file $testarea");
      push @cFiles, "$makearea/$file"           # Keep list of .c files
	 if ($ls =~ m{\.c$});
      (System ("/bin/grep /home/accts/$userid $file"))
	 || warn ("$command: possible dependency on private file in $ls\n\n");
   }

   Prepare($testarea, $REQUIRED, $OPTIONAL);
   
   if (!($fork = fork)) {                       # Non-setgid child runs test
      $) = $(;                                  # Run as real group
      $SIG{'HUP'} = $SIG{'INT'} = $SIG{'QUIT'}  # Allow signals
		  = $SIG{'TERM'} = 'DEFAULT';
      print "Executing $script\n";
      utime (time(), time(), @cFiles);
      exec ($script)
	 || die ("$command: cannot execute $script\n");
   }

   waitpid ($fork, 0);
   System ("/bin/rm -rf $testarea");            # Clean up
}

exit(0);

sub type {                                      # Find file type
   my ($name) = @_;
   return Backtick ("/usr/bin/file -b $name");
}

sub baseName {                                  # Return basename
   my ($name) = @_;
   $name =~ s{.*/}{};
   return $name;
}

sub untaint {                                   # Return untainted argument
   my ($tainted) = @_;
   $tainted =~ /^(.+)$/;
   return $1;
}


# Get the userid from the uid (2006/08/25: getpwuid() hangs!)
sub Getpwuid  { # (UID)
    my ($uid) = @_;
    $uid = &untaint ($uid);
    my $result = `/usr/bin/getent passwd $uid`;
    $result = &untaint ($result);
    my @fields = split (":", $result);
    return $fields[0];
}


# Execute COMMAND after escaping metacharacters
sub System {
   my ($command) = @_;
   my @args = split (m{\s+}, $command);
   return system (@args);
}


# Backtick COMMAND after escaping metacharacters
sub Backtick {
   my ($command) = @_;
   my @args = split (m{\s+}, $command);
   open (my $fh, '-|', @args);
   my @output = <$fh>;
   close ($fh);
   return (@output > 1) ? @output : $output[0];
}

sub Prepare {
    my ($makearea, $REQUIRED, $OPTIONAL) = @_;
    # copy required staff-supplied files
    if (-d "$REQUIRED") {
	@ls = Backtick("/bin/ls $REQUIRED");
	foreach $ls (@ls) {
	    if ($ls ne "") {
		chomp($ls);
		$ls = &untaint($ls);
		$file = "$REQUIRED/$ls";
		print "Copying $ls";
		System("/bin/cp $file $makearea");
	    }
	}
    }
    
    # copy optional staff-supplied files if they aren't already there
    if (-d "$OPTIONAL") {
	@ls = Backtick("/bin/ls $OPTIONAL");
	foreach $ls (@ls) {
	    if ($ls ne "") {
		chomp($ls);
		$ls = &untaint($ls);
		$file = "$OPTIONAL/$ls";
		unless (-e "$makearea/$ls")
		{
		    print("Copying $ls\n");
		    System("/bin/cp $file $makearea");
		}
	    }
	}
    }
}

sub Build {
    my ($BUILD) = @_;
    if (-d "$BUILD") {
	@ls = Backtick("/bin/ls $BUILD");
	foreach $ls (@ls) {
	    if ($ls ne "") {
		chomp($ls);
		$ls = &untaint($ls);
		
		if (!($fork = fork)) {                       # Non-setgid child runs make
		    $) = $(;                                  # Run as real group
		    $SIG{'HUP'} = $SIG{'INT'} = $SIG{'QUIT'}  # Allow signals
		    = $SIG{'TERM'} = 'DEFAULT';
		    print "Making from $ls\n";
		    if ($course =~ m{.23}) {                  # CPSC ?23 adds cs323/bin/Wall
			$ENV{PATH} =                           #   to search path to force
			    "/c/cs323/bin/Wall:$ENV{PATH}";  #   -std=c99 -Wall -pedantic
		    }
		    
		    $file = "$BUILD/$ls";
		    exec ("hash gcc cc ; /usr/bin/make -e -f $file")
			|| die ("$command: cannot execute make -e -f $file\n");
		    
		}
		waitpid ($fork, 0);
	    }
    	}
    }
}
