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