[system] / branches / ghe3_dev / webwork2 / lib / WeBWorK / Utils.pm Repository:
ViewVC logotype

View of /branches/ghe3_dev/webwork2/lib/WeBWorK/Utils.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4166 - (download) (as text) (annotate)
Mon Jun 26 18:03:48 2006 UTC (6 years, 10 months ago) by gage
Original Path: trunk/webwork2/lib/WeBWorK/Utils.pm
File size: 26728 byte(s)
Make File::Copy available

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2006 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork-modperl/lib/WeBWorK/Utils.pm,v 1.74 2006/04/17 21:17:08 sh002i Exp $
    5 #
    6 # This program is free software; you can redistribute it and/or modify it under
    7 # the terms of either: (a) the GNU General Public License as published by the
    8 # Free Software Foundation; either version 2, or (at your option) any later
    9 # version, or (b) the "Artistic License" which comes with this package.
   10 #
   11 # This program is distributed in the hope that it will be useful, but WITHOUT
   12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   13 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
   14 # Artistic License for more details.
   15 ################################################################################
   16 
   17 package WeBWorK::Utils;
   18 use base qw(Exporter);
   19 
   20 =head1 NAME
   21 
   22 WeBWorK::Utils - useful utilities used by other WeBWorK modules.
   23 
   24 =cut
   25 
   26 use strict;
   27 use warnings;
   28 #use Apache::DB;
   29 use DateTime;
   30 use Date::Parse;
   31 use Date::Format;
   32 use File::Copy;
   33 use File::Spec;
   34 use Time::Zone;
   35 use MIME::Base64;
   36 use Errno;
   37 use File::Path qw(rmtree);
   38 use Carp;
   39 
   40 use constant MKDIR_ATTEMPTS => 10;
   41 
   42 # "standard" WeBWorK date/time format (for set definition files):
   43 #     %m/%d/%y at %I:%M%P
   44 # where:
   45 #     %m = month number, starting with 01
   46 #     %d = numeric day of the month, with leading zeros (eg 01..31)
   47 #     %Y = year (4 digits)
   48 #     %I = hour, 12 hour clock, leading 0's)
   49 #     %M = minute, leading 0's
   50 #     %P = am or pm (Yes %p and %P are backwards :)
   51 use constant DATE_FORMAT => "%m/%d/%Y at %I:%M%P %Z";
   52 
   53 our @EXPORT    = ();
   54 our @EXPORT_OK = qw(
   55   runtime_use
   56   readFile
   57   readDirectory
   58   listFilesRecursive
   59   surePathToFile
   60   makeTempDirectory
   61   removeTempDirectory
   62   path_is_subdir
   63   formatDateTime
   64   parseDateTime
   65   textDateTime
   66   intDateTime
   67   timeToSec
   68   writeLog
   69   writeCourseLog
   70   writeTimingLogEntry
   71   list2hash
   72   ref2string
   73   decodeAnswers
   74   encodeAnswers
   75   max
   76   pretty_print_rh
   77   cryptPassword
   78   dequote
   79   undefstr
   80   fisher_yates_shuffle
   81   sortByName
   82 );
   83 
   84 =head1 FUNCTIONS
   85 
   86 =cut
   87 
   88 ################################################################################
   89 # Lowlevel thingies
   90 ################################################################################
   91 
   92 sub runtime_use($) {
   93   croak "runtime_use: no module specified" unless $_[0];
   94   eval "package Main; require $_[0]; import $_[0]";
   95   die $@ if $@;
   96 }
   97 
   98 #sub backtrace($) {
   99 # my ($style) = @_;
  100 # $style = "warn" unless $style;
  101 # my @bt = DB->backtrace;
  102 # shift @bt; # Remove "backtrace" from the backtrace;
  103 # if ($style eq "die") {
  104 #   die join "\n", @bt;
  105 # } elsif ($style eq "warn") {
  106 #   warn join "\n", @bt;
  107 # } elsif ($style eq "print") {
  108 #   print join "\n", @bt;
  109 # } elsif ($style eq "return") {
  110 #   return @bt;
  111 # }
  112 #}
  113 
  114 ################################################################################
  115 # Filesystem interaction
  116 ################################################################################
  117 
  118 =head2 Filesystem interaction
  119 
  120 =over
  121 
  122 =cut
  123 
  124 # Convert Windows and Mac (classic) line endings to UNIX line endings in a string.
  125 # Windows uses CRLF, Mac uses CR, UNIX uses LF. (CR is ASCII 15, LF if ASCII 12)
  126 sub force_eoln($) {
  127   my ($string) = @_;
  128   $string =~ s/\015\012?/\012/g;
  129   return $string;
  130 }
  131 
  132 sub readFile($) {
  133   my $fileName = shift;
  134   local $/ = undef; # slurp the whole thing into one string
  135   open my $dh, "<", $fileName
  136     or die "failed to read file $fileName: $!";
  137   my $result = <$dh>;
  138   close $dh;
  139   return force_eoln($result);
  140 }
  141 
  142 sub readDirectory($) {
  143   my $dirName = shift;
  144   opendir my $dh, $dirName
  145     or die "Failed to read directory $dirName: $!";
  146   my @result = readdir $dh;
  147   close $dh;
  148   return @result;
  149 }
  150 
  151 =item @matches = listFilesRecusive($dir, $match_qr, $prune_qr, $match_full, $prune_full)
  152 
  153 Traverses the directory tree rooted at $dir, returning a list of files, named
  154 pipes, and sockets matching the regular expression $match_qr. Directories
  155 matching the regular expression $prune_qr are not visited.
  156 
  157 $match_full and $prune_full are boolean values that indicate whether $match_qr
  158 and $prune_qr, respectively, should be applied to the bare directory entry
  159 (false) or to the path to the directory entry relative to $dir.
  160 
  161 @matches is a list of paths relative to $dir.
  162 
  163 =cut
  164 
  165 sub listFilesRecursiveHelper($$$$$$);
  166 sub listFilesRecursive($;$$$$) {
  167   my ($dir, $match_qr, $prune_qr, $match_full, $prune_full) = @_;
  168   return listFilesRecursiveHelper($dir, "", $match_qr, $prune_qr, $match_full, $prune_full);
  169 }
  170 
  171 sub listFilesRecursiveHelper($$$$$$) {
  172   my ($base_dir, $curr_dir, $match_qr, $prune_qr, $match_full, $prune_full) = @_;
  173 
  174   my $full_dir = "$base_dir/$curr_dir";
  175 
  176   my @dir_contents = readDirectory($full_dir);
  177 
  178   my @matches;
  179 
  180   foreach my $dir_entry (@dir_contents) {
  181     my $full_path = "$full_dir/$dir_entry";
  182 
  183     # determine whether the entry is a directory or a file, taking into account the
  184     my $is_dir;
  185     my $is_file;
  186     if (-l $full_path) {
  187       my $link_target = "$full_dir/" . readlink $full_path;
  188       if ($link_target) {
  189         $is_dir = -d $link_target;
  190         $is_file = !$is_dir && -f $link_target || -p $link_target || -S $link_target;
  191       } else {
  192         warn "Couldn't resolve symlink $full_path: $!";
  193       }
  194     } else {
  195       $is_dir = -d $full_path;
  196       $is_file = !$is_dir && -f $full_path || -p $full_path || -S $full_path;
  197     }
  198 
  199     if ($is_dir) {
  200       # standard things to skip
  201       next if $dir_entry eq ".";
  202       next if $dir_entry eq "..";
  203 
  204       # skip unreadable directories (and broken symlinks, incidentally)
  205       unless (-r $full_path) {
  206         warn "Directory/symlink $full_path not readable";
  207         next;
  208       }
  209 
  210       # check $prune_qr
  211       my $subdir = ($curr_dir eq "") ? $dir_entry : "$curr_dir/$dir_entry";
  212       if (defined $prune_qr) {
  213         my $prune_string = $prune_full ? $subdir : $dir_entry;
  214         next if $prune_string =~ m/$prune_qr/;
  215       }
  216 
  217       # everything looks good, time to recurse!
  218       push @matches, listFilesRecursiveHelper($base_dir, $subdir, $match_qr, $prune_qr, $match_full, $prune_full);
  219     } elsif ($is_file) {
  220       my $file = ($curr_dir eq "") ? $dir_entry : "$curr_dir/$dir_entry";
  221       my $match_string = $match_full ? $file : $dir_entry;
  222       if (not defined $match_string or $match_string =~ m/$match_qr/) {
  223         push @matches, $file;
  224       }
  225     } else {
  226       # otherwise, it's a character device or a block device, and i don't
  227       # suppose we want anything to do with those ;-)
  228     }
  229   }
  230 
  231   return @matches;
  232 }
  233 
  234 # A very useful macro for making sure that all of the directories to a file have
  235 # been constructed.
  236 sub surePathToFile($$) {
  237   # constructs intermediate directories enroute to the file
  238   # the input path must be the path relative to this starting directory
  239   my $start_directory = shift;
  240   my $path = shift;
  241   my $delim = "/";
  242   unless ($start_directory and $path ) {
  243     warn "missing directory<br> surePathToFile  start_directory   path ";
  244     return '';
  245   }
  246   # use the permissions/group on the start directory itself as a template
  247   my ($perms, $groupID) = (stat $start_directory)[2,5];
  248   #warn "&urePathToTmpFile: perms=$perms groupID=$groupID\n";
  249 
  250   # if the path starts with $start_directory (which is permitted but optional) remove this initial segment
  251   $path =~ s|^$start_directory|| if $path =~ m|^$start_directory|;
  252 
  253 
  254   # find the nodes on the given path
  255         my @nodes = split("$delim",$path);
  256 
  257   # create new path
  258   $path = $start_directory; #convertPath("$tmpDirectory");
  259 
  260   while (@nodes>1) {  # the last node is the file name
  261     $path = $path . shift (@nodes) . "/"; #convertPath($path . shift (@nodes) . "/");
  262     #FIXME  this make directory command may not be fool proof.
  263     unless (-e $path) {
  264       mkdir($path, $perms)
  265         or warn "Failed to create directory $path with start directory $start_directory ";
  266     }
  267 
  268   }
  269 
  270   $path = $path . shift(@nodes); #convertPath($path . shift(@nodes));
  271   return $path;
  272 }
  273 
  274 sub makeTempDirectory($$) {
  275   my ($parent, $basename) = @_;
  276   # Loop until we're able to create a directory, or it fails for some
  277   # reason other than there already being something there.
  278   my $triesRemaining = MKDIR_ATTEMPTS;
  279   my ($fullPath, $success);
  280   do {
  281     my $suffix = join "", map { ('A'..'Z','a'..'z','0'..'9')[int rand 62] } 1 .. 8;
  282     $fullPath = "$parent/$basename.$suffix";
  283     $success = mkdir $fullPath;
  284   } until ($success or not $!{EEXIST});
  285   die "Failed to create directory $fullPath: $!"
  286     unless $success;
  287   return $fullPath;
  288 }
  289 
  290 sub removeTempDirectory($) {
  291   my ($dir) = @_;
  292   rmtree($dir, 0, 0);
  293 }
  294 
  295 =item path_is_subdir($path, $dir, $allow_relative)
  296 
  297 Ensures that $path refers to a location "inside" $dir. If $allow_relative is
  298 true and $path is not absoulte, it is assumed to be relative to $dir.
  299 
  300 The method of checking is rather rudimentary at the moment. First, upreferences
  301 ("..") are disallowed, in $path, then it is checked to make sure that some
  302 prefix of it matches $dir.
  303 
  304 If either of these checks fails, a false value is returned. Otherwise, a true
  305 value is returned.
  306 
  307 =cut
  308 
  309 sub path_is_subdir($$;$) {
  310   my ($path, $dir, $allow_relative) = @_;
  311 
  312   unless ($path =~ /^\//) {
  313     if ($allow_relative) {
  314       $path = "$dir/$path";
  315     } else {
  316       return 0;
  317     }
  318   }
  319 
  320   $path = File::Spec->canonpath($path);
  321   $path .= "/" unless $path =~ m|/$|;
  322   return 0 if $path =~ m#(^\.\.$|^\.\./|/\.\./|/\.\.$)#;
  323 
  324   $dir = File::Spec->canonpath($dir);
  325   $dir .= "/" unless $dir =~ m|/$|;
  326   return 0 unless $path =~ m|^$dir|;
  327 
  328   return 1;
  329 }
  330 
  331 =back
  332 
  333 =cut
  334 
  335 ################################################################################
  336 # Date/time processing
  337 ################################################################################
  338 
  339 =head2 Date/time processing
  340 
  341 =over
  342 
  343 =item $dateTime = parseDateTime($string, $display_tz)
  344 
  345 Parses $string as a datetime. If $display_tz is given, $string is assumed to be
  346 in that timezone. Otherwise, the server's timezone is used. The result,
  347 $dateTime, is an integer UNIX datetime (epoch) in the server's timezone.
  348 
  349 =cut
  350 
  351 # This is a modified version of the subroutine of the same name from WeBWorK
  352 # 1.9.05's scripts/FILE.pl (v1.13). It has been modified to understand time
  353 # zones. The time zone specification must appear at the end of the string and be
  354 # preceded by whitespace. The return value is a list consisting of the following
  355 # elements:
  356 #
  357 #     ($second, $minute, $hour, $day, $month, $year, $zone)
  358 #
  359 # $second, $minute, $hour, $day, and $month are zero-indexed. $year is the
  360 # number of years since 1900. $zone is a string (hopefully) representing the
  361 # time zone.
  362 #
  363 # Error handling has also been improved. Exceptions are now thrown for errors,
  364 # and more information is given about the nature of errors.
  365 #
  366 sub unformatDateAndTime {
  367   my ($string) = @_;
  368   my $orgString = $string;
  369 
  370   $string =~ s|^\s+||;
  371   $string =~ s|\s+$||;
  372   $string =~ s|at| at |i; ## OK if forget to enter spaces or use wrong case
  373   $string =~ s|AM| AM|i;  ## OK if forget to enter spaces or use wrong case
  374   $string =~ s|PM| PM|i;  ## OK if forget to enter spaces or use wrong case
  375   $string =~ s|,| at |; ## start translating old form of date/time to new form
  376 
  377   # case where the at is missing: MM/DD/YYYY at HH:MM AMPM ZONE
  378   unformatDateAndTime_error($orgString, "The 'at' appears to be missing.")
  379     if $string =~ m|^\s*[\/\d]+\s+[:\d]+|;
  380 
  381   my ($date, $at, $time, $AMPM, $TZ) = split /\s+/, $string;
  382 
  383   unformatDateAndTime_error($orgString, "The date and/or time appear to be missing.", $date, $time, $AMPM, $TZ)
  384     unless defined $date and defined $at and defined $time;
  385 
  386   # deal with military time
  387   unless ($time =~ /:/) {
  388     {  ##bare block for 'case" structure
  389       $time =~ /(\d\d)(\d\d)/;
  390       my $tmp_hour = $1;
  391       my $tmp_min = $2;
  392       if ($tmp_hour eq '00') {$time = "12:$tmp_min"; $AMPM = 'AM';last;}
  393       if ($tmp_hour eq '12') {$time = "12:$tmp_min"; $AMPM = 'PM';last;}
  394       if ($tmp_hour < 12) {$time = "$tmp_hour:$tmp_min"; $AMPM = 'AM';last;}
  395       if ($tmp_hour < 24) {
  396         $tmp_hour = $tmp_hour - 12;
  397         $time = "$tmp_hour:$tmp_min";
  398         $AMPM = 'PM';
  399       }
  400     }  ##end of bare block for 'case" structure
  401 
  402   }
  403 
  404   # default value for $AMPM
  405   $AMPM = "AM" unless defined $AMPM;
  406 
  407   my ($mday, $mon, $year, $wday, $yday, $sec, $pm, $min, $hour);
  408   $sec=0;
  409   $time =~ /^([0-9]+)\s*\:\s*([0-9]*)/;
  410   $min=$2;
  411   $hour = $1;
  412   unformatDateAndTime_error($orgString, "Hour must be in the range [1,12].", $date, $time, $AMPM, $TZ)
  413     if $hour < 1 or $hour > 12;
  414   unformatDateAndTime_error($orgString, "Minute must be in the range [0-59].", $date, $time, $AMPM, $TZ)
  415     if $min < 0 or $min > 59;
  416   $pm = 0;
  417   $pm = 12 if ($AMPM =~/PM/ and $hour < 12);
  418   $hour += $pm;
  419   $hour = 0 if ($AMPM =~/AM/ and $hour == 12);
  420   $date =~  m|([0-9]+)\s*/\s*([0-9]+)/\s*([0-9]+)|;
  421   $mday =$2;
  422   $mon=($1-1);
  423   unformatDateAndTime_error($orgString, "Day must be in the range [1,31].", $date, $time, $AMPM, $TZ)
  424     if $mday < 1 or $mday > 31;
  425   unformatDateAndTime_error($orgString, "Month must be in the range [1,12].", $date, $time, $AMPM, $TZ)
  426     if $mon < 0 or $mon > 11;
  427   $year=$3;
  428   $wday="";
  429   $yday="";
  430   return ($sec, $min, $hour, $mday, $mon, $year, $TZ);
  431 }
  432 
  433 sub unformatDateAndTime_error {
  434 
  435   if (@_ > 2) {
  436     my ($orgString, $error, $date, $time, $AMPM, $TZ) = @_;
  437     $date = "(undefined)" unless defined $date;
  438     $time = "(undefined)" unless defined $time;
  439     $AMPM = "(undefined)" unless defined $AMPM;
  440     $TZ   = "(undefined)" unless defined $TZ;
  441     die "Incorrect date/time format \"$orgString\": $error\n",
  442       "Correct format is MM/DD/YY at HH:MM AMPM ZONE\n",
  443       "\tdate = $date\n",
  444       "\ttime = $time\n",
  445       "\tampm = $AMPM\n",
  446       "\tzone = $TZ\n";
  447   } else {
  448     my ($orgString, $error) = @_;
  449     die "Incorrect date/time format \"$orgString\": $error\n",
  450       "Correct format is MM/DD/YY at HH:MM AMPM ZONE\n";
  451   }
  452 }
  453 
  454 sub parseDateTime($;$) {
  455   my ($string, $display_tz) = @_;
  456   $display_tz ||= "local";
  457   #warn "parseDateTime('$string', '$display_tz')\n";
  458 
  459   # use WeBWorK 1 date parsing routine
  460   my ($second, $minute, $hour, $day, $month, $year, $zone) = unformatDateAndTime($string);
  461   my $zone_str = defined $zone ? $zone : "UNDEF";
  462   #warn "\tunformatDateAndTime: $second $minute $hour $day $month $year $zone_str\n";
  463 
  464   # DateTime expects month 1-12, not 0-11
  465   $month++;
  466 
  467   # Do what Time::Local does to ambiguous years
  468   {
  469     my $ThisYear     = (localtime())[5]; # FIXME: should be relative to $string's timezone
  470     my $Breakpoint   = ($ThisYear + 50) % 100;
  471     my $NextCentury  = $ThisYear - $ThisYear % 100;
  472        $NextCentury += 100 if $Breakpoint < 50;
  473     my $Century      = $NextCentury - 100;
  474     my $SecOff       = 0;
  475 
  476     if ($year >= 1000) {
  477       # leave alone
  478     } elsif ($year < 100 and $year >= 0) {
  479       $year += ($year > $Breakpoint) ? $Century : $NextCentury;
  480       $year += 1900;
  481     } else {
  482       $year += 1900;
  483     }
  484   }
  485 
  486   my $epoch;
  487 
  488   if (defined $zone and $zone ne "") {
  489     if (DateTime::TimeZone->is_valid_name($zone)) {
  490       #warn "\t\$zone is valid according to DateTime::TimeZone\n";
  491 
  492       my $dt = new DateTime(
  493         year      => $year,
  494         month     => $month,
  495         day       => $day,
  496         hour      => $hour,
  497         minute    => $minute,
  498         second    => $second,
  499         time_zone => $zone,
  500       );
  501       #warn "\t\$dt = ", $dt->strftime(DATE_FORMAT), "\n";
  502 
  503       $epoch = $dt->epoch;
  504       #warn "\t\$dt->epoch = $epoch\n";
  505     } else {
  506       #warn "\t\$zone is invalid according to DateTime::TimeZone, so we ask Time::Zone\n";
  507 
  508       # treat the date/time as UTC
  509       my $dt = new DateTime(
  510         year      => $year,
  511         month     => $month,
  512         day       => $day,
  513         hour      => $hour,
  514         minute    => $minute,
  515         second    => $second,
  516         time_zone => "UTC",
  517       );
  518       #warn "\t\$dt = ", $dt->strftime(DATE_FORMAT), "\n";
  519 
  520       # convert to an epoch value
  521       my $utc_epoch = $dt->epoch
  522         or die "Date/time '$string' not representable as an epoch. Get more bits!\n";
  523       #warn "\t\$utc_epoch = $utc_epoch\n";
  524 
  525       # get offset for supplied timezone and utc_epoch
  526       my $offset = tz_offset($zone, $utc_epoch) or die "Time zone '$zone' not recognized.\n";
  527       #warn "\t\$zone is valid according to Time::Zone (\$offset = $offset)\n";
  528 
  529       #$epoch = $utc_epoch + $offset;
  530       ##warn "\t\$epoch = \$utc_epoch + \$offset = $epoch\n";
  531 
  532       $dt->subtract(seconds => $offset);
  533       #warn "\t\$dt - \$offset = ", $dt->strftime(DATE_FORMAT), "\n";
  534 
  535       $epoch = $dt->epoch;
  536       #warn "\t\$epoch = $epoch\n";
  537     }
  538   } else {
  539     #warn "\t\$zone not supplied, using \$display_tz\n";
  540 
  541     my $dt = new DateTime(
  542       year      => $year,
  543       month     => $month,
  544       day       => $day,
  545       hour      => $hour,
  546       minute    => $minute,
  547       second    => $second,
  548       time_zone => $display_tz,
  549     );
  550     #warn "\t\$dt = ", $dt->strftime(DATE_FORMAT), "\n";
  551 
  552     $epoch = $dt->epoch;
  553     #warn "\t\$epoch = $epoch\n";
  554   }
  555 
  556   return $epoch;
  557 }
  558 
  559 =item $string = formatDateTime($dateTime, $display_tz)
  560 
  561 Formats the UNIX datetime $dateTime in the standard WeBWorK datetime format.
  562 $dateTime is assumed to be in the server's time zone. If $display_tz is given,
  563 the datetime is converted from the server's timezone to the timezone specified.
  564 
  565 =cut
  566 
  567 sub formatDateTime($;$) {
  568   my ($dateTime, $display_tz) = @_;
  569   $display_tz ||= "local";
  570   #warn "formatDateTime('$dateTime', '$display_tz')\n";
  571 
  572   my $dt = DateTime->from_epoch(epoch => $dateTime, time_zone => $display_tz);
  573   #warn "\t\$dt = ", $dt->strftime(DATE_FORMAT), "\n";
  574   return $dt->strftime(DATE_FORMAT);
  575 }
  576 
  577 =item $string = textDateTime($string_or_dateTime)
  578 
  579 Accepts a UNIX datetime or a formatted string, returns a formatted string.
  580 
  581 =cut
  582 
  583 sub textDateTime($) {
  584   return ($_[0] =~ m/^\d*$/) ? formatDateTime($_[0]) : $_[0];
  585 }
  586 
  587 =item $dateTIme = intDateTime($string_or_dateTime)
  588 
  589 Accepts a UNIX datetime or a formatted string, returns a UNIX datetime.
  590 
  591 =cut
  592 
  593 sub intDateTime($) {
  594   return ($_[0] =~ m/^\d*$/) ?  $_[0] : parseDateTime($_[0]);
  595 }
  596 
  597 =item $timeinsec = timeToSec($time)
  598 
  599 Makes a stab at converting a time (with a possible unit) into a number of
  600 seconds.
  601 
  602 =cut
  603 
  604 sub timeToSec($) {
  605     my $t = shift();
  606     if ( $t =~ /^(\d+)\s+(\S+)\s*$/ ) {
  607   my ( $val, $unit ) = ( $1, $2 );
  608   if ( $unit =~ /month/i || $unit =~ /mon/i ) {
  609       $val *= 18144000;  # this assumes 30 days/month
  610   } elsif ( $unit =~ /week/i || $unit =~ /wk/i ) {
  611       $val *= 604800;
  612   } elsif ( $unit =~ /day/i || $unit =~ /dy/i ) {
  613       $val *= 86400;
  614   } elsif ( $unit =~ /hour/i || $unit =~ /hr/i ) {
  615       $val *= 3600;
  616   } elsif ( $unit =~ /minute/i || $unit =~ /min/i ) {
  617       $val *= 60;
  618   } elsif ( $unit =~ /second/i || $unit =~ /sec/i || $unit =~ /^s$/i ) {
  619       # do nothing
  620   } else {
  621       warn("Unrecognized time unit $unit.\nAssuming seconds.\n");
  622   }
  623   return $val;
  624     } elsif ( $t =~ /^(\d+)$/ ) {
  625   return $t;
  626     } else {
  627   warn("Unrecognized time interval: $t\n");
  628   return 0;
  629     }
  630 }
  631 
  632 =back
  633 
  634 =cut
  635 
  636 ################################################################################
  637 # Logging
  638 ################################################################################
  639 
  640 sub writeLog($$@) {
  641   my ($ce, $facility, @message) = @_;
  642   unless ($ce->{webworkFiles}->{logs}->{$facility}) {
  643     warn "There is no log file for the $facility facility defined.\n";
  644     return;
  645   }
  646   my $logFile = $ce->{webworkFiles}->{logs}->{$facility};
  647   surePathToFile($ce->{webworkDirs}->{root}, $logFile);
  648   local *LOG;
  649   if (open LOG, ">>", $logFile) {
  650     print LOG "[", time2str("%a %b %d %H:%M:%S %Y", time), "] @message\n";
  651     close LOG;
  652   } else {
  653     warn "failed to open $logFile for writing: $!";
  654   }
  655 }
  656 
  657 sub writeCourseLog($$@) {
  658   my ($ce, $facility, @message) = @_;
  659   unless ($ce->{courseFiles}->{logs}->{$facility}) {
  660     warn "There is no course log file for the $facility facility defined.\n";
  661     return;
  662   }
  663   my $logFile = $ce->{courseFiles}->{logs}->{$facility};
  664   surePathToFile($ce->{courseDirs}->{root}, $logFile);
  665   local *LOG;
  666   if (open LOG, ">>", $logFile) {
  667     print LOG "[", time2str("%a %b %d %H:%M:%S %Y", time), "] @message\n";
  668     close LOG;
  669   } else {
  670     warn "failed to open $logFile for writing: $!";
  671   }
  672 }
  673 
  674 # $ce - a WeBWork::CourseEnvironment object
  675 # $function - fully qualified function name
  676 # $details - any information, do not use the characters '[' or ']'
  677 # $beginEnd - the string "begin", "intermediate", or "end"
  678 # use the intermediate step begun or completed for INTERMEDIATE
  679 # use an empty string for $details when calling for END
  680 # Information printed in format:
  681 # [formatted date & time ] processID unixTime BeginEnd $function  $details
  682 sub writeTimingLogEntry($$$$) {
  683   my ($ce, $function, $details, $beginEnd) = @_;
  684   $beginEnd = ($beginEnd eq "begin") ? ">" : ($beginEnd eq "end") ? "<" : "-";
  685   writeLog($ce, "timing", "$$ ".time." $beginEnd $function [$details]");
  686 }
  687 
  688 ################################################################################
  689 # Data munging
  690 ################################################################################
  691 
  692 sub list2hash(@) {
  693   map {$_ => "0"} @_;
  694 }
  695 
  696 sub refBaseType($) {
  697   my $ref = shift;
  698   $ref =~ m/(\w+)\(/; # this might not be robust...
  699   return $1;
  700 }
  701 
  702 sub ref2string($;$);
  703 sub ref2string($;$) {
  704   my $ref = shift;
  705   my $dontExpand = shift || {};
  706   my $refType = ref $ref;
  707   my $result;
  708   if ($refType and not $dontExpand->{$refType}) {
  709     my $baseType = refBaseType($ref);
  710     $result .= '<font size="1" color="grey">' . $refType;
  711     $result .= " ($baseType)" if $baseType and $refType ne $baseType;
  712     $result .= ":</font><br>";
  713     $result .= '<table border="1" cellpadding="2">';
  714     if ($baseType eq "HASH") {
  715       my %hash = %$ref;
  716       foreach (sort keys %hash) {
  717         $result .= '<tr valign="top">';
  718         $result .= "<td>$_</td>";
  719         $result .= "<td>" . ref2string($hash{$_}, $dontExpand) . "</td>";
  720         $result .= "</tr>";
  721       }
  722     } elsif ($baseType eq "ARRAY") {
  723       my @array = @$ref;
  724       # special case for Problem, Set, and User objects, which are defined
  725       # using lists and contain a @FIELDS package variable:
  726       no strict 'refs';
  727       my @FIELDS = eval { @{$refType."::FIELDS"} };
  728       use strict 'refs';
  729       undef @FIELDS unless scalar @FIELDS == scalar @array and not $@;
  730       foreach (0 .. $#array) {
  731         $result .= '<tr valign="top">';
  732         $result .= "<td>$_</td>";
  733         $result .= "<td>".$FIELDS[$_]."</td>" if @FIELDS;
  734         $result .= "<td>" . ref2string($array[$_], $dontExpand) . "</td>";
  735         $result .= "</tr>";
  736       }
  737     } elsif ($baseType eq "SCALAR") {
  738       my $scalar = $$ref;
  739       $result .= '<tr valign="top">';
  740       $result .= "<td>$scalar</td>";
  741       $result .= "</tr>";
  742     } else {
  743       # perhaps a coderef? in any case, i don't feel like dealing with it!
  744       $result .= '<tr valign="top">';
  745       $result .= "<td>$ref</td>";
  746       $result .= "</tr>";
  747     }
  748     $result .= "</table>"
  749   } else {
  750     $result .= defined $ref ? $ref : '<font color="red">undef</font>';
  751   }
  752 }
  753 our $BASE64_ENCODED = 'base64_encoded:';
  754 #  use constant BASE64_ENCODED = 'base64_encoded;
  755 #  was not evaluated in the matching and substitution
  756 #  statements
  757 sub decodeAnswers($) {
  758   my $string = shift;
  759   return unless defined $string and $string;
  760 
  761   if ($string =~/^$BASE64_ENCODED/o) {
  762     $string =~ s/^$BASE64_ENCODED//o;
  763     $string = decode_base64($string);
  764   }
  765 
  766   my @array = split m/##/, $string;
  767   $array[$_] =~ s/\\#\\/#/g foreach 0 .. $#array;
  768   push @array, "" if @array%2;
  769   return @array; # it's actually a hash ;)
  770 }
  771 
  772 sub encodeAnswers(\%\@) {
  773   my %hash = %{ shift() };
  774   my @order = @{ shift() };
  775   my $string = "";
  776   foreach my $name (@order) {
  777     my $value = defined $hash{$name} ? $hash{$name} : "";
  778     $name  =~ s/#/\\#\\/g; # this is a WEIRD way to escape things
  779     $value =~ s/#/\\#\\/g; # and it's not my fault!
  780     if ($value =~ m/\\$/) {
  781       # if the value ends with a backslash, string2hash will
  782       # interpret that as a normal escape sequence (not part
  783       # of the weird pound escape sequence) if the next
  784       # character is &. So we have to protect against this.
  785       # will adding a spcae at the end of the last answer
  786       # hurt anything? i don't think so...
  787       $value .= " ";
  788     }
  789     $string .= "$name##$value##"; # this is also not my fault
  790   }
  791   $string =~ s/##$//; # remove last pair of hashs
  792 
  793   $string = $BASE64_ENCODED.encode_base64($string, "");
  794   # Empty string in second argument prevents end-of-line characters from being used.
  795   # This is nice for examining database contents manually since it prevents newlines
  796   # from being introduced into database records.
  797 
  798   return $string;
  799 }
  800 
  801 sub max(@) {
  802   my $soFar;
  803   foreach my $item (@_) {
  804     $soFar = $item unless defined $soFar;
  805     if ($item > $soFar) {
  806       $soFar = $item;
  807     }
  808   }
  809   return defined $soFar ? $soFar : 0;
  810 }
  811 
  812 sub pretty_print_rh($) {
  813   my $rh = shift;
  814   foreach my $key (sort keys %{$rh})  {
  815     warn "  $key => ",$rh->{$key},"\n";
  816   }
  817 }
  818 
  819 sub cryptPassword($) {
  820   my ($clearPassword) = @_;
  821   my $salt = join("", ('.','/','0'..'9','A'..'Z','a'..'z')[rand 64, rand 64]);
  822   my $cryptPassword = crypt($clearPassword, $salt);
  823   return $cryptPassword;
  824 }
  825 
  826 # from the Perl Cookbook, first edition, page 25:
  827 sub dequote($) {
  828   local $_ = shift;
  829   my ($white, $leader); # common whitespace and common leading string
  830   if (/^\s*(?:([^\w\s]+)(\s*).*\n)(?:\s*\1\2?.*\n)+$/) {
  831     ($white, $leader) = ($2, quotemeta($1));
  832   } else {
  833     ($white, $leader) = (/^(\s+)/, '');
  834   }
  835   s/^\s*?$leader(?:$white)?//gm;
  836   return $_;
  837 }
  838 
  839 sub undefstr($@) {
  840   map { defined $_ ? $_ : $_[0] } @_[1..$#_];
  841 }
  842 
  843 # shuffle an array in place
  844 # Perl Cookbook, Recipe 4.17. Randomizing an Array
  845 sub fisher_yates_shuffle {
  846   my $array = shift;
  847   my $i;
  848   for ($i = @$array; --$i; ) {
  849     my $j = int rand ($i+1);
  850     next if $i == $j;
  851     @$array[$i,$j] = @$array[$j,$i];
  852   }
  853 }
  854 
  855 ################################################################################
  856 # Sorting
  857 ################################################################################
  858 
  859 # p. 101, Camel, 3rd ed.
  860 # The <=> and cmp operators return -1 if the left operand is less than the
  861 # right operand, 0 if they are equal, and +1 if the left operand is greater
  862 # than the right operand.
  863 sub sortByName($@) {
  864   my ($field, @items) = @_;
  865   return sort {
  866     my @aParts = split m/(?<=\D)(?=\d)|(?<=\d)(?=\D)/, defined $field ? $a->$field : $a;
  867     my @bParts = split m/(?<=\D)(?=\d)|(?<=\d)(?=\D)/, defined $field ? $b->$field : $b;
  868     while (@aParts and @bParts) {
  869       my $aPart = shift @aParts;
  870       my $bPart = shift @bParts;
  871       my $aNumeric = $aPart =~ m/^\d*$/;
  872       my $bNumeric = $bPart =~ m/^\d*$/;
  873 
  874       # numbers should come before words
  875       return -1 if     $aNumeric and not $bNumeric;
  876       return +1 if not $aNumeric and     $bNumeric;
  877 
  878       # both have the same type
  879       if ($aNumeric and $bNumeric) {
  880         next if $aPart == $bPart; # check next pair
  881         return $aPart <=> $bPart; # compare numerically
  882       } else {
  883         next if $aPart eq $bPart; # check next pair
  884         return $aPart cmp $bPart; # compare lexicographically
  885       }
  886     }
  887     return +1 if @aParts; # a has more sections, should go second
  888     return -1 if @bParts; # a had fewer sections, should go first
  889   } @items;
  890 }
  891 
  892 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9