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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 6370 - (download) (as text) (annotate)
Thu Jul 15 03:18:34 2010 UTC (2 years, 10 months ago) by gage
File size: 31916 byte(s)
changes to support geogebra applets


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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9