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