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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 6775 - (view) (download) (as text)

1 : sh002i 440 ################################################################################
2 : sh002i 1663 # WeBWorK Online Homework Delivery System
3 : sh002i 5319 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
4 : gage 6182 # $CVSHeader: webwork2/lib/WeBWorK/Utils.pm,v 1.83 2009/07/12 23:48:00 gage Exp $
5 : sh002i 1663 #
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 : sh002i 440 ################################################################################
16 :    
17 : sh002i 410 package WeBWorK::Utils;
18 : sh002i 818 use base qw(Exporter);
19 : sh002i 410
20 : sh002i 455 =head1 NAME
21 :    
22 :     WeBWorK::Utils - useful utilities used by other WeBWorK modules.
23 :    
24 :     =cut
25 :    
26 : sh002i 412 use strict;
27 :     use warnings;
28 : sh002i 1257 #use Apache::DB;
29 : sh002i 2770 use DateTime;
30 : sh002i 2774 use Date::Parse;
31 : sh002i 2782 use Date::Format;
32 : gage 4166 use File::Copy;
33 : sh002i 4015 use File::Spec;
34 : sh002i 2858 use Time::Zone;
35 : gage 3223 use MIME::Base64;
36 : sh002i 1145 use Errno;
37 : sh002i 1150 use File::Path qw(rmtree);
38 : gage 6366 use Storable;
39 : sh002i 2003 use Carp;
40 : sh002i 412
41 : sh002i 1145 use constant MKDIR_ATTEMPTS => 10;
42 :    
43 : sh002i 2770 # "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 : sh002i 2858 # %Y = year (4 digits)
49 : sh002i 2770 # %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 : sh002i 2858 use constant DATE_FORMAT => "%m/%d/%Y at %I:%M%P %Z";
53 : sh002i 2770
54 : sh002i 410 our @EXPORT = ();
55 : sh002i 424 our @EXPORT_OK = qw(
56 : gage 5679 after
57 :     before
58 :     between
59 :     constituency_hash
60 :     cryptPassword
61 :     decodeAnswers
62 :     dequote
63 :     encodeAnswers
64 :     fisher_yates_shuffle
65 :     formatDateTime
66 : gage 6082 has_aux_files
67 : gage 5679 intDateTime
68 :     list2hash
69 : sh002i 2754 listFilesRecursive
70 : sh002i 2769 makeTempDirectory
71 : gage 5679 max
72 : gage 6082 not_blank
73 : gage 5679 parseDateTime
74 :     path_is_subdir
75 :     pretty_print_rh
76 :     readDirectory
77 :     readFile
78 :     ref2string
79 : sh002i 2769 removeTempDirectory
80 : gage 5679 runtime_use
81 :     sortByName
82 :     surePathToFile
83 : sh002i 2744 textDateTime
84 : glarose 3377 timeToSec
85 : gage 5679 trim_spaces
86 :     undefstr
87 :     writeCourseLog
88 : sh002i 562 writeLog
89 :     writeTimingLogEntry
90 : sh002i 424 );
91 : sh002i 410
92 : sh002i 2955 =head1 FUNCTIONS
93 :    
94 :     =cut
95 :    
96 : sh002i 2769 ################################################################################
97 :     # Lowlevel thingies
98 :     ################################################################################
99 :    
100 : sh002i 4494 # 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 : sh002i 410 die $@ if $@;
121 :     }
122 :    
123 : sh002i 2769 #sub backtrace($) {
124 : sh002i 1257 # 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 : malsyned 1045
139 : sh002i 2769 ################################################################################
140 :     # Filesystem interaction
141 :     ################################################################################
142 :    
143 : sh002i 2955 =head2 Filesystem interaction
144 :    
145 :     =over
146 :    
147 :     =cut
148 :    
149 : sh002i 2754 # 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 : sh002i 410 sub readFile($) {
158 :     my $fileName = shift;
159 : sh002i 1150 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 : gage 2481 return force_eoln($result);
165 : sh002i 410 }
166 : sh002i 412
167 : malsyned 974 sub readDirectory($) {
168 : sh002i 1150 my $dirName = shift;
169 :     opendir my $dh, $dirName
170 : sh002i 1529 or die "Failed to read directory $dirName: $!";
171 : sh002i 1150 my @result = readdir $dh;
172 :     close $dh;
173 :     return @result;
174 : malsyned 974 }
175 :    
176 : sh002i 2754 =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 : sh002i 3579
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 : sh002i 2754 # 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 : sh002i 3579 } elsif ($is_file) {
245 : sh002i 2754 my $file = ($curr_dir eq "") ? $dir_entry : "$curr_dir/$dir_entry";
246 :     my $match_string = $match_full ? $file : $dir_entry;
247 : dpvc 2825 if (not defined $match_string or $match_string =~ m/$match_qr/) {
248 : sh002i 2754 push @matches, $file;
249 :     }
250 : sh002i 3579 } 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 : sh002i 2754 }
254 :     }
255 :    
256 :     return @matches;
257 :     }
258 :    
259 : sh002i 2769 # A very useful macro for making sure that all of the directories to a file have
260 :     # been constructed.
261 :     sub surePathToFile($$) {
262 : gage 3610 # constructs intermediate directories enroute to the file
263 : sh002i 2769 # the input path must be the path relative to this starting directory
264 :     my $start_directory = shift;
265 :     my $path = shift;
266 : gage 3610 my $delim = "/";
267 : sh002i 2769 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 : gage 3610 while (@nodes>1) { # the last node is the file name
286 : sh002i 2769 $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 : gage 3614 or warn "Failed to create directory $path with start directory $start_directory ";
291 : sh002i 2769 }
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 : sh002i 4051 =item path_is_subdir($path, $dir, $allow_relative)
321 : sh002i 4015
322 : sh002i 4051 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 : sh002i 4015
325 : sh002i 4051 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 : sh002i 4015 If either of these checks fails, a false value is returned. Otherwise, a true
330 :     value is returned.
331 :    
332 :     =cut
333 :    
334 : sh002i 4051 sub path_is_subdir($$;$) {
335 :     my ($path, $dir, $allow_relative) = @_;
336 : sh002i 4015
337 : sh002i 4051 unless ($path =~ /^\//) {
338 :     if ($allow_relative) {
339 :     $path = "$dir/$path";
340 :     } else {
341 :     return 0;
342 :     }
343 :     }
344 :    
345 : sh002i 4015 $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 : sh002i 2955 =back
357 :    
358 :     =cut
359 :    
360 : sh002i 2769 ################################################################################
361 :     # Date/time processing
362 :     ################################################################################
363 :    
364 : sh002i 2770 =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 : sh002i 2858 # 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 : sh002i 3710 # and more information is given about the nature of errors.
390 : sh002i 2858 #
391 :     sub unformatDateAndTime {
392 :     my ($string) = @_;
393 : sh002i 3710 my $orgString = $string;
394 :    
395 : sh002i 2858 $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 : sh002i 3710
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 : sh002i 2858 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 : sh002i 3710
429 :     # default value for $AMPM
430 :     $AMPM = "AM" unless defined $AMPM;
431 :    
432 :     my ($mday, $mon, $year, $wday, $yday, $sec, $pm, $min, $hour);
433 : sh002i 2858 $sec=0;
434 :     $time =~ /^([0-9]+)\s*\:\s*([0-9]*)/;
435 :     $min=$2;
436 :     $hour = $1;
437 : sh002i 3710 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 : sh002i 2858 $pm = 0;
442 :     $pm = 12 if ($AMPM =~/PM/ and $hour < 12);
443 :     $hour += $pm;
444 :     $hour = 0 if ($AMPM =~/AM/ and $hour == 12);
445 : sh002i 3710 $date =~ m|([0-9]+)\s*/\s*([0-9]+)/\s*([0-9]+)|;
446 : sh002i 2858 $mday =$2;
447 :     $mon=($1-1);
448 : sh002i 3710 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 : sh002i 2858 $year=$3;
453 :     $wday="";
454 :     $yday="";
455 :     return ($sec, $min, $hour, $mday, $mon, $year, $TZ);
456 :     }
457 :    
458 : sh002i 3710 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 : sh002i 2858
479 : sh002i 2770 sub parseDateTime($;$) {
480 :     my ($string, $display_tz) = @_;
481 : gage 6182 warn "time zone not defined".caller() unless defined($display_tz);
482 : sh002i 2770 $display_tz ||= "local";
483 : gage 6775
484 :    
485 : sh002i 2858 # 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 : sh002i 2770
490 : sh002i 2858 # DateTime expects month 1-12, not 0-11
491 :     $month++;
492 : sh002i 2770
493 : sh002i 2858 # 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 : sh002i 2770
512 : sh002i 2858 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 : sh002i 412 }
584 :    
585 : sh002i 2770 =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 : gage 6407 warn "Utils::formatDateTime is not a method. ", join(" ",caller(2)) if ref($dateTime); # catch bad calls to Utils::formatDateTime
596 : gage 6667 warn "not defined formatDateTime('$dateTime', '$display_tz') ",join(" ",caller(2)) unless $display_tz;
597 :     $dateTime = $dateTime ||0; # do our best to provide default values
598 :     $display_tz ||= "local"; # do our best to provide default vaules
599 : sh002i 2770
600 :     my $dt = DateTime->from_epoch(epoch => $dateTime, time_zone => $display_tz);
601 : sh002i 2858 #warn "\t\$dt = ", $dt->strftime(DATE_FORMAT), "\n";
602 : sh002i 2770 return $dt->strftime(DATE_FORMAT);
603 : sh002i 412 }
604 : sh002i 422
605 : sh002i 2770 =item $string = textDateTime($string_or_dateTime)
606 :    
607 :     Accepts a UNIX datetime or a formatted string, returns a formatted string.
608 :    
609 :     =cut
610 :    
611 : sh002i 2744 sub textDateTime($) {
612 :     return ($_[0] =~ m/^\d*$/) ? formatDateTime($_[0]) : $_[0];
613 :     }
614 :    
615 : sh002i 2770 =item $dateTIme = intDateTime($string_or_dateTime)
616 :    
617 :     Accepts a UNIX datetime or a formatted string, returns a UNIX datetime.
618 :    
619 :     =cut
620 :    
621 : sh002i 2744 sub intDateTime($) {
622 :     return ($_[0] =~ m/^\d*$/) ? $_[0] : parseDateTime($_[0]);
623 :     }
624 :    
625 : glarose 3377 =item $timeinsec = timeToSec($time)
626 :    
627 :     Makes a stab at converting a time (with a possible unit) into a number of
628 :     seconds.
629 :    
630 :     =cut
631 :    
632 :     sub timeToSec($) {
633 :     my $t = shift();
634 :     if ( $t =~ /^(\d+)\s+(\S+)\s*$/ ) {
635 :     my ( $val, $unit ) = ( $1, $2 );
636 :     if ( $unit =~ /month/i || $unit =~ /mon/i ) {
637 :     $val *= 18144000; # this assumes 30 days/month
638 :     } elsif ( $unit =~ /week/i || $unit =~ /wk/i ) {
639 :     $val *= 604800;
640 :     } elsif ( $unit =~ /day/i || $unit =~ /dy/i ) {
641 :     $val *= 86400;
642 :     } elsif ( $unit =~ /hour/i || $unit =~ /hr/i ) {
643 :     $val *= 3600;
644 :     } elsif ( $unit =~ /minute/i || $unit =~ /min/i ) {
645 :     $val *= 60;
646 :     } elsif ( $unit =~ /second/i || $unit =~ /sec/i || $unit =~ /^s$/i ) {
647 :     # do nothing
648 :     } else {
649 :     warn("Unrecognized time unit $unit.\nAssuming seconds.\n");
650 :     }
651 :     return $val;
652 :     } elsif ( $t =~ /^(\d+)$/ ) {
653 :     return $t;
654 :     } else {
655 :     warn("Unrecognized time interval: $t\n");
656 :     return 0;
657 :     }
658 :     }
659 :    
660 : sh002i 4503 =item before($time, $now)
661 :    
662 :     True if $now is less than $time. If $now is not specified, the value of time()
663 :     is used.
664 :    
665 :     =cut
666 :    
667 :     sub before { return (@_==2) ? $_[1] < $_[0] : time < $_[0] }
668 :    
669 :     =item after($time, $now)
670 :    
671 :     True if $now is greater than $time. If $now is not specified, the value of time()
672 :     is used.
673 :    
674 :     =cut
675 :    
676 :     sub after { return (@_==2) ? $_[1] > $_[0] : time > $_[0] }
677 :    
678 :     =item between($start, $end, $now)
679 :    
680 :     True if $now is greater than or equal to $start and less than or equal to $end.
681 :     If $now is not specified, the value of time() is used.
682 :    
683 :     =cut
684 :    
685 :     sub between { my $t = (@_==3) ? $_[2] : time; return $t >= $_[0] && $t <= $_[1] }
686 :    
687 : sh002i 2770 =back
688 :    
689 :     =cut
690 :    
691 : sh002i 2769 ################################################################################
692 :     # Logging
693 :     ################################################################################
694 :    
695 : sh002i 562 sub writeLog($$@) {
696 :     my ($ce, $facility, @message) = @_;
697 :     unless ($ce->{webworkFiles}->{logs}->{$facility}) {
698 :     warn "There is no log file for the $facility facility defined.\n";
699 :     return;
700 :     }
701 :     my $logFile = $ce->{webworkFiles}->{logs}->{$facility};
702 : gage 3309 surePathToFile($ce->{webworkDirs}->{root}, $logFile);
703 : sh002i 562 local *LOG;
704 :     if (open LOG, ">>", $logFile) {
705 :     print LOG "[", time2str("%a %b %d %H:%M:%S %Y", time), "] @message\n";
706 :     close LOG;
707 :     } else {
708 :     warn "failed to open $logFile for writing: $!";
709 :     }
710 :     }
711 : sh002i 558
712 : gage 1387 sub writeCourseLog($$@) {
713 :     my ($ce, $facility, @message) = @_;
714 :     unless ($ce->{courseFiles}->{logs}->{$facility}) {
715 :     warn "There is no course log file for the $facility facility defined.\n";
716 :     return;
717 :     }
718 :     my $logFile = $ce->{courseFiles}->{logs}->{$facility};
719 : gage 3309 surePathToFile($ce->{courseDirs}->{root}, $logFile);
720 : gage 1387 local *LOG;
721 :     if (open LOG, ">>", $logFile) {
722 :     print LOG "[", time2str("%a %b %d %H:%M:%S %Y", time), "] @message\n";
723 :     close LOG;
724 :     } else {
725 :     warn "failed to open $logFile for writing: $!";
726 :     }
727 :     }
728 :    
729 : sh002i 631 # $ce - a WeBWork::CourseEnvironment object
730 :     # $function - fully qualified function name
731 :     # $details - any information, do not use the characters '[' or ']'
732 : sh002i 692 # $beginEnd - the string "begin", "intermediate", or "end"
733 :     # use the intermediate step begun or completed for INTERMEDIATE
734 : sh002i 631 # use an empty string for $details when calling for END
735 : gage 3306 # Information printed in format:
736 :     # [formatted date & time ] processID unixTime BeginEnd $function $details
737 : sh002i 562 sub writeTimingLogEntry($$$$) {
738 :     my ($ce, $function, $details, $beginEnd) = @_;
739 : sh002i 692 $beginEnd = ($beginEnd eq "begin") ? ">" : ($beginEnd eq "end") ? "<" : "-";
740 : sh002i 562 writeLog($ce, "timing", "$$ ".time." $beginEnd $function [$details]");
741 :     }
742 :    
743 : sh002i 2769 ################################################################################
744 :     # Data munging
745 :     ################################################################################
746 : gage 5679 ## Utility function to trim whitespace off the start and end of its input
747 :     sub trim_spaces {
748 :     my $in = shift;
749 : gage 5932 return '' unless $in; # skip blank spaces
750 : gage 5679 $in =~ s/^\s*(.*?)\s*$/$1/;
751 :     return($in);
752 :     }
753 : sh002i 2769 sub list2hash(@) {
754 : malsyned 970 map {$_ => "0"} @_;
755 :     }
756 :    
757 : sh002i 2769 sub refBaseType($) {
758 :     my $ref = shift;
759 :     $ref =~ m/(\w+)\(/; # this might not be robust...
760 :     return $1;
761 : malsyned 970 }
762 :    
763 : sh002i 424 sub ref2string($;$);
764 :     sub ref2string($;$) {
765 :     my $ref = shift;
766 :     my $dontExpand = shift || {};
767 :     my $refType = ref $ref;
768 : sh002i 422 my $result;
769 : sh002i 424 if ($refType and not $dontExpand->{$refType}) {
770 :     my $baseType = refBaseType($ref);
771 :     $result .= '<font size="1" color="grey">' . $refType;
772 : sh002i 425 $result .= " ($baseType)" if $baseType and $refType ne $baseType;
773 : sh002i 424 $result .= ":</font><br>";
774 :     $result .= '<table border="1" cellpadding="2">';
775 :     if ($baseType eq "HASH") {
776 :     my %hash = %$ref;
777 :     foreach (sort keys %hash) {
778 :     $result .= '<tr valign="top">';
779 :     $result .= "<td>$_</td>";
780 :     $result .= "<td>" . ref2string($hash{$_}, $dontExpand) . "</td>";
781 :     $result .= "</tr>";
782 :     }
783 :     } elsif ($baseType eq "ARRAY") {
784 :     my @array = @$ref;
785 : sh002i 429 # special case for Problem, Set, and User objects, which are defined
786 :     # using lists and contain a @FIELDS package variable:
787 :     no strict 'refs';
788 :     my @FIELDS = eval { @{$refType."::FIELDS"} };
789 :     use strict 'refs';
790 :     undef @FIELDS unless scalar @FIELDS == scalar @array and not $@;
791 : sh002i 424 foreach (0 .. $#array) {
792 :     $result .= '<tr valign="top">';
793 :     $result .= "<td>$_</td>";
794 : sh002i 429 $result .= "<td>".$FIELDS[$_]."</td>" if @FIELDS;
795 : sh002i 424 $result .= "<td>" . ref2string($array[$_], $dontExpand) . "</td>";
796 :     $result .= "</tr>";
797 :     }
798 :     } elsif ($baseType eq "SCALAR") {
799 :     my $scalar = $$ref;
800 :     $result .= '<tr valign="top">';
801 :     $result .= "<td>$scalar</td>";
802 :     $result .= "</tr>";
803 : sh002i 422 } else {
804 : sh002i 424 # perhaps a coderef? in any case, i don't feel like dealing with it!
805 :     $result .= '<tr valign="top">';
806 :     $result .= "<td>$ref</td>";
807 :     $result .= "</tr>";
808 : sh002i 422 }
809 : sh002i 424 $result .= "</table>"
810 : sh002i 422 } else {
811 : sh002i 424 $result .= defined $ref ? $ref : '<font color="red">undef</font>';
812 :     }
813 : sh002i 422 }
814 : gage 3326 our $BASE64_ENCODED = 'base64_encoded:';
815 :     # use constant BASE64_ENCODED = 'base64_encoded;
816 :     # was not evaluated in the matching and substitution
817 :     # statements
818 : gage 6367
819 : gage 6366 # sub decodeAnswers($) {
820 :     # my $string = shift;
821 :     # return unless defined $string and $string;
822 :     #
823 :     # if ($string =~/^$BASE64_ENCODED/o) {
824 :     # $string =~ s/^$BASE64_ENCODED//o;
825 :     # $string = decode_base64($string);
826 :     # }
827 :     #
828 :     # my @array = split m/##/, $string;
829 :     # $array[$_] =~ s/\\#\\/#/g foreach 0 .. $#array;
830 :     # push @array, "" if @array%2;
831 :     # return @array; # it's actually a hash ;)
832 :     # }
833 : gage 6371
834 : sh002i 2769 sub decodeAnswers($) {
835 : gage 6366 my $serialized = shift;
836 :     return unless defined $serialized and $serialized;
837 :     my $array_ref = eval{ Storable::thaw($serialized) };
838 :     if ($@) {
839 : gage 6667 warn "problem fetching answers -- possibly left over from base64 days. Not to worry -- press preview or submit and this will go away permanently for this question. $@";
840 : gage 6366 return ();
841 :     } else {
842 :     return @{$array_ref};
843 : gage 3232 }
844 : sh002i 422 }
845 :    
846 : sh002i 2769 sub encodeAnswers(\%\@) {
847 : gage 6366 my %hash = %{shift()};
848 :     my @order = @{shift()};
849 :     my @ordered_hash = ();
850 :     foreach my $key (@order) {
851 :     push @ordered_hash, $key, $hash{$key};
852 : sh002i 2769 }
853 : gage 6366 return Storable::freeze( \@ordered_hash);
854 : gage 3232
855 : sh002i 1111 }
856 :    
857 : gage 6366 # sub encodeAnswers(\%\@) {
858 :     # my %hash = %{ shift() };
859 :     # my @order = @{ shift() };
860 :     # my $string = "";
861 :     # foreach my $name (@order) {
862 :     # my $value = defined $hash{$name} ? $hash{$name} : "";
863 :     # $name =~ s/#/\\#\\/g; # this is a WEIRD way to escape things
864 :     # $value =~ s/#/\\#\\/g; # and it's not my fault!
865 :     # if ($value =~ m/\\$/) {
866 :     # # if the value ends with a backslash, string2hash will
867 :     # # interpret that as a normal escape sequence (not part
868 :     # # of the weird pound escape sequence) if the next
869 :     # # character is &. So we have to protect against this.
870 :     # # will adding a spcae at the end of the last answer
871 :     # # hurt anything? i don't think so...
872 :     # $value .= " ";
873 :     # }
874 :     # $string .= "$name##$value##"; # this is also not my fault
875 :     # }
876 :     # $string =~ s/##$//; # remove last pair of hashs
877 :     #
878 :     # $string = $BASE64_ENCODED.encode_base64($string, "");
879 :     # # Empty string in second argument prevents end-of-line characters from being used.
880 :     # # This is nice for examining database contents manually since it prevents newlines
881 :     # # from being introduced into database records.
882 :     #
883 :     # return $string;
884 :     # }
885 :    
886 : sh002i 2769 sub max(@) {
887 :     my $soFar;
888 :     foreach my $item (@_) {
889 :     $soFar = $item unless defined $soFar;
890 :     if ($item > $soFar) {
891 :     $soFar = $item;
892 :     }
893 :     }
894 :     return defined $soFar ? $soFar : 0;
895 : sh002i 1145 }
896 :    
897 : sh002i 2769 sub pretty_print_rh($) {
898 : gage 1137 my $rh = shift;
899 :     foreach my $key (sort keys %{$rh}) {
900 :     warn " $key => ",$rh->{$key},"\n";
901 :     }
902 :     }
903 : sh002i 1145
904 : sh002i 2769 sub cryptPassword($) {
905 : malsyned 1287 my ($clearPassword) = @_;
906 :     my $salt = join("", ('.','/','0'..'9','A'..'Z','a'..'z')[rand 64, rand 64]);
907 :     my $cryptPassword = crypt($clearPassword, $salt);
908 :     return $cryptPassword;
909 :     }
910 :    
911 : sh002i 1900 # from the Perl Cookbook, first edition, page 25:
912 :     sub dequote($) {
913 :     local $_ = shift;
914 :     my ($white, $leader); # common whitespace and common leading string
915 :     if (/^\s*(?:([^\w\s]+)(\s*).*\n)(?:\s*\1\2?.*\n)+$/) {
916 :     ($white, $leader) = ($2, quotemeta($1));
917 :     } else {
918 :     ($white, $leader) = (/^(\s+)/, '');
919 :     }
920 :     s/^\s*?$leader(?:$white)?//gm;
921 :     return $_;
922 :     }
923 :    
924 : sh002i 1945 sub undefstr($@) {
925 :     map { defined $_ ? $_ : $_[0] } @_[1..$#_];
926 :     }
927 :    
928 : gage 6082
929 : sh002i 3029 # shuffle an array in place
930 :     # Perl Cookbook, Recipe 4.17. Randomizing an Array
931 :     sub fisher_yates_shuffle {
932 :     my $array = shift;
933 :     my $i;
934 :     for ($i = @$array; --$i; ) {
935 :     my $j = int rand ($i+1);
936 :     next if $i == $j;
937 :     @$array[$i,$j] = @$array[$j,$i];
938 :     }
939 :     }
940 :    
941 : sh002i 4697 sub constituency_hash {
942 :     my $hash = {};
943 :     @$hash{@_} = ();
944 :     return $hash;
945 :     }
946 :    
947 : sh002i 2769 ################################################################################
948 :     # Sorting
949 :     ################################################################################
950 :    
951 :     # p. 101, Camel, 3rd ed.
952 :     # The <=> and cmp operators return -1 if the left operand is less than the
953 :     # right operand, 0 if they are equal, and +1 if the left operand is greater
954 :     # than the right operand.
955 : glarose 4836 #
956 :     # FIXME: I've added the ability to do multiple field sorts, below; I'm
957 :     # leaving this code, commented out, in case there's a good reason to
958 :     # revert to this and do multiple field sorts differently. -glr 2007/03/05
959 :     # sub sortByName($@) {
960 :     # my ($field, @items) = @_;
961 :     # return sort {
962 :     # my @aParts = split m/(?<=\D)(?=\d)|(?<=\d)(?=\D)/, defined $field ? $a->$field : $a;
963 :     # my @bParts = split m/(?<=\D)(?=\d)|(?<=\d)(?=\D)/, defined $field ? $b->$field : $b;
964 :     # while (@aParts and @bParts) {
965 :     # my $aPart = shift @aParts;
966 :     # my $bPart = shift @bParts;
967 :     # my $aNumeric = $aPart =~ m/^\d*$/;
968 :     # my $bNumeric = $bPart =~ m/^\d*$/;
969 :    
970 :     # # numbers should come before words
971 :     # return -1 if $aNumeric and not $bNumeric;
972 :     # return +1 if not $aNumeric and $bNumeric;
973 :    
974 :     # # both have the same type
975 :     # if ($aNumeric and $bNumeric) {
976 :     # next if $aPart == $bPart; # check next pair
977 :     # return $aPart <=> $bPart; # compare numerically
978 :     # } else {
979 :     # next if $aPart eq $bPart; # check next pair
980 :     # return $aPart cmp $bPart; # compare lexicographically
981 :     # }
982 :     # }
983 :     # return +1 if @aParts; # a has more sections, should go second
984 :     # return -1 if @bParts; # a had fewer sections, should go first
985 :     # } @items;
986 :     # }
987 :    
988 : sh002i 2769 sub sortByName($@) {
989 :     my ($field, @items) = @_;
990 : glarose 4836
991 :     my %itemsByIndex = ();
992 :     if ( ref( $field ) eq 'ARRAY' ) {
993 :     foreach my $item ( @items ) {
994 :     my $key = '';
995 :     foreach ( @$field ) {
996 :     $key .= $item->$_; # in this case we assume
997 :     } # all entries in @$field
998 :     $itemsByIndex{$key} = $item; # are defined.
999 :     }
1000 :     } else {
1001 :     %itemsByIndex = map {(defined $field)?$_->$field:$_ => $_} @items;
1002 :     }
1003 :    
1004 :     my @sKeys = sort {
1005 :     my @aParts = split m/(?<=\D)(?=\d)|(?<=\d)(?=\D)/, $a;
1006 :     my @bParts = split m/(?<=\D)(?=\d)|(?<=\d)(?=\D)/, $b;
1007 :    
1008 : sh002i 2769 while (@aParts and @bParts) {
1009 :     my $aPart = shift @aParts;
1010 :     my $bPart = shift @bParts;
1011 :     my $aNumeric = $aPart =~ m/^\d*$/;
1012 :     my $bNumeric = $bPart =~ m/^\d*$/;
1013 :    
1014 :     # numbers should come before words
1015 :     return -1 if $aNumeric and not $bNumeric;
1016 :     return +1 if not $aNumeric and $bNumeric;
1017 :    
1018 :     # both have the same type
1019 :     if ($aNumeric and $bNumeric) {
1020 :     next if $aPart == $bPart; # check next pair
1021 :     return $aPart <=> $bPart; # compare numerically
1022 :     } else {
1023 :     next if $aPart eq $bPart; # check next pair
1024 :     return $aPart cmp $bPart; # compare lexicographically
1025 :     }
1026 :     }
1027 :     return +1 if @aParts; # a has more sections, should go second
1028 :     return -1 if @bParts; # a had fewer sections, should go first
1029 : glarose 4836 } (keys %itemsByIndex);
1030 :    
1031 :     return map{$itemsByIndex{$_}} @sKeys;
1032 : sh002i 2769 }
1033 :    
1034 : gage 6082 ################################################################################
1035 :     # Validate strings and labels
1036 :     ################################################################################
1037 : glarose 4836
1038 : gage 6082 sub not_blank ($) { # check that a string exists and is not blank
1039 :     my $str = shift;
1040 :     return( defined($str) and $str =~/\S/ );
1041 :     }
1042 : glarose 4836
1043 : gage 6082 ###########################################################
1044 :     # If things have worked so far determine if the file might be accompanied by auxiliary files
1045 :    
1046 :     #
1047 :     sub has_aux_files ($) { # determine whether a question has auxiliary files
1048 :     # a path ending in foo/foo.pg is assumed to contain auxilliary files
1049 :     my $path = shift;
1050 :     if ( not_blank($path) ) {
1051 :     my ($dir, $prob) = $path =~ m|([^/]+)/([^/]+)\.pg$|; # must be a problem file ending in .pg
1052 :     return 1 if (defined($dir) and defined ($prob) and $dir eq $prob);
1053 :     } else {
1054 :     warn "This subroutine cannot handle empty paths: |$path|",caller();
1055 :     }
1056 :     return 0; # no aux files with this .pg file
1057 :    
1058 :     }
1059 :    
1060 : sh002i 422 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9