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

View of /trunk/webwork2/lib/WeBWorK/Utils.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4051 - (download) (as text) (annotate)
Mon Apr 17 21:17:12 2006 UTC (7 years, 1 month ago) by sh002i
File size: 26705 byte(s)
Resolve bug #994 ("Try it" yields:  sourceFilePath is unsafe!)

SetMaker uses a relative path for sourceFilePath, which is nice, but
which path_is_subdir didn't account for. Added thrid argument,
$allow_relative, to that utility which causes $dir to be prepended to
$path. Modified Problem.pm to trigger this behavior.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9