################################################################################
# WeBWorK Online Homework Delivery System
# Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
# $CVSHeader: webwork2/lib/WeBWorK/Utils.pm,v 1.48 2004/09/10 02:32:09 sh002i Exp $
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of either: (a) the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version, or (b) the "Artistic License" which comes with this package.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
# Artistic License for more details.
################################################################################
package WeBWorK::Utils;
use base qw(Exporter);
=head1 NAME
WeBWorK::Utils - useful utilities used by other WeBWorK modules.
=cut
use strict;
use warnings;
#use Apache::DB;
use DateTime;
use Errno;
use File::Path qw(rmtree);
use Carp;
use constant MKDIR_ATTEMPTS => 10;
# "standard" WeBWorK date/time format (for set definition files):
# %m/%d/%y at %I:%M%P
# where:
# %m = month number, starting with 01
# %d = numeric day of the month, with leading zeros (eg 01..31)
# %y = year (2 digits)
# %I = hour, 12 hour clock, leading 0's)
# %M = minute, leading 0's
# %P = am or pm (Yes %p and %P are backwards :)
use constant DATE_FORMAT => "%m/%d/%y at %I:%M%P";
our @EXPORT = ();
our @EXPORT_OK = qw(
runtime_use
readFile
readDirectory
listFilesRecursive
surePathToFile
makeTempDirectory
removeTempDirectory
formatDateTime
parseDateTime
textDateTime
intDateTime
writeLog
writeCourseLog
writeTimingLogEntry
list2hash
ref2string
decodeAnswers
encodeAnswers
max
pretty_print_rh
cryptPassword
dequote
undefstr
sortByName
);
################################################################################
# Lowlevel thingies
################################################################################
sub runtime_use($) {
croak "runtime_use: no module specified" unless $_[0];
eval "package Main; require $_[0]; import $_[0]";
die $@ if $@;
}
#sub backtrace($) {
# my ($style) = @_;
# $style = "warn" unless $style;
# my @bt = DB->backtrace;
# shift @bt; # Remove "backtrace" from the backtrace;
# if ($style eq "die") {
# die join "\n", @bt;
# } elsif ($style eq "warn") {
# warn join "\n", @bt;
# } elsif ($style eq "print") {
# print join "\n", @bt;
# } elsif ($style eq "return") {
# return @bt;
# }
#}
################################################################################
# Filesystem interaction
################################################################################
# Convert Windows and Mac (classic) line endings to UNIX line endings in a string.
# Windows uses CRLF, Mac uses CR, UNIX uses LF. (CR is ASCII 15, LF if ASCII 12)
sub force_eoln($) {
my ($string) = @_;
$string =~ s/\015\012?/\012/g;
return $string;
}
sub readFile($) {
my $fileName = shift;
local $/ = undef; # slurp the whole thing into one string
open my $dh, "<", $fileName
or die "failed to read file $fileName: $!";
my $result = <$dh>;
close $dh;
return force_eoln($result);
}
sub readDirectory($) {
my $dirName = shift;
opendir my $dh, $dirName
or die "Failed to read directory $dirName: $!";
my @result = readdir $dh;
close $dh;
return @result;
}
=item @matches = listFilesRecusive($dir, $match_qr, $prune_qr, $match_full, $prune_full)
Traverses the directory tree rooted at $dir, returning a list of files, named
pipes, and sockets matching the regular expression $match_qr. Directories
matching the regular expression $prune_qr are not visited.
$match_full and $prune_full are boolean values that indicate whether $match_qr
and $prune_qr, respectively, should be applied to the bare directory entry
(false) or to the path to the directory entry relative to $dir.
@matches is a list of paths relative to $dir.
=cut
sub listFilesRecursiveHelper($$$$$$);
sub listFilesRecursive($;$$$$) {
my ($dir, $match_qr, $prune_qr, $match_full, $prune_full) = @_;
return listFilesRecursiveHelper($dir, "", $match_qr, $prune_qr, $match_full, $prune_full);
}
sub listFilesRecursiveHelper($$$$$$) {
my ($base_dir, $curr_dir, $match_qr, $prune_qr, $match_full, $prune_full) = @_;
my $full_dir = "$base_dir/$curr_dir";
my @dir_contents = readDirectory($full_dir);
my @matches;
foreach my $dir_entry (@dir_contents) {
my $full_path = "$full_dir/$dir_entry";
if (-d $full_path or -l $full_path) {
# standard things to skip
next if $dir_entry eq ".";
next if $dir_entry eq "..";
# skip unreadable directories (and broken symlinks, incidentally)
unless (-r $full_path) {
warn "Directory/symlink $full_path not readable";
next;
}
# check $prune_qr
my $subdir = ($curr_dir eq "") ? $dir_entry : "$curr_dir/$dir_entry";
if (defined $prune_qr) {
my $prune_string = $prune_full ? $subdir : $dir_entry;
next if $prune_string =~ m/$prune_qr/;
}
# everything looks good, time to recurse!
push @matches, listFilesRecursiveHelper($base_dir, $subdir, $match_qr, $prune_qr, $match_full, $prune_full);
} elsif (-f $full_path or -p $full_path or -S $full_path) {
my $file = ($curr_dir eq "") ? $dir_entry : "$curr_dir/$dir_entry";
my $match_string = $match_full ? $file : $dir_entry;
if (not defined $match_string or $dir_entry =~ m/$match_qr/) {
push @matches, $file;
}
}
}
return @matches;
}
# A very useful macro for making sure that all of the directories to a file have
# been constructed.
sub surePathToFile($$) {
# constructs intermediate
# the input path must be the path relative to this starting directory
my $start_directory = shift;
my $path = shift;
my $delim = "/"; #&getDirDelim();
unless ($start_directory and $path ) {
warn "missing directory
surePathToFile start_directory path ";
return '';
}
# use the permissions/group on the start directory itself as a template
my ($perms, $groupID) = (stat $start_directory)[2,5];
#warn "&urePathToTmpFile: perms=$perms groupID=$groupID\n";
# if the path starts with $start_directory (which is permitted but optional) remove this initial segment
$path =~ s|^$start_directory|| if $path =~ m|^$start_directory|;
#$path = convertPath($path);
# find the nodes on the given path
my @nodes = split("$delim",$path);
# create new path
$path = $start_directory; #convertPath("$tmpDirectory");
while (@nodes>1) {
$path = $path . shift (@nodes) . "/"; #convertPath($path . shift (@nodes) . "/");
#FIXME this make directory command may not be fool proof.
unless (-e $path) {
mkdir($path, $perms)
or warn "Failed to create directory $path";
}
}
$path = $path . shift(@nodes); #convertPath($path . shift(@nodes));
return $path;
}
sub makeTempDirectory($$) {
my ($parent, $basename) = @_;
# Loop until we're able to create a directory, or it fails for some
# reason other than there already being something there.
my $triesRemaining = MKDIR_ATTEMPTS;
my ($fullPath, $success);
do {
my $suffix = join "", map { ('A'..'Z','a'..'z','0'..'9')[int rand 62] } 1 .. 8;
$fullPath = "$parent/$basename.$suffix";
$success = mkdir $fullPath;
} until ($success or not $!{EEXIST});
die "Failed to create directory $fullPath: $!"
unless $success;
return $fullPath;
}
sub removeTempDirectory($) {
my ($dir) = @_;
rmtree($dir, 0, 0);
}
################################################################################
# Date/time processing
################################################################################
=head2 Date/time processing
=over
=item $dateTime = parseDateTime($string, $display_tz)
Parses $string as a datetime. If $display_tz is given, $string is assumed to be
in that timezone. Otherwise, the server's timezone is used. The result,
$dateTime, is an integer UNIX datetime (epoch) in the server's timezone.
=cut
sub parseDateTime($;$) {
my ($string, $display_tz) = @_;
$display_tz ||= "local";
warn "parseDateTime('$string', '$display_tz')\n";
# Method #1: using Date::Parse
use Date::Parse;
$string =~ s/\s*\bat\b\s*/ /; # Date::Parse can't handle the "at" in WeBWorK datetimes.
my $epoch = str2time($string);
#warn "\tMethod #1: str2time($string) = $epoch\n";
my $dt = DateTime->from_epoch(epoch => $epoch, time_zone => "local");
#warn "\tMethod #1: \$dt = ", $dt->strftime(DATE_FORMAT." %Z"), "\n";
# Method #2: using Date::Manip
#use Date::Manip;
#my $dm = ParseDateString($string);
#warn "\tMethod #2: ParseDateString($string) = $dm\n";
#use DateTime::Format::DateManip;
#my $dt = DateTime::Format::DateManip->parse_datetime($dm);
#warn "\tdMethod #2: \$dt = ", $dt->strftime(DATE_FORMAT." %Z"), "\n";
my $dt2 = $dt->clone->set_time_zone("floating")->set_time_zone($display_tz);
#warn "\t\$dt2 = ", $dt2->strftime(DATE_FORMAT." %Z"), "\n";
my $epoch2 = $dt2->epoch;
#warn "\t\$epoch2 (return value) = $epoch2\n";
return $epoch2;
}
=item $string = formatDateTime($dateTime, $display_tz)
Formats the UNIX datetime $dateTime in the standard WeBWorK datetime format.
$dateTime is assumed to be in the server's time zone. If $display_tz is given,
the datetime is converted from the server's timezone to the timezone specified.
=cut
sub formatDateTime($;$) {
my ($dateTime, $display_tz) = @_;
$display_tz ||= "local";
#warn "formatDateTime('$dateTime', '$display_tz')\n";
my $dt = DateTime->from_epoch(epoch => $dateTime, time_zone => $display_tz);
#warn "\t\$dt = ", $dt->strftime(DATE_FORMAT." %Z"), "\n";
return $dt->strftime(DATE_FORMAT);
}
=item $string = textDateTime($string_or_dateTime)
Accepts a UNIX datetime or a formatted string, returns a formatted string.
=cut
sub textDateTime($) {
return ($_[0] =~ m/^\d*$/) ? formatDateTime($_[0]) : $_[0];
}
=item $dateTIme = intDateTime($string_or_dateTime)
Accepts a UNIX datetime or a formatted string, returns a UNIX datetime.
=cut
sub intDateTime($) {
return ($_[0] =~ m/^\d*$/) ? $_[0] : parseDateTime($_[0]);
}
=back
=cut
################################################################################
# Logging
################################################################################
sub writeLog($$@) {
my ($ce, $facility, @message) = @_;
unless ($ce->{webworkFiles}->{logs}->{$facility}) {
warn "There is no log file for the $facility facility defined.\n";
return;
}
my $logFile = $ce->{webworkFiles}->{logs}->{$facility};
local *LOG;
if (open LOG, ">>", $logFile) {
print LOG "[", time2str("%a %b %d %H:%M:%S %Y", time), "] @message\n";
close LOG;
} else {
warn "failed to open $logFile for writing: $!";
}
}
sub writeCourseLog($$@) {
my ($ce, $facility, @message) = @_;
unless ($ce->{courseFiles}->{logs}->{$facility}) {
warn "There is no course log file for the $facility facility defined.\n";
return;
}
my $logFile = $ce->{courseFiles}->{logs}->{$facility};
local *LOG;
if (open LOG, ">>", $logFile) {
print LOG "[", time2str("%a %b %d %H:%M:%S %Y", time), "] @message\n";
close LOG;
} else {
warn "failed to open $logFile for writing: $!";
}
}
# $ce - a WeBWork::CourseEnvironment object
# $function - fully qualified function name
# $details - any information, do not use the characters '[' or ']'
# $beginEnd - the string "begin", "intermediate", or "end"
# use the intermediate step begun or completed for INTERMEDIATE
# use an empty string for $details when calling for END
sub writeTimingLogEntry($$$$) {
my ($ce, $function, $details, $beginEnd) = @_;
return unless defined $ce->{webworkFiles}->{logs}->{timing};
$beginEnd = ($beginEnd eq "begin") ? ">" : ($beginEnd eq "end") ? "<" : "-";
writeLog($ce, "timing", "$$ ".time." $beginEnd $function [$details]");
}
################################################################################
# Data munging
################################################################################
sub list2hash(@) {
map {$_ => "0"} @_;
}
sub refBaseType($) {
my $ref = shift;
$ref =~ m/(\w+)\(/; # this might not be robust...
return $1;
}
sub ref2string($;$);
sub ref2string($;$) {
my $ref = shift;
my $dontExpand = shift || {};
my $refType = ref $ref;
my $result;
if ($refType and not $dontExpand->{$refType}) {
my $baseType = refBaseType($ref);
$result .= '' . $refType;
$result .= " ($baseType)" if $baseType and $refType ne $baseType;
$result .= ":
";
$result .= '
| $_ | "; $result .= "" . ref2string($hash{$_}, $dontExpand) . " | "; $result .= "|
| $_ | "; $result .= "".$FIELDS[$_]." | " if @FIELDS; $result .= "" . ref2string($array[$_], $dontExpand) . " | "; $result .= "
| $scalar | "; $result .= "||
| $ref | "; $result .= "