package WeBWorK::PG::Translator; use strict; use warnings; use Opcode; use Safe; use Net::SMTP; use WeBWorK::Utils qw(runtime_use); use WeBWorK::PG::IO; # loading GD within the Safe compartment has occasionally caused infinite recursion # Putting these use statements here seems to avoid this problem # It is not clear that this is essential once things are working properly. use Exporter; use DynaLoader; #use GD; =head1 NAME WeBWorK::PG::Translator - Evaluate PG code and evaluate answers safely =head1 SYNPOSIS my $pt = new PGtranslator; # create a translator; $pt->environment(\%envir); # provide the environment variable for the problem $pt->initialize(); # initialize the translator $pt-> set_mask(); # set the operation mask for the translator safe compartment $pt->source_string($source); # provide the source string for the problem $pt -> unrestricted_load("${courseScriptsDirectory}PG.pl"); $pt -> unrestricted_load("${courseScriptsDirectory}dangerousMacros.pl"); # load the unprotected macro files # these files are evaluated with the Safe compartment wide open # other macros are loaded from within the problem using loadMacros $pt ->translate(); # translate the problem (the out following 4 pieces of information are created) $PG_PROBLEM_TEXT_ARRAY_REF = $pt->ra_text(); # output text for the body of the HTML file (in array form) $PG_PROBLEM_TEXT_REF = $pt->r_text(); # output text for the body of the HTML file $PG_HEADER_TEXT_REF = $pt->r_header;#\$PG_HEADER_TEXT; # text for the header of the HTML file $PG_ANSWER_HASH_REF = $pt->rh_correct_answers; # a hash of answer evaluators $PG_FLAGS_REF = $pt ->rh_flags; # misc. status flags. $pt -> process_answers(\%inputs); # evaluates all of the answers using submitted answers from %input my $rh_answer_results = $pt->rh_evaluated_answers; # provides a hash of the results of evaluating the answers. my $rh_problem_result = $pt->grade_problem; # grades the problem using the default problem grading method. =head1 DESCRIPTION This module defines an object which will translate a problem written in the Problem Generating (PG) language =cut =head2 be_strict This creates a substitute for C which cannot be used in PG problem sets or PG macro files. Use this way to imitate the behavior of C BEGIN { be_strict(); # an alias for use strict. # This means that all global variable # must contain main:: as a prefix. } =cut BEGIN { sub be_strict { # allows the use of strict within macro packages. require 'strict.pm'; strict::import(); } } =head2 evaluate_modules Usage: $obj -> evaluate_modules('WWPlot', 'Fun', 'Circle'); $obj -> evaluate_modules('reset'); Adds the modules WWPlot.pm, Fun.pm and Circle.pm in the courseScripts directory to the list of modules which can be used by the PG problems. The keyword 'reset' or 'erase' erases the list of modules already loaded =cut #my @class_modules = (); #sub evaluate_modules{ # my $self = shift; # my @modules = @_; # # temporary - # # We need a method for setting the course directory without calling Global. # # my $courseScriptsDirectory = $self->rh_directories->{courseScriptsDirectory}; # my $save_SIG_die_trap = $SIG{__DIE__}; # $SIG{__DIE__} = sub {CORE::die(@_) }; # while (@modules) { # my $module_name = shift @modules; # print STDERR "evaluate_modules: about to evaluate $module_name\n"; # $module_name =~ s/\.pm$//; # remove trailing .pm if someone forgot # if ($module_name eq 'reset' or $module_name eq 'erase' ) { # @class_modules = (); # next; # } # if ( -r "${courseScriptsDirectory}${module_name}.pm" ) { # eval(qq! require "${courseScriptsDirectory}${module_name}.pm"; import ${module_name};! ); # warn "Errors in including the module ${courseScriptsDirectory}$module_name.pm $@" if $@; # } else { # eval(qq! require "${module_name}.pm"; import ${module_name};! ); # warn "Errors in including either the module $module_name.pm or ${courseScriptsDirectory}${module_name}.pm $@" if $@; # } # push(@class_modules, "\%${module_name}::"); # print STDERR "loading $module_name\n"; # } # $SIG{__DIE__} = $save_SIG_die_trap; #} sub evaluate_modules { my $self = shift; local $SIG{__DIE__} = "DEFAULT"; # we're going to be eval()ing code foreach (@_) { # ensure that the name is in fact a base name s/\.pm$// and warn "fixing your broken package name: $_.pm => $_"; # generate a full package name from the base name unless (/::/) { $_ = "WeBWorK::PG::$_"; } # call runtime_use on the package name # don't worry -- runtime_use won't load a package twice! runtime_use $_; warn "Failed to evaluate module $_: $@"; # record this in the appropriate place push @{$self->{ra_included_modules}}, "\%${_}::"; } } =head2 load_extra_packages Usage: $obj -> load_extra_packages('AlgParserWithImplicitExpand', 'Expr','ExprWithImplicitExpand'); Loads extra packages for modules that contain more than one package. Works in conjunction with evaluate_modules. It is assumed that the file containing the extra packages (along with the base pachage name which is the same as the name of the file minus the .pm extension) has already been loaded using evaluate_modules =cut sub load_extra_packages{ my $self = shift; my @package_list = @_; my $package_name; foreach $package_name (@package_list) { # ensure that the name is in fact a base name s/\.pm$// and warn "fixing your broken package name: $_.pm => $_"; # generate a full package name from the base name unless (/::/) { $_ = "WeBWorK::PG::$_"; } # call runtime_use on the package name # don't worry -- runtime_use won't load a package twice! import $_; warn "Failed to evaluate module $_: $@" if $@; # record this in the appropriate place push @{$self->{ra_included_modules}}, "\%${_}::"; } } =head2 new Creates the translator object. =cut sub new { my $class = shift; my $safe_cmpt = new Safe; #('PG_priv'); my $self = { envir => undef, PG_PROBLEM_TEXT_ARRAY_REF => [], PG_PROBLEM_TEXT_REF => 0, PG_HEADER_TEXT_REF => 0, PG_ANSWER_HASH_REF => {}, PG_FLAGS_REF => {}, safe => $safe_cmpt, safe_compartment_name => $safe_cmpt->root, errors => "", source => "", rh_correct_answers => {}, rh_student_answers => {}, rh_evaluated_answers => {}, rh_problem_result => {}, rh_problem_state => { recorded_score => 0, # the score recorded in the data base num_of_correct_ans => 0, # the number of correct attempts at doing the problem num_of_incorrect_ans => 0, # the number of incorrect attempts }, rf_problem_grader => \&std_problem_grader, rf_safety_filter => \&safetyFilter, # ra_included_modules is now populated independantly of @class_modules: ra_included_modules => [], # [ @class_modules ], rh_directories => {}, }; bless $self, $class; } =pod (b) The following routines defined within the PG module are shared: &be_strict &read_whole_problem_file &convertPath &surePathToTmpFile &fileFromPath &directoryFromPath &createFile &includePGtext &PG_answer_eval &PG_restricted_eval &send_mail_to &PGsort In addition the environment hash C<%envir> is shared. This variable is unpacked when PG.pl is run and provides most of the environment variables for each problem template. =for html environment variables =cut =pod (c) Sharing macros: The macros shared with the safe compartment are '&read_whole_problem_file' '&convertPath' '&surePathToTmpFile' '&fileFromPath' '&directoryFromPath' '&createFile' '&PG_answer_eval' '&PG_restricted_eval' '&be_strict' '&send_mail_to' '&PGsort' '&dumpvar' '&includePGtext' =cut # SHARE variables and routines with safe compartment my %shared_subroutine_hash = ( '&read_whole_problem_file' => 'PGtranslator', #the values are dummies. '&convertPath' => 'PGtranslator', '&surePathToTmpFile' => 'PGtranslator', '&fileFromPath' => 'PGtranslator', '&directoryFromPath' => 'PGtranslator', '&createFile' => 'PGtranslator', '&PG_answer_eval' => 'PGtranslator', '&PG_restricted_eval' => 'PGtranslator', '&be_strict' => 'PGtranslator', '&send_mail_to' => 'PGtranslator', '&PGsort' => 'PGtranslator', '&dumpvar' => 'PGtranslator', '&includePGtext' => 'PGtranslator', ); sub initialize { my $self = shift; my $safe_cmpt = $self->{safe}; #print "initializing safeCompartment",$safe_cmpt -> root(), "\n"; $safe_cmpt -> share(keys %shared_subroutine_hash); # begin experiment? no strict; local %envir = %{ $self->{envir} }; $safe_cmpt -> share('%envir'); #local($rf_answer_eval) = sub { $self->PG_answer_eval(@_); }; #local($rf_restricted_eval) = sub { $self->PG_restricted_eval(@_); }; #$safe_cmpt -> share('$rf_answer_eval'); #$safe_cmpt -> share('$rf_restricted_eval'); use strict; # end experiment # ra_included_modules is now populated independantly of @class_modules: #$self->{ra_included_modules} = [@class_modules]; $safe_cmpt -> share_from('WeBWorK::PG', $self->{ra_included_modules} ); } sub environment{ my $self = shift; my $envirref = shift; if ( defined($envirref) ) { if (ref($envirref) eq 'HASH') { %{ $self -> {envir} } = %$envirref; } else { $self ->{errors} .= "ERROR: The environment method for PG_translate objects requires a reference to a hash"; } } $self->{envir} ; #reference to current environment } =head2 Safe compartment pass through macros =cut sub mask { my $self = shift; my $mask = shift; my $safe_compartment = $self->{safe}; $safe_compartment->mask($mask); } sub permit { my $self = shift; my @array = shift; my $safe_compartment = $self->{safe}; $safe_compartment->permit(@array); } sub deny { my $self = shift; my @array = shift; my $safe_compartment = $self->{safe}; $safe_compartment->deny(@array); } sub share_from { my $self = shift; my $pckg_name = shift; my $array_ref =shift; my $safe_compartment = $self->{safe}; $safe_compartment->share_from($pckg_name,$array_ref); } sub source_string { my $self = shift; my $temp = shift; my $out; if ( ref($temp) eq 'SCALAR') { $self->{source} = $$temp; $out = $self->{source}; } elsif ($temp) { $self->{source} = $temp; $out = $self->{source}; } $self -> {source}; } sub source_file { my $self = shift; my $filePath = shift; local(*SOURCEFILE); local($/); $/ = undef; # allows us to treat the file as a single line my $err = ""; if ( open(SOURCEFILE, "<$filePath") ) { $self -> {source} = ; close(SOURCEFILE); } else { $self->{errors} .= "Can't open file: $filePath"; croak( "Can't open file: $filePath\n" ); } $err; } sub unrestricted_load { my $self = shift; my $filePath = shift; my $safe_cmpt = $self ->{safe}; my $store_mask = $safe_cmpt->mask(); $safe_cmpt->mask(Opcode::empty_opset()); my $safe_cmpt_package_name = $safe_cmpt->root(); my $macro_file_name = fileFromPath($filePath); $macro_file_name =~s/\.pl//; # trim off the extenstion my $export_subroutine_name = "_${macro_file_name}_export"; my $init_subroutine_name = "_${macro_file_name}_init"; my $macro_file_loaded; my $local_errors = ""; no strict; $macro_file_loaded = defined(&{"${safe_cmpt_package_name}::$init_subroutine_name"} ); print STDERR "$macro_file_name has not yet been loaded\n" unless $macro_file_loaded; unless ($macro_file_loaded) { # print "loading $filePath\n"; ## load the $filePath file ## Using rdo insures that the $filePath file is loaded for every problem, allowing initializations to occur. ## Ordinary mortals should not be fooling with the fundamental macros in these files. my $local_errors = ""; if (-r $filePath ) { $safe_cmpt -> rdo( "$filePath" ) ; #warn "There were problems compiling the file: $filePath:
--$@" if $@; $local_errors ="\nThere were problems compiling the file:\n $filePath\n $@\n" if $@; $self ->{errors} .= $local_errors if $local_errors; use strict; } else { $local_errors = "Can't open file $filePath for reading\n"; $self ->{errors} .= $local_errors if $local_errors; } $safe_cmpt -> mask($store_mask); } $macro_file_loaded = defined(&{"${safe_cmpt_package_name}::$init_subroutine_name"} ); $local_errors .= "\nUnknown error. Unable to load $filePath\n" if ($local_errors eq '' and not $macro_file_loaded); print STDERR "$filePath is properly loaded\n\n" if $macro_file_loaded; $local_errors; } sub nameSpace { my $self = shift; $self->{safe}->root; } sub a_text { my $self = shift; @{$self->{PG_PROBLEM_TEXT_ARRAY_REF}}; } sub header { my $self = shift; ${$self->{PG_HEADER_TEXT_REF}}; } sub h_flags { my $self = shift; %{$self->{PG_FLAGS_REF}}; } sub rh_flags { my $self = shift; $self->{PG_FLAGS_REF}; } sub h_answers{ my $self = shift; %{$self->{PG_ANSWER_HASH_REF}}; } sub ra_text { my $self = shift; $self->{PG_PROBLEM_TEXT_ARRAY_REF}; } sub r_text { my $self = shift; $self->{PG_PROBLEM_TEXT_REF}; } sub r_header { my $self = shift; $self->{PG_HEADER_TEXT_REF}; } sub rh_directories { my $self = shift; my $rh_directories = shift; $self->{rh_directories}=$rh_directories if ref($rh_directories) eq 'HASH'; $self->{rh_directories}; } sub rh_correct_answers { my $self = shift; my @in = @_; return $self->{rh_correct_answers} if @in == 0; if ( ref($in[0]) eq 'HASH' ) { $self->{rh_correct_answers} = { %{ $in[0] } }; # store a copy of the hash } else { $self->{rh_correct_answers} = { @in }; # store a copy of the hash } $self->{rh_correct_answers} } sub rf_problem_grader { my $self = shift; my $in = shift; return $self->{rf_problem_grader} unless defined($in); if (ref($in) =~/CODE/ ) { $self->{rf_problem_grader} = $in; } else { die "ERROR: Attempted to install a problem grader which was not a reference to a subroutine."; } $self->{rf_problem_grader} } sub errors{ my $self = shift; $self->{errors}; } # sub DESTROY { # my $self = shift; # my $nameSpace = $self->nameSpace; # no strict 'refs'; # my $nm = "${nameSpace}::"; # my $nsp = \%{"$nm"}; # my @list = keys %$nsp; # while (@list) { # my $name = pop(@list); # if ( defined(&{$nsp->{$name}}) ) { # #print "checking \&$name\n"; # unless (exists( $shared_subroutine_hash{"\&$name"} ) ) { # undef( &{$nsp->{$name}} ); # #print "destroying \&$name\n"; # } else { # #delete( $nsp->{$name} ); # #print "what is left",join(" ",%$nsp) ,"\n\n"; # } # # } # if ( defined(${$nsp->{$name}}) ) { # #undef( ${$nsp->{$name}} ); ## unless commented out download hardcopy bombs with Perl 5.6 # #print "destroying \$$name\n"; # } # if ( defined(@{$nsp->{$name}}) ) { # undef( @{$nsp->{$name}} ); # #print "destroying \@$name\n"; # } # if ( defined(%{$nsp->{$name}}) ) { # undef( %{$nsp->{$name}} ) unless $name =~ /::/ ; # #print "destroying \%$name\n"; # } # # changed for Perl 5.6 # delete ( $nsp->{$name} ) if defined($nsp->{$name}); # this must be uncommented in Perl 5.6 to reinitialize variables # # changed for Perl 5.6 # #print "deleting $name\n"; # #undef( @{$nsp->{$name}} ) if defined(@{$nsp->{$name}}); # #undef( %{$nsp->{$name}} ) if defined(%{$nsp->{$name}}) and $name ne "main::"; # } # # use strict; # #print "\nObject going bye-bye\n"; # # } =head2 set_mask (e) Now we close the safe compartment. Only the certain operations can be used within PG problems and the PG macro files. These include the subroutines shared with the safe compartment as defined above and most Perl commands which do not involve file access, access to the system or evaluation. Specifically the following are allowed time() # gives the current Unix time # used to determine whether solutions are visible. atan, sin cos exp log sqrt # arithemetic commands -- more are defined in PGauxiliaryFunctions.pl The following are specifically not allowed: eval() unlink, symlink, system, exec print require =cut ############################################################################## ## restrict the operations allowed within the safe compartment sub set_mask { my $self = shift; my $safe_cmpt = $self ->{safe}; $safe_cmpt->mask(Opcode::full_opset()); # allow no operations $safe_cmpt->permit(qw( :default )); $safe_cmpt->permit(qw(time)); # used to determine whether solutions are visible. $safe_cmpt->permit(qw( atan2 sin cos exp log sqrt )); # just to make sure we'll deny some things specifically $safe_cmpt->deny(qw(entereval)); $safe_cmpt->deny(qw ( unlink symlink system exec )); $safe_cmpt->deny(qw(print require)); } ############################################################################ =head2 Translate =cut sub translate { my $self = shift; my @PROBLEM_TEXT_OUTPUT = (); my $safe_cmpt = $self ->{safe}; my $evalString = $self -> {source}; $self ->{errors} .= qq{ERROR: This problem file was empty!\n} unless ($evalString) ; $self ->{errors} .= qq{ERROR: You must define the environment before translating.} unless defined( $self->{envir} ); # reset the error detection my $save_SIG_die_trap = $SIG{__DIE__}; $SIG{__DIE__} = sub {CORE::die(@_) }; =pod (3) B The input text is subjected to two global replacements. First every incidence of BEGIN_TEXT problem text END_TEXT is replaced by TEXT( EV3( <<'END_TEXT' ) ); problem text END_TEXT The first construction is syntactic sugar for the second. This is explained in C. Second every incidence of \ (backslash) is replaced by \\ (double backslash). Third each incidence of ~~ is replaced by a single backslash. This is done to alleviate a basic incompatibility between TeX and Perl. TeX uses backslashes constantly to denote a command word (as opposed to text which is to be entered literally). Perl uses backslash to escape the following symbol. This escape mechanism takes place immediately when a Perl script is compiled and takes place throughout the code and within every quoted string (both double and single quoted strings) with the single exception of single quoted "here" documents. That is backlashes which appear in TEXT(<<'EOF'); ... text including \{ \} for example EOF are the only ones not immediately evaluated. This behavior makes it very difficult to use TeX notation for defining mathematics within text. The initial global replacement, before compiling a PG problem, allows one to use backslashes within text without doubling them. (The anomolous behavior inside single quoted "here" documents is compensated for by the behavior of the evaluation macro EV3.) This makes typing TeX easy, but introduces one difficulty in entering normal Perl code. The second global replacement provides a work around for this -- use ~~ when you would ordinarily use a backslash in Perl code. In order to define a carriage return use ~~n rather than \n; in order to define a reference to a variable you must use ~~@array rather than \@array. This is annoying and a source of simple compiler errors, but must be lived with. The problems are not evaluated in strict mode, so global variables can be used without warnings. =cut ############################################################################ ########################################## ###### PG preprocessing code ############# ########################################## # BEGIN_TEXT and END_TEXT must occur on a line by themselves. $evalString =~ s/\n\s*END_TEXT[\s;]*\n/\nEND_TEXT\n/g; $evalString =~ s/\n\s*BEGIN_TEXT[\s;]*\n/\nTEXT\(EV3\(<<'END_TEXT'\)\);\n/g; $evalString =~ s/ENDDOCUMENT.*/ENDDOCUMENT();/s; # remove text after ENDDOCUMENT $evalString =~ s/\\/\\\\/g; # \ can't be used for escapes because of TeX conflict $evalString =~ s/~~/\\/g; # use ~~ as escape instead, use # for comments =pod (4) B Evaluate the text within the safe compartment. Save the errors. The safe compartment is a new one unless the $safeCompartment was set to zero in which case the previously defined safe compartment is used. (See item 1.) =cut my ($PG_PROBLEM_TEXT_REF, $PG_HEADER_TEXT_REF, $PG_ANSWER_HASH_REF, $PG_FLAGS_REF) =$safe_cmpt->reval(" $evalString"); # This section could use some more error messages. In particular if a problem doesn't produce the right output, the user needs # information about which problem was at fault. # # $self->{errors} .= $@; # push(@PROBLEM_TEXT_OUTPUT , split(/(\n)/,$$PG_PROBLEM_TEXT_REF) ) if defined($$PG_PROBLEM_TEXT_REF ); push(@PROBLEM_TEXT_OUTPUT , split(/^/,$$PG_PROBLEM_TEXT_REF) ) if ref($PG_PROBLEM_TEXT_REF ) eq 'SCALAR'; ## This is better than using defined($$PG_PROBLEM_TEXT_REF) ## Because more pleasant feedback is given ## when the problem doesn't render. # try to get the \n to appear at the end of the line use strict; ############################################################################# ########## end EVALUATION code ########### ############################################################################# =pod (5) B The error provided by Perl is truncated slightly and returned. In the text string which would normally contain the rendered problem. The original text string is given line numbers and concatenated to the errors. =cut ########################################## ###### PG error processing code ########## ########################################## my (@input,$lineNumber,$line); if ($self -> {errors}) { #($self -> {errors}) =~ s/ {errors}) =~ s/>/>/g; #try to clean up errors so they will look ok $self ->{errors} =~ s/\[.+?\.pl://gm; #erase [Fri Dec 31 12:58:30 1999] processProblem7.pl: #$self -> {errors} =~ s/eval\s+'(.|[\n|r])*$//; #end trying to clean up errors so they will look ok push(@PROBLEM_TEXT_OUTPUT , qq!\n
        Problem!.
                    $self->{envir} ->{'probNum'}.
                    qq!\nERROR caught by PGtranslator while processing problem file:! .
                	$self->{envir}->{'probFileName'}.
                	"\n****************\r\n" .
                	$self -> {errors}."\r\n" .
                	"****************
\n"); push(@PROBLEM_TEXT_OUTPUT , "------Input Read\r\n"); $self->{source} =~ s/{source}); $lineNumber = 1; foreach $line (@input) { chomp($line); push(@PROBLEM_TEXT_OUTPUT, "$lineNumber\t\t$line\r\n"); $lineNumber ++; } push(@PROBLEM_TEXT_OUTPUT ,"\n-----
\r\n"); } =pod (6) B Returns: $PG_PROBLEM_TEXT_ARRAY_REF -- Reference to a string containing the rendered text. $PG_HEADER_TEXT_REF -- Reference to a string containing material to placed in the header (for use by JavaScript) $PG_ANSWER_HASH_REF -- Reference to an array containing the answer evaluators. $PG_FLAGS_REF -- Reference to a hash containing flags and other references: 'error_flag' is set to 1 if there were errors in rendering =cut ## we need to make sure that the other output variables are defined ## If the eval failed with errors, one or more of these variables won't be defined. $PG_ANSWER_HASH_REF = {} unless defined($PG_ANSWER_HASH_REF); $PG_HEADER_TEXT_REF = \( "" ) unless defined($PG_HEADER_TEXT_REF); $PG_FLAGS_REF = {} unless defined($PG_FLAGS_REF); $PG_FLAGS_REF->{'error_flag'} = 1 if $self -> {errors}; my $PG_PROBLEM_TEXT = join("",@PROBLEM_TEXT_OUTPUT); $self ->{ PG_PROBLEM_TEXT_REF } = \$PG_PROBLEM_TEXT; $self ->{ PG_PROBLEM_TEXT_ARRAY_REF } = \@PROBLEM_TEXT_OUTPUT; $self ->{ PG_HEADER_TEXT_REF } = $PG_HEADER_TEXT_REF; $self ->{ rh_correct_answers } = $PG_ANSWER_HASH_REF; $self ->{ PG_FLAGS_REF } = $PG_FLAGS_REF; $SIG{__DIE__} = $save_SIG_die_trap; $self ->{errors}; } # end translate =head2 Answer evaluation methods =cut =head3 access methods $obj->rh_student_answers =cut sub rh_evaluated_answers { my $self = shift; my @in = @_; return $self->{rh_evaluated_answers} if @in == 0; if ( ref($in[0]) eq 'HASH' ) { $self->{rh_evaluated_answers} = { %{ $in[0] } }; # store a copy of the hash } else { $self->{rh_evaluated_answers} = { @in }; # store a copy of the hash } $self->{rh_evaluated_answers}; } sub rh_problem_result { my $self = shift; my @in = @_; return $self->{rh_problem_result} if @in == 0; if ( ref($in[0]) eq 'HASH' ) { $self->{rh_problem_result} = { %{ $in[0] } }; # store a copy of the hash } else { $self->{rh_problem_result} = { @in }; # store a copy of the hash } $self->{rh_problem_result}; } sub rh_problem_state { my $self = shift; my @in = @_; return $self->{rh_problem_state} if @in == 0; if ( ref($in[0]) eq 'HASH' ) { $self->{rh_problem_state} = { %{ $in[0] } }; # store a copy of the hash } else { $self->{rh_problem_state} = { @in }; # store a copy of the hash } $self->{rh_problem_state}; } =head3 process_answers $obj->process_answers() =cut sub process_answers{ my $self = shift; my @in = @_; my %h_student_answers; if (ref($in[0]) eq 'HASH' ) { %h_student_answers = %{ $in[0] }; #receiving a reference to a hash of answers } else { %h_student_answers = @in; # receiving a hash of answers } my $rh_correct_answers = $self->rh_correct_answers(); my @answer_entry_order = ( defined($self->{PG_FLAGS_REF}->{ANSWER_ENTRY_ORDER}) ) ? @{$self->{PG_FLAGS_REF}->{ANSWER_ENTRY_ORDER}} : keys %{$rh_correct_answers}; # apply each instructors answer to the corresponding student answer foreach my $ans_name ( @answer_entry_order ) { my ($ans, $errors) = $self->filter_answer( $h_student_answers{$ans_name} ); no strict; # evaluate the answers inside the safe compartment. local($rf_fun,$temp_ans) = (undef,undef); if ( defined($rh_correct_answers ->{$ans_name} ) ) { $rf_fun = $rh_correct_answers->{$ans_name}; } else { warn "There is no answer evaluator for the question labeled $ans_name"; } $temp_ans = $ans; $temp_ans = '' unless defined($temp_ans); #make sure that answer is always defined # in case the answer evaluator forgets to check $self->{safe}->share('$rf_fun','$temp_ans'); # reset the error detection my $save_SIG_die_trap = $SIG{__DIE__}; $SIG{__DIE__} = sub {CORE::die(@_) }; my $rh_ans_evaluation_result; if (ref($rf_fun) eq 'CODE' ) { $rh_ans_evaluation_result = $self->{safe} ->reval( '&{ $rf_fun }($temp_ans)' ) ; warn "Error in PGtranslator.pm::process_answers: Answer $ans_name:
\n $@\n" if $@; } elsif (ref($rf_fun) eq 'AnswerEvaluator') { $rh_ans_evaluation_result = $self->{safe} ->reval('$rf_fun->evaluate($temp_ans)'); warn "Error in PGtranslator.pm::process_answers: Answer $ans_name:
\n $@\n" if $@; warn "Evaluation error: Answer $ans_name:
\n", $rh_ans_evaluation_result->error_flag(), " :: ",$rh_ans_evaluation_result->error_message(),"
\n" if defined($rh_ans_evaluation_result) and defined($rh_ans_evaluation_result->error_flag()); } else { warn "Error in PGtranslator5.pm::process_answers: Answer $ans_name:
\n Unrecognized evaluator type |", ref($rf_fun), "|"; } $SIG{__DIE__} = $save_SIG_die_trap; use strict; unless ( ( ref($rh_ans_evaluation_result) eq 'HASH') or ( ref($rh_ans_evaluation_result) eq 'AnswerHash') ) { warn "Error in PGtranslator5.pm::process_answers: Answer $ans_name:
\n Answer evaluators must return a hash or an AnswerHash type, not type |", ref($rh_ans_evaluation_result), "|"; } $rh_ans_evaluation_result ->{ans_message} .= "$errors \n" if $errors; $rh_ans_evaluation_result ->{ans_name} = $ans_name; $self->{rh_evaluated_answers}->{$ans_name} = $rh_ans_evaluation_result; } $self->rh_evaluated_answers; } =head3 grade_problem $obj->rh_problem_state(%problem_state); # sets the current problem state $obj->grade_problem(%form_options); =cut sub grade_problem { my $self = shift; my %form_options = @_; my $rf_grader = $self->{rf_problem_grader}; ($self->{rh_problem_result},$self->{rh_problem_state} ) = &{$rf_grader}( $self -> {rh_evaluated_answers}, $self -> {rh_problem_state}, %form_options ); ($self->{rh_problem_result}, $self->{rh_problem_state} ) ; } sub rf_std_problem_grader { my $self = shift; return \&std_problem_grader; } sub old_std_problem_grader{ my $rh_evaluated_answers = shift; my %flags = @_; # not doing anything with these yet my %evaluated_answers = %{$rh_evaluated_answers}; my $allAnswersCorrectQ=1; foreach my $ans_name (keys %evaluated_answers) { # I'm not sure if this check is really useful. if (ref($evaluated_answers{$ans_name} ) eq 'HASH' ) { $allAnswersCorrectQ = 0 unless( 1 == $evaluated_answers{$ans_name}->{score} ); } else { warn "Error: Answer $ans_name is not a hash"; warn "$evaluated_answers{$ans_name}"; } } # Notice that "all answers are correct" if there are no questions. { score => $allAnswersCorrectQ, prev_tries => 0, partial_credit => $allAnswersCorrectQ, errors => "", type => 'old_std_problem_grader', flags => {}, # not doing anything with these yet }; # hash output } ##################################### # This is a model for plug-in problem graders ##################################### sub std_problem_grader{ my $rh_evaluated_answers = shift; my $rh_problem_state = shift; my %form_options = @_; my %evaluated_answers = %{$rh_evaluated_answers}; # The hash $rh_evaluated_answers typically contains: # 'answer1' => 34, 'answer2'=> 'Mozart', etc. # By default the old problem state is simply passed back out again. my %problem_state = %$rh_problem_state; # %form_options might include # The user login name # The permission level of the user # The studentLogin name for this psvn. # Whether the form is asking for a refresh or is submitting a new answer. # initial setup of the answer my %problem_result = ( score => 0, errors => '', type => 'std_problem_grader', msg => '', ); # Checks my $ansCount = keys %evaluated_answers; # get the number of answers unless ($ansCount > 0 ) { $problem_result{msg} = "This problem did not ask any questions."; return(\%problem_result,\%problem_state); } if ($ansCount > 1 ) { $problem_result{msg} = 'In order to get credit for this problem all answers must be correct.' ; } unless (defined( $form_options{answers_submitted}) and $form_options{answers_submitted} == 1) { return(\%problem_result,\%problem_state); } my $allAnswersCorrectQ=1; foreach my $ans_name (keys %evaluated_answers) { # I'm not sure if this check is really useful. if ( ( ref($evaluated_answers{$ans_name} ) eq 'HASH' ) or ( ref($evaluated_answers{$ans_name}) eq 'AnswerHash' ) ) { $allAnswersCorrectQ = 0 unless( 1 == $evaluated_answers{$ans_name}->{score} ); } else { warn "Error: Answer $ans_name is not a hash"; warn "$evaluated_answers{$ans_name}"; warn "This probably means that the answer evaluator is for this answer is not working correctly."; $problem_result{error} = "Error: Answer $ans_name is not a hash: $evaluated_answers{$ans_name}"; } } # report the results $problem_result{score} = $allAnswersCorrectQ; # I don't like to put in this bit of code. # It makes it hard to construct error free problem graders # I would prefer to know that the problem score was numeric. unless ($problem_state{recorded_score} =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ ) { $problem_state{recorded_score} = 0; # This gets rid of non-numeric scores } # if ($allAnswersCorrectQ == 1 or $problem_state{recorded_score} == 1) { $problem_state{recorded_score} = 1; } else { $problem_state{recorded_score} = 0; } $problem_state{num_of_correct_ans}++ if $allAnswersCorrectQ == 1; $problem_state{num_of_incorrect_ans}++ if $allAnswersCorrectQ == 0; (\%problem_result, \%problem_state); } sub rf_avg_problem_grader { my $self = shift; return \&avg_problem_grader; } sub avg_problem_grader{ my $rh_evaluated_answers = shift; my $rh_problem_state = shift; my %form_options = @_; my %evaluated_answers = %{$rh_evaluated_answers}; # The hash $rh_evaluated_answers typically contains: # 'answer1' => 34, 'answer2'=> 'Mozart', etc. # By default the old problem state is simply passed back out again. my %problem_state = %$rh_problem_state; # %form_options might include # The user login name # The permission level of the user # The studentLogin name for this psvn. # Whether the form is asking for a refresh or is submitting a new answer. # initial setup of the answer my $total=0; my %problem_result = ( score => 0, errors => '', type => 'avg_problem_grader', msg => '', ); my $count = keys %evaluated_answers; $problem_result{msg} = 'You can earn partial credit on this problem.' if $count >1; # Return unless answers have been submitted unless ($form_options{answers_submitted} == 1) { return(\%problem_result,\%problem_state); } # Answers have been submitted -- process them. foreach my $ans_name (keys %evaluated_answers) { $total += $evaluated_answers{$ans_name}->{score}; } # Calculate score rounded to three places to avoid roundoff problems $problem_result{score} = $total/$count if $count; # increase recorded score if the current score is greater. $problem_state{recorded_score} = $problem_result{score} if $problem_result{score} > $problem_state{recorded_score}; $problem_state{num_of_correct_ans}++ if $total == $count; $problem_state{num_of_incorrect_ans}++ if $total < $count ; warn "Error in grading this problem the total $total is larger than $count" if $total > $count; (\%problem_result, \%problem_state); } =head3 safetyFilter ($filtered_ans, $errors) = $obj ->filter_ans($ans) $obj ->rf_safety_filter() =cut sub filter_answer { my $self = shift; my $ans = shift; my @filtered_answers; my $errors=''; if (ref($ans) eq 'ARRAY') { #handle the case where the answer comes from several inputs with the same name # In many cases this will be passed as a reference to an array # if it is passed as a single string (separated by \0 characters) as # some early versions of CGI behave, then # it is unclear what will happen when the answer is filtered. foreach my $item (@{$ans}) { my ($filtered_ans, $error) = &{ $self->{rf_safety_filter} } ($item); push(@filtered_answers, $filtered_ans); $errors .= " ". $error if $error; # add error message if error is non-zero. } (\@filtered_answers,$errors); } else { &{ $self->{rf_safety_filter} } ($ans); } } sub rf_safety_filter { my $self = shift; my $rf_filter = shift; $self->{rf_safety_filter} = $rf_filter if $rf_filter and ref($rf_filter) eq 'CODE'; warn "The safety_filter must be a reference to a subroutine" unless ref($rf_filter) eq 'CODE' ; $self->{rf_safety_filter} } sub safetyFilter { my $answer = shift; # accepts one answer and checks it my $submittedAnswer = $answer; $answer = '' unless defined $answer; my ($errorno); $answer =~ tr/\000-\037/ /; #### Return if answer field is empty ######## unless ($answer =~ /\S/) { # $errorno = "
No answer was submitted."; $errorno = 0; ## don't report blank answer as error return ($answer,$errorno); } ######### replace ^ with ** (for exponentiation) # $answer =~ s/\^/**/g; ######### Return if forbidden characters are found unless ($answer =~ /^[a-zA-Z0-9_\-\+ \t\/@%\*\.\n^\(\)]+$/ ) { $answer =~ tr/a-zA-Z0-9_\-\+ \t\/@%\*\.\n^\(\)/#/c; $errorno = "
There are forbidden characters in your answer: $submittedAnswer
"; return ($answer,$errorno); } $errorno = 0; return($answer, $errorno); } ## Check submittedAnswer for forbidden characters, etc. # ($submittedAnswer,$errorno) = safetyFilter($submittedAnswer); # $errors .= "No answer was submitted.
" if $errorno == 1; # $errors .= "There are forbidden characters in your answer: $submittedAnswer
" if $errorno ==2; # ## Check correctAnswer for forbidden characters, etc. # unless (ref($correctAnswer) ) { #skip check if $correctAnswer is a function # ($correctAnswer,$errorno) = safetyFilter($correctAnswer); # $errors .= "No correct answer is given in the statement of the problem. # Please report this to your instructor.
" if $errorno == 1; # $errors .= "There are forbidden characters in the problems answer. # Please report this to your instructor.
" if $errorno == 2; # } =head2 PGsort Because of the way sort is optimized in Perl, the symbols $a and $b have special significance. C$b} @list> C sorts the list numerically and lexically respectively. If C is used in a problem, before the sort routine is defined in a macro, then things get badly confused. To correct this, the following macros are defined in dangerougMacros.pl which is evaluated before the problem template is read. PGsort sub { $_[0] <=> $_[1] }, @list; PGsort sub { $_[0] cmp $_[1] }, @list; provide slightly slower, but safer, routines for the PG language. (The subroutines for ordering are B. Note the commas!) =cut # This sort can cause troubles because of its special use of $a and $b # Putting it in dangerousMacros.pl worked frequently, but not always. # In particular ANS( ans_eva1 ans_eval2) caused trouble. # One answer at a time did not --- very strange. sub PGsort { my $sort_order = shift; die "Must supply an ordering function with PGsort: PGsort sub {\$a cmp \$b }, \@list\n" unless ref($sort_order) eq 'CODE'; sort {&$sort_order($a,$b)} @_; } =head2 includePGtext includePGtext($string_ref, $envir_ref) Calls C recursively with the $safeCompartment variable set to 0 so that the rendering continues in the current safe compartment. The output is the same as the output from createPGtext. This is used in processing some of the sample CAPA files. =cut #this is a method for importing additional PG files from within one PG file. # sub includePGtext { # my $self = shift; # my $string_ref =shift; # my $envir_ref = shift; # $self->environment($envir_ref); # $self->createPGtext($string_ref); # } # evaluation macros no strict; # this is important -- I guess because eval operates on code which is not written with strict in mind. =head2 PG_restricted_eval PG_restricted_eval($string) Evaluated in package 'main'. Result of last statement is returned. When called from within a safe compartment the safe compartment package is 'main'. =cut sub PG_restricted_eval { my $string = shift; my ($pck,$file,$line) = caller; my $save_SIG_warn_trap = $SIG{__WARN__}; $SIG{__WARN__} = sub { CORE::die @_}; my $save_SIG_die_trap = $SIG{__DIE__}; $SIG{__DIE__}= sub {CORE::die @_}; no strict; my $out = eval ("package main; " . $string ); my $errors =$@; my $full_error_report = "PG_restricted_eval detected error at line $line of file $file \n" . $errors . "The calling package is $pck\n" if defined($errors) && $errors =~/\S/; use strict; $SIG{__DIE__} = $save_SIG_die_trap; $SIG{__WARN__} = $save_SIG_warn_trap; return (wantarray) ? ($out, $errors,$full_error_report) : $out; } =head2 PG_answer_eval PG_answer_eval($string) Evaluated in package defined by the current safe compartment. Result of last statement is returned. When called from within a safe compartment the safe compartment package is 'main'. There is still some confusion about how these two evaluation subroutines work and how best to define them. It is useful to have two evaluation procedures since at some point one might like to make the answer evaluations more stringent. =cut sub PG_answer_eval { local($string) = shift; # I made this local just in case -- see PG_estricted_eval my $errors = ''; my $full_error_report = ''; my ($pck,$file,$line) = caller; # Because of the global variable $PG::compartment_name and $PG::safe_cmpt # only one problem safe compartment can be active at a time. # This might cause problems at some point. In that case a cleverer way # of insuring that the package stays in scope until the answer is evaluated # will be required. # This is pretty tricky and doesn't always work right. # We seem to need PG_priv instead of main when PG_answer_eval is called within a completion # 'package PG_priv; ' my $save_SIG_warn_trap = $SIG{__WARN__}; $SIG{__WARN__} = sub { CORE::die @_}; my $save_SIG_die_trap = $SIG{__DIE__}; $SIG{__DIE__}= sub {CORE::die @_}; my $save_SIG_FPE_trap= $SIG{'FPE'}; #$SIG{'FPE'} = \&main::PG_floating_point_exception_handler; #$SIG{'FPE'} = sub {exit(0)}; no strict; my $out = eval('package main;'.$string); $out = '' unless defined($out); $errors .=$@; $full_error_report = "ERROR: at line $line of file $file $errors The calling package is $pck\n" if defined($errors) && $errors =~/\S/; use strict; $SIG{__DIE__} = $save_SIG_die_trap; $SIG{__WARN__} = $save_SIG_warn_trap; $SIG{'FPE'} = $save_SIG_FPE_trap; return (wantarray) ? ($out, $errors,$full_error_report) : $out; } sub dumpvar { my ($packageName) = @_; local(*alias); sub emit { print @_; } *stash = *{"${packageName}::"}; $, = " "; emit "Content-type: text/html\n\n
\n";
    
    
    while ( ($varName, $globValue) = each %stash) {
        emit "$varName\n";
        
	*alias = $globValue;
	next if $varName=~/main/;
	
	if (defined($alias) ) {
	    emit "  \$$varName $alias \n";
	}
	
	if ( defined(@alias) ) {
	    emit "  \@$varName @alias \n";
	}
	if (defined(%alias) ) {
	    emit "  %$varName \n";
	    foreach $key (keys %alias) {
	        emit "    $key => $alias{$key}\n";
	    }



	}
    }
    emit "
"; } use strict; #### for error checking and debugging purposes sub pretty_print_rh { my $rh = shift; foreach my $key (sort keys %{$rh}) { warn " $key => ",$rh->{$key},"\n"; } } # end evaluation subroutines 1;