Parent Directory
|
Revision Log
Insure that error files can be read from the web even if the files are served using lighttpd and port 8080 instead of the main server. This adds group readable and executable permissions to the working directory for creating the tex files.
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm,v 1.102 2009/09/25 00:39:49 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 17 package WeBWorK::ContentGenerator::Hardcopy; 18 use base qw(WeBWorK::ContentGenerator); 19 20 =head1 NAME 21 22 WeBWorK::ContentGenerator::Hardcopy - generate printable versions of one or more 23 problem sets. 24 25 =cut 26 27 use strict; 28 use warnings; 29 30 #use Apache::Constants qw/:common REDIRECT/; 31 #use CGI qw(-nosticky ); 32 use WeBWorK::CGI; 33 34 use File::Path; 35 use File::Temp qw/tempdir/; 36 use String::ShellQuote; 37 use WeBWorK::DB::Utils qw/user2global/; 38 use WeBWorK::Debug; 39 use WeBWorK::Form; 40 use WeBWorK::HTML::ScrollingRecordList qw/scrollingRecordList/; 41 use WeBWorK::PG; 42 use WeBWorK::Utils qw/readFile decodeAnswers/; 43 use PGrandom; 44 45 =head1 CONFIGURATION VARIABLES 46 47 =over 48 49 =item $PreserveTempFiles 50 51 If true, don't delete temporary files. 52 53 =cut 54 55 our $PreserveTempFiles = 0 unless defined $PreserveTempFiles; 56 57 =back 58 59 =cut 60 61 our $HC_DEFAULT_FORMAT = "pdf"; # problems if this is not an allowed format for the user... 62 our %HC_FORMATS = ( 63 tex => { name => "TeX Source", subr => "generate_hardcopy_tex" }, 64 pdf => { name => "Adobe PDF", subr => "generate_hardcopy_pdf" }, 65 ); 66 67 # custom fields used in $self hash 68 # FOR HEAVEN'S SAKE, PLEASE KEEP THIS UP-TO-DATE! 69 # 70 # final_file_url 71 # contains the URL of the final hardcopy file generated 72 # set by generate_hardcopy(), used by pre_header_initialize() and body() 73 # 74 # temp_file_map 75 # reference to a hash mapping temporary file names to URL 76 # set by pre_header_initialize(), used by body() 77 # 78 # hardcopy_errors 79 # reference to array containing HTML strings describing generation errors (and warnings) 80 # used by add_errors(), get_errors(), get_errors_ref() 81 # 82 # at_least_one_problem_rendered_without_error 83 # set to a true value by write_problem_tex if it is able to sucessfully render 84 # a problem. checked by generate_hardcopy to determine whether to continue 85 # with the generation process. 86 # 87 # versioned 88 # set to a true value in write_set_tex if the set_id indicates that 89 # the set being rendered is a versioned set; this is used in 90 # write_problem_tex to determine which problem merging routine from 91 # DB.pm to use, and to indicate what problem number in a versioned 92 # test we're on 93 # 94 # mergedSets 95 # a reference to a hash { userID!setID => setObject }, where setID is 96 # either the set id or the fake versioned set id "setName,vN" depending 97 # on whether the set is a versioned set or not. this may include the 98 # sets for which the hardcopy is being generated (or may not), depending 99 # on whether they were needed to determine the required permissions for 100 # generating a hardcopy 101 # 102 # canShowScore 103 # a reference to a hash { userID!setID => boolean }, where setID is either 104 # the set id or the fake versioned set id "setName,vN" depending on whether 105 # the set is a versioned set or not, and the value of the boolean is 106 # determined by the corresponding userSet's value of hide_score and the 107 # current time 108 109 ################################################################################ 110 # UI subroutines 111 ################################################################################ 112 113 sub pre_header_initialize { 114 my ($self) = @_; 115 my $r = $self->r; 116 my $ce = $r->ce; 117 my $db = $r->db; 118 my $authz = $r->authz; 119 120 my $userID = $r->param("user"); 121 my $eUserID = $r->param("effectiveUser"); 122 my @setIDs = $r->param("selected_sets"); 123 my @userIDs = $r->param("selected_users"); 124 my $hardcopy_format = $r->param("hardcopy_format"); 125 my $generate_hardcopy = $r->param("generate_hardcopy"); 126 my $send_existing_hardcopy = $r->param("send_existing_hardcopy"); 127 my $final_file_url = $r->param("final_file_url"); 128 129 # if there's an existing hardcopy file that can be sent, get set up to do that 130 if ($send_existing_hardcopy) { 131 $self->reply_with_redirect($final_file_url); 132 $self->{final_file_url} = $final_file_url; 133 $self->{send_hardcopy} = 1; 134 return; 135 } 136 137 # this should never happen, but apparently it did once (see bug #714), so we check for it 138 die "Parameter 'user' not defined -- this should never happen" unless defined $userID; 139 140 if ($generate_hardcopy) { 141 my $validation_failed = 0; 142 143 # set default format 144 $hardcopy_format = $HC_DEFAULT_FORMAT unless defined $hardcopy_format; 145 146 # make sure format is valid 147 unless (grep { $_ eq $hardcopy_format } keys %HC_FORMATS) { 148 $self->addbadmessage("'$hardcopy_format' is not a valid hardcopy format."); 149 $validation_failed = 1; 150 } 151 152 # make sure we are allowed to generate hardcopy in this format 153 unless ($authz->hasPermissions($userID, "download_hardcopy_format_$hardcopy_format")) { 154 $self->addbadmessage("You do not have permission to generate hardcopy in $hardcopy_format format."); 155 $validation_failed = 1; 156 } 157 158 # is there at least one user and set selected? 159 unless (@userIDs) { 160 $self->addbadmessage("Please select at least one user and try again."); 161 $validation_failed = 1; 162 } 163 164 # when students don't select any sets the size of @setIDs is 1 with a null character in $setIDs[0]. 165 # when professors don't select any sets the size of @setIDs is 0. 166 # the following test "unless ((@setIDs) and ($setIDs[0] =~ /\S+/))" catches both cases and prevents 167 # warning messages in the case of a professor's empty array. 168 unless ((@setIDs) and ($setIDs[0] =~ /\S+/)) { 169 $self->addbadmessage("Please select at least one set and try again."); 170 $validation_failed = 1; 171 } 172 173 # is the user allowed to request multiple sets/users at a time? 174 my $perm_multiset = $authz->hasPermissions($userID, "download_hardcopy_multiset"); 175 my $perm_multiuser = $authz->hasPermissions($userID, "download_hardcopy_multiuser"); 176 177 my $perm_viewhidden = $authz->hasPermissions($userID, "view_hidden_work"); 178 my $perm_viewfromip = $authz->hasPermissions($userID, "view_ip_restricted_sets"); 179 180 if (@setIDs > 1 and not $perm_multiset) { 181 $self->addbadmessage("You are not permitted to generate hardcopy for multiple sets. Please select a single set and try again."); 182 $validation_failed = 1; 183 } 184 if (@userIDs > 1 and not $perm_multiuser) { 185 $self->addbadmessage("You are not permitted to generate hardcopy for multiple users. Please select a single user and try again."); 186 $validation_failed = 1; 187 } 188 if (@userIDs and $userIDs[0] ne $eUserID and not $perm_multiuser) { 189 $self->addbadmessage("You are not permitted to generate hardcopy for other users."); 190 $validation_failed = 1; 191 # FIXME -- download_hardcopy_multiuser controls both whether a user can generate hardcopy 192 # that contains sets for multiple users AND whether she can generate hardcopy that contains 193 # sets for users other than herself. should these be separate permission levels? 194 } 195 196 # to check if the set has a "hide_work" flag, or if we aren't 197 # allowed to view the set from the user's IP address, we 198 # need the userset objects; if we've not failed validation 199 # yet, get those to check on this 200 my %canShowScore = (); 201 my %mergedSets = (); 202 unless ($validation_failed ) { 203 foreach my $sid ( @setIDs ) { 204 my($s,undef,$v) = ($sid =~ /([^,]+)(,v(\d+))?$/); 205 foreach my $uid ( @userIDs ) { 206 if ( $perm_viewhidden && $perm_viewfromip ) { 207 $canShowScore{"$uid!$sid"} = 1; 208 } else { 209 my $userSet; 210 if ( defined($v) ) { 211 $userSet = $db->getMergedSetVersion($uid,$s,$v); 212 } else { 213 $userSet = $db->getMergedSet($uid,$s); 214 } 215 $mergedSets{"$uid!$sid"} = $userSet; 216 if ( ! $perm_viewhidden && 217 defined( $userSet->hide_work ) && 218 ( $userSet->hide_work eq 'Y' || 219 ( $userSet->hide_work eq 'BeforeAnswerDate' && 220 time < $userSet->answer_date ) ) ) { 221 $validation_failed = 1; 222 $self->addbadmessage("You are not permitted to generate a hardcopy for a set with hidden work."); 223 last; 224 } 225 226 if ( $authz->invalidIPAddress($userSet) ) { 227 $validation_failed = 1; 228 $self->addbadmessage("You are not allowed to generate a " . 229 "hardcopy for " . $userSet->set_id . 230 " from your IP address, " . 231 $r->connection->remote_ip . "."); 232 last; 233 } 234 235 $canShowScore{"$uid!$sid"} = 236 ( ! defined( $userSet->hide_score ) || 237 $userSet->hide_score eq '' ) || 238 ( $userSet ->hide_score eq 'N' || 239 ( $userSet->hide_score eq 'BeforeAnswerDate' && 240 time >= $userSet->answer_date ) ); 241 # die("hide_score = ", $userSet->hide_score, "; canshow{$uid!$sid} = ", (($canShowScore{"$uid!$sid"})?"True":"False"), "\n"); 242 243 } 244 last if $validation_failed; 245 } 246 } 247 } 248 249 unless ($validation_failed) { 250 $self->{canShowScore} = \%canShowScore; 251 $self->{mergedSets} = \%mergedSets; 252 my ($final_file_url, %temp_file_map) = $self->generate_hardcopy($hardcopy_format, \@userIDs, \@setIDs); 253 if ($self->get_errors) { 254 # store the URLs in self hash so that body() can make a link to it 255 $self->{final_file_url} = $final_file_url; 256 $self->{temp_file_map} = \%temp_file_map; 257 } else { 258 # send the file only 259 $self->reply_with_redirect($final_file_url); 260 } 261 } 262 } 263 } 264 265 sub body { 266 my ($self) = @_; 267 my $userID = $self->r->param("user"); 268 my $perm_view_errors = $self->r->authz->hasPermissions($userID, "download_hardcopy_view_errors"); 269 $perm_view_errors = (defined($perm_view_errors) ) ? $perm_view_errors : 0; 270 if (my $num = $self->get_errors) { 271 my $final_file_url = $self->{final_file_url}; 272 my %temp_file_map = %{$self->{temp_file_map}}; 273 if($perm_view_errors) { 274 my $errors_str = $num > 1 ? "errors" : "error"; 275 print CGI::p("$num $errors_str occured while generating hardcopy:"); 276 277 print CGI::ul(CGI::li($self->get_errors_ref)); 278 } 279 280 if ($final_file_url) { 281 print CGI::p( 282 "A hardcopy file was generated, but it may not be complete or correct.", 283 "Please check that no problems are missing and that they are all legible." , 284 "If not, please inform your instructor.<br />", 285 CGI::a({href=>$final_file_url}, "Download Hardcopy"), 286 ); 287 } else { 288 print CGI::p( 289 "WeBWorK was unable to generate a paper copy of this homework set. Please inform your instructor. " 290 ); 291 292 } 293 if($perm_view_errors) { 294 if (%temp_file_map) { 295 print CGI::start_p(); 296 print "You can also examine the following temporary files: "; 297 my $first = 1; 298 while (my ($temp_file_name, $temp_file_url) = each %temp_file_map) { 299 if ($first) { 300 $first = 0; 301 } else { 302 print ", "; 303 } 304 print CGI::a({href=>$temp_file_url}, " $temp_file_name"); 305 } 306 print CGI::end_p(); 307 } 308 } 309 310 print CGI::hr(); 311 } 312 313 # don't display the retry form if there are errors and the user doesn't have permission to view the errors. 314 unless ($self->get_errors and not $perm_view_errors) { 315 $self->display_form(); 316 } 317 ''; # return a blank 318 } 319 320 sub display_form { 321 my ($self) = @_; 322 my $r = $self->r; 323 my $db = $r->db; 324 my $authz = $r->authz; 325 my $userID = $r->param("user"); 326 my $eUserID = $r->param("effectiveUser"); 327 328 # first time we show up here, fill in some values 329 unless ($r->param("in_hc_form")) { 330 # if a set was passed in via the path_info, add that to the list of sets. 331 my $singleSet = $r->urlpath->arg("setID"); 332 if (defined $singleSet and $singleSet ne "") { 333 my @selected_sets = $r->param("selected_sets"); 334 $r->param("selected_sets" => [ @selected_sets, $singleSet]) unless grep { $_ eq $singleSet } @selected_sets; 335 } 336 337 # if no users are selected, select the effective user 338 my @selected_users = $r->param("selected_users"); 339 unless (@selected_users) { 340 $r->param("selected_users" => $eUserID); 341 } 342 } 343 344 my $perm_multiset = $authz->hasPermissions($userID, "download_hardcopy_multiset"); 345 my $perm_multiuser = $authz->hasPermissions($userID, "download_hardcopy_multiuser"); 346 my $perm_texformat = $authz->hasPermissions($userID, "download_hardcopy_format_tex"); 347 my $perm_unopened = $authz->hasPermissions($userID, "view_unopened_sets"); 348 my $perm_unpublished = $authz->hasPermissions($userID, "view_unpublished_sets"); 349 350 # get formats 351 my @formats; 352 foreach my $format (keys %HC_FORMATS) { 353 push @formats, $format if $authz->hasPermissions($userID, "download_hardcopy_format_$format"); 354 } 355 356 # get format names hash for radio buttons 357 my %format_labels = map { $_ => $HC_FORMATS{$_}{name} || $_ } @formats; 358 359 # get users for selection 360 my @Users; 361 if ($perm_multiuser) { 362 # if we're allowed to select multiple users, get all the users 363 # DBFIXME shouldn't need to pass list of users, should use iterator for results? 364 @Users = $db->getUsers($db->listUsers); 365 } else { 366 # otherwise, we get our own record only 367 @Users = $db->getUser($eUserID); 368 } 369 370 # get sets for selection 371 # DBFIXME should use WHERE clause to filter on open_date and published, rather then getting all 372 my @globalSetIDs; 373 my @GlobalSets; 374 if ($perm_multiuser) { 375 # if we're allowed to select sets for multiple users, get all sets. 376 @globalSetIDs = $db->listGlobalSets; 377 @GlobalSets = $db->getGlobalSets(@globalSetIDs); 378 } else { 379 # otherwise, only get the sets assigned to the effective user. 380 # note that we are getting GlobalSets, but using the list of UserSets assigned to the 381 # effective user. this is because if we pass UserSets to ScrollingRecordList it will 382 # give us composite IDs back, which is a pain in the ass to deal with. 383 @globalSetIDs = $db->listUserSets($eUserID); 384 @GlobalSets = $db->getGlobalSets(@globalSetIDs); 385 } 386 # we also want to get the versioned sets for this user 387 # FIXME: this is another place where we assume that there is a 388 # one-to-one correspondence between assignment_type =~ gateway 389 # and versioned sets. I think we really should have a 390 # "is_versioned" flag on set objects instead. 391 my @versionedSets = grep {$_->assignment_type =~ /gateway/} @GlobalSets; 392 my @SetVersions = (); 393 foreach my $v (@versionedSets) { 394 my @usv = map { [$eUserID, $v->set_id, $_] } ( $db->listSetVersions( $eUserID, $v->set_id ) ); 395 push( @SetVersions, $db->getSetVersions( @usv ) ); 396 } 397 # FIXME: this is a hideous, horrible hack. the identifying key for 398 # a global set is the set_id. those for a set version are the 399 # set_id and version_id. but this means that we have trouble 400 # displaying them both together in HTML::scrollingRecordList. 401 # so we brutally play tricks with the set_id here, which probably 402 # is not very robust, and certainly is aesthetically displeasing. 403 # yuck. 404 foreach ( @SetVersions ) { 405 $_->set_id($_->set_id . ",v" . $_->version_id); 406 } 407 408 # filter out unwanted sets 409 my @WantedGlobalSets; 410 foreach my $i (0 .. $#GlobalSets) { 411 my $Set = $GlobalSets[$i]; 412 unless (defined $Set) { 413 warn "\$GlobalSets[$i] (ID $globalSetIDs[$i]) not defined -- skipping"; 414 next; 415 } 416 next unless $Set->open_date <= time or $perm_unopened; 417 next unless $Set->published or $perm_unpublished; 418 # also skip gateway sets, for which we have to have a 419 # version to print something 420 next if $Set->assignment_type =~ /gateway/; 421 push @WantedGlobalSets, $Set; 422 } 423 424 my $scrolling_user_list = scrollingRecordList({ 425 name => "selected_users", 426 request => $r, 427 default_sort => "lnfn", 428 default_format => "lnfn_uid", 429 default_filters => ["all"], 430 size => 20, 431 multiple => $perm_multiuser, 432 }, @Users); 433 434 my $scrolling_set_list = scrollingRecordList({ 435 name => "selected_sets", 436 request => $r, 437 default_sort => "set_id", 438 default_format => "sid", 439 default_filters => ["all"], 440 size => 20, 441 multiple => $perm_multiset, 442 }, @WantedGlobalSets, @SetVersions ); 443 444 # we change the text a little bit depending on whether the user has multiuser privileges 445 my $ss = $perm_multiuser ? "s" : ""; 446 my $aa = $perm_multiuser ? " " : " a "; 447 my $phrase_for_privileged_users = $perm_multiuser ? "to privileged users or" : ""; 448 my $button_label = $perm_multiuser ? "Generate hardcopy for selected sets and selected users" :"Generate hardcopy"; 449 450 # print CGI::start_p(); 451 # print "Select the homework set$ss for which to generate${aa}hardcopy version$ss."; 452 # if ($authz->hasPermissions($userID, "download_hardcopy_multiuser")) { 453 # print "You may also select multiple users from the users list. You will receive hardcopy for each (set, user) pair."; 454 # } 455 # print CGI::end_p(); 456 457 print CGI::start_form(-method=>"POST", -action=>$r->uri); 458 print $self->hidden_authen_fields(); 459 print CGI::hidden("in_hc_form", 1); 460 461 if ($perm_multiuser and $perm_multiset) { 462 print CGI::p("Select the homework sets for which to generate hardcopy versions. You may" 463 ." also select multiple users from the users list. You will receive hardcopy" 464 ." for each (set, user) pair."); 465 466 print CGI::table({class=>"FormLayout"}, 467 CGI::Tr({}, 468 CGI::th("Users"), 469 CGI::th("Sets"), 470 ), 471 CGI::Tr({}, 472 CGI::td($scrolling_user_list), 473 CGI::td($scrolling_set_list), 474 ), 475 ); 476 } else { # single user mode 477 #FIXME -- do a better job of getting the set and the user when in the single set mode 478 my $selected_set_id = $r->param("selected_sets"); 479 $selected_set_id = '' unless defined $selected_set_id; 480 481 my $selected_user_id = $Users[0]->user_id; 482 print CGI::hidden("selected_sets", $selected_set_id ), 483 CGI::hidden( "selected_users", $selected_user_id); 484 485 # make display for versioned sets a bit nicer 486 $selected_set_id =~ s/,v(\d+)$/ (test $1)/; 487 488 print CGI::p("Download hardcopy of set ", $selected_set_id, " for ", $Users[0]->first_name, " ",$Users[0]->last_name,"?"); 489 490 } 491 print CGI::table({class=>"FormLayout"}, 492 CGI::Tr({}, 493 CGI::td({colspan=>2, class=>"ButtonRow"}, 494 CGI::small("You may choose to show any of the following data. Correct answers and solutions are only 495 available $phrase_for_privileged_users after the answer date of the homework set."), 496 CGI::br(), 497 CGI::b("Show:"), " ", 498 CGI::checkbox( 499 -name => "showCorrectAnswers", 500 -checked => scalar($r->param("showCorrectAnswers")) || 0, 501 -label => "Correct answers", 502 ), 503 CGI::checkbox( 504 -name => "showHints", 505 -checked => scalar($r->param("showHints")) || 0, 506 -label => "Hints", 507 ), 508 CGI::checkbox( 509 -name => "showSolutions", 510 -checked => scalar($r->param("showSolutions")) || 0, 511 -label => "Solutions", 512 ), 513 ), 514 ), 515 CGI::Tr({}, 516 CGI::td({colspan=>2, class=>"ButtonRow"}, 517 CGI::b("Hardcopy Format:"), " ", 518 CGI::radio_group( 519 -name => "hardcopy_format", 520 -values => \@formats, 521 -default => scalar($r->param("hardcopy_format")) || $HC_DEFAULT_FORMAT, 522 -labels => \%format_labels, 523 ), 524 ), 525 ), 526 CGI::Tr({}, 527 CGI::td({colspan=>2, class=>"ButtonRow"}, 528 CGI::submit( 529 -name => "generate_hardcopy", 530 -value => $button_label, 531 #-style => "width: 45ex", 532 ), 533 ), 534 ), 535 ); 536 537 print CGI::end_form(); 538 539 return ""; 540 } 541 542 ################################################################################ 543 # harddcopy generating subroutines 544 ################################################################################ 545 546 sub generate_hardcopy { 547 my ($self, $format, $userIDsRef, $setIDsRef) = @_; 548 my $r = $self->r; 549 my $ce = $r->ce; 550 my $db = $r->db; 551 my $authz = $r->authz; 552 553 my $courseID = $r->urlpath->arg("courseID"); 554 my $userID = $r->param("user"); 555 my $eUserID = $r->param("effectiveUser"); 556 557 # we want to make the temp directory web-accessible, for error reporting 558 # use mkpath to ensure it exists (mkpath is pretty much ``mkdir -p'') 559 my $temp_dir_parent_path = $ce->{courseDirs}{html_temp} . "/hardcopy"; 560 eval { mkpath($temp_dir_parent_path) }; 561 if ($@) { 562 die "Couldn't create hardcopy directory $temp_dir_parent_path: $@"; 563 } 564 565 # create a randomly-named working directory in the hardcopy directory 566 my $temp_dir_path = eval { tempdir("work.XXXXXXXX", DIR => $temp_dir_parent_path) }; 567 if ($@) { 568 $self->add_errors("Couldn't create temporary working directory: ".CGI::code(CGI::escapeHTML($@))); 569 return; 570 } 571 # make sure the directory can be read by other daemons e.g. lighttpd 572 chmod 0755, $temp_dir_path; 573 574 575 # do some error checking 576 unless (-e $temp_dir_path) { 577 $self->add_errors("Temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path)) 578 ."' does not exist, but creation didn't fail. This shouldn't happen."); 579 return; 580 } 581 unless (-w $temp_dir_path) { 582 $self->add_errors("Temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path)) 583 ."' is not writeable."); 584 $self->delete_temp_dir($temp_dir_path); 585 return; 586 } 587 588 my $tex_file_name = "hardcopy.tex"; 589 my $tex_file_path = "$temp_dir_path/$tex_file_name"; 590 591 # write TeX 592 my $open_result = open my $FH, ">", $tex_file_path; 593 unless ($open_result) { 594 $self->add_errors("Failed to open file '".CGI::code(CGI::escapeHTML($tex_file_path)) 595 ."' for writing: ".CGI::code(CGI::escapeHTML($!))); 596 $self->delete_temp_dir($temp_dir_path); 597 return; 598 } 599 $self->write_multiuser_tex($FH, $userIDsRef, $setIDsRef); 600 close $FH; 601 602 # if no problems got rendered successfully, we can't continue 603 unless ($self->{at_least_one_problem_rendered_without_error}) { 604 $self->add_errors("No problems rendered. Can't continue."); 605 $self->delete_temp_dir($temp_dir_path); 606 return; 607 } 608 609 # if no hardcopy.tex file was generated, fail now 610 unless (-e "$temp_dir_path/hardcopy.tex") { 611 $self->add_errors("'".CGI::code("hardcopy.tex")."' not written to temporary directory '" 612 .CGI::code(CGI::escapeHTML($temp_dir_path))."'. Can't continue."); 613 $self->delete_temp_dir($temp_dir_path); 614 return; 615 } 616 617 # determine base name of final file 618 my $final_file_user = @$userIDsRef > 1 ? "multiuser" : $userIDsRef->[0]; 619 my $final_file_set = @$setIDsRef > 1 ? "multiset" : $setIDsRef->[0]; 620 my $final_file_basename = "$courseID.$final_file_user.$final_file_set"; 621 622 # call format subroutine 623 # $final_file_name is the name of final hardcopy file 624 # @temp_files is a list of temporary files of interest used by the subroutine 625 # (all are relative to $temp_dir_path) 626 my $format_subr = $HC_FORMATS{$format}{subr}; 627 my ($final_file_name, @temp_files) = $self->$format_subr($temp_dir_path, $final_file_basename); 628 my $final_file_path = "$temp_dir_path/$final_file_name"; 629 630 #warn "final_file_name=$final_file_name\n"; 631 #warn "temp_files=@temp_files\n"; 632 633 # calculate URLs for each temp file of interest 634 # makeTempDirectory's interface forces us to reverse-engineer the name of the temp dir from the path 635 my $temp_dir_parent_url = $ce->{courseURLs}{html_temp} . "/hardcopy"; 636 (my $temp_dir_url = $temp_dir_path) =~ s/^$temp_dir_parent_path/$temp_dir_parent_url/; 637 my %temp_file_map; 638 foreach my $temp_file_name (@temp_files) { 639 $temp_file_map{$temp_file_name} = "$temp_dir_url/$temp_file_name"; 640 } 641 642 my $final_file_url; 643 644 # make sure final file exists 645 unless (-e $final_file_path) { 646 $self->add_errors("Final hardcopy file '".CGI::code(CGI::escapeHTML($final_file_path)) 647 ."' not found after calling '".CGI::code(CGI::escapeHTML($format_subr))."': " 648 .CGI::code(CGI::escapeHTML($!))); 649 return $final_file_url, %temp_file_map; 650 } 651 652 # try to move the hardcopy file out of the temp directory 653 # set $final_file_url accordingly 654 my $final_file_final_path = "$temp_dir_parent_path/$final_file_name"; 655 my $mv_cmd = "2>&1 " . $ce->{externalPrograms}{mv} . " " . shell_quote($final_file_path, $final_file_final_path); 656 my $mv_out = readpipe $mv_cmd; 657 if ($?) { 658 $self->add_errors("Failed to move hardcopy file '".CGI::code(CGI::escapeHTML($final_file_name)) 659 ."' from '".CGI::code(CGI::escapeHTML($temp_dir_path))."' to '" 660 .CGI::code(CGI::escapeHTML($temp_dir_parent_path))."':".CGI::br() 661 .CGI::pre(CGI::escapeHTML($mv_out))); 662 $final_file_url = "$temp_dir_url/$final_file_name"; 663 } else { 664 $final_file_url = "$temp_dir_parent_url/$final_file_name"; 665 } 666 667 # remove the temp directory if there are no errors 668 unless ($self->get_errors or $PreserveTempFiles) { 669 $self->delete_temp_dir($temp_dir_path); 670 } 671 672 warn "Preserved temporary files in directory '$temp_dir_path'.\n" if $PreserveTempFiles; 673 674 return $final_file_url, %temp_file_map; 675 } 676 677 # helper function to remove temp dirs 678 sub delete_temp_dir { 679 my ($self, $temp_dir_path) = @_; 680 681 my $rm_cmd = "2>&1 " . $self->r->ce->{externalPrograms}{rm} . " -rf " . shell_quote($temp_dir_path); 682 my $rm_out = readpipe $rm_cmd; 683 if ($?) { 684 $self->add_errors("Failed to remove temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path))."':" 685 .CGI::br().CGI::pre($rm_out)); 686 return 0; 687 } else { 688 return 1; 689 } 690 } 691 692 # format subroutines 693 # 694 # assume that TeX source is located at $temp_dir_path/hardcopy.tex 695 # the generated file will being with $final_file_basename 696 # first element of return value is the name of the generated file (relative to $temp_dir_path) 697 # rest of return value elements are names of temporary files that may be of interest in the 698 # case of an error, relative to $temp_dir_path. these are returned whether or not an error 699 # actually occured. 700 701 sub generate_hardcopy_tex { 702 my ($self, $temp_dir_path, $final_file_basename) = @_; 703 704 my $final_file_name; 705 706 # try to rename tex file 707 my $src_name = "hardcopy.tex"; 708 my $dest_name = "$final_file_basename.tex"; 709 my $mv_cmd = "2>&1 " . $self->r->ce->{externalPrograms}{mv} . " " . shell_quote("$temp_dir_path/$src_name", "$temp_dir_path/$dest_name"); 710 my $mv_out = readpipe $mv_cmd; 711 if ($?) { 712 $self->add_errors("Failed to rename '".CGI::code(CGI::escapeHTML($src_name))."' to '" 713 .CGI::code(CGI::escapeHTML($dest_name))."' in directory '" 714 .CGI::code(CGI::escapeHTML($temp_dir_path))."':".CGI::br() 715 .CGI::pre(CGI::escapeHTML($mv_out))); 716 $final_file_name = $src_name; 717 } else { 718 $final_file_name = $dest_name; 719 } 720 721 return $final_file_name; 722 } 723 724 sub generate_hardcopy_pdf { 725 my ($self, $temp_dir_path, $final_file_basename) = @_; 726 727 # call pdflatex - we don't want to chdir in the mod_perl process, as 728 # that might step on the feet of other things (esp. in Apache 2.0) 729 my $pdflatex_cmd = "cd " . shell_quote($temp_dir_path) . " && " 730 . $self->r->ce->{externalPrograms}{pdflatex} 731 . " >pdflatex.stdout 2>pdflatex.stderr hardcopy"; 732 if (my $rawexit = system $pdflatex_cmd) { 733 my $exit = $rawexit >> 8; 734 my $signal = $rawexit & 127; 735 my $core = $rawexit & 128; 736 $self->add_errors("Failed to convert TeX to PDF with command '" 737 .CGI::code(CGI::escapeHTML($pdflatex_cmd))."' (exit=$exit signal=$signal core=$core)."); 738 739 # read hardcopy.log and report first error 740 my $hardcopy_log = "$temp_dir_path/hardcopy.log"; 741 if (-e $hardcopy_log) { 742 if (open my $LOG, "<", $hardcopy_log) { 743 my $line; 744 while ($line = <$LOG>) { 745 last if $line =~ /^!\s+/; 746 } 747 my $first_error = $line; 748 while ($line = <$LOG>) { 749 last if $line =~ /^!\s+/; 750 $first_error .= $line; 751 } 752 close $LOG; 753 if (defined $first_error) { 754 $self->add_errors("First error in TeX log is:".CGI::br(). 755 CGI::pre(CGI::escapeHTML($first_error))); 756 } else { 757 $self->add_errors("No errors encoundered in TeX log."); 758 } 759 } else { 760 $self->add_errors("Could not read TeX log: ".CGI::code(CGI::escapeHTML($!))); 761 } 762 } else { 763 $self->add_errors("No TeX log was found."); 764 } 765 } 766 767 my $final_file_name; 768 769 # try rename the pdf file 770 my $src_name = "hardcopy.pdf"; 771 my $dest_name = "$final_file_basename.pdf"; 772 my $mv_cmd = "2>&1 " . $self->r->ce->{externalPrograms}{mv} . " " . shell_quote("$temp_dir_path/$src_name", "$temp_dir_path/$dest_name"); 773 my $mv_out = readpipe $mv_cmd; 774 if ($?) { 775 $self->add_errors("Failed to rename '".CGI::code(CGI::escapeHTML($src_name))."' to '" 776 .CGI::code(CGI::escapeHTML($dest_name))."' in directory '" 777 .CGI::code(CGI::escapeHTML($temp_dir_path))."':".CGI::br() 778 .CGI::pre(CGI::escapeHTML($mv_out))); 779 $final_file_name = $src_name; 780 } else { 781 $final_file_name = $dest_name; 782 } 783 784 return $final_file_name, qw/hardcopy.tex hardcopy.log hardcopy.aux pdflatex.stdout pdflatex.stderr/; 785 } 786 787 ################################################################################ 788 # TeX aggregating subroutines 789 ################################################################################ 790 791 sub write_multiuser_tex { 792 my ($self, $FH, $userIDsRef, $setIDsRef) = @_; 793 my $r = $self->r; 794 my $ce = $r->ce; 795 796 my @userIDs = @$userIDsRef; 797 my @setIDs = @$setIDsRef; 798 799 # get snippets 800 my $preamble = $ce->{webworkFiles}->{hardcopySnippets}->{preamble}; 801 my $postamble = $ce->{webworkFiles}->{hardcopySnippets}->{postamble}; 802 my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{userDivider}; 803 804 # write preamble 805 $self->write_tex_file($FH, $preamble); 806 807 # write section for each user 808 while (defined (my $userID = shift @userIDs)) { 809 $self->write_multiset_tex($FH, $userID, @setIDs); 810 $self->write_tex_file($FH, $divider) if @userIDs; # divide users, but not after the last user 811 } 812 813 # write postamble 814 $self->write_tex_file($FH, $postamble); 815 } 816 817 sub write_multiset_tex { 818 my ($self, $FH, $targetUserID, @setIDs) = @_; 819 my $r = $self->r; 820 my $ce = $r->ce; 821 my $db = $r->db; 822 823 # get user record 824 my $TargetUser = $db->getUser($targetUserID); # checked 825 unless ($TargetUser) { 826 $self->add_errors("Can't generate hardcopy for user '".CGI::code(CGI::escapeHTML($targetUserID))."' -- no such user exists.\n"); 827 return; 828 } 829 830 # get set divider 831 my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{setDivider}; 832 833 # write each set 834 while (defined (my $setID = shift @setIDs)) { 835 $self->write_set_tex($FH, $TargetUser, $setID); 836 $self->write_tex_file($FH, $divider) if @setIDs; # divide sets, but not after the last set 837 } 838 } 839 840 sub write_set_tex { 841 my ($self, $FH, $TargetUser, $setID) = @_; 842 my $r = $self->r; 843 my $ce = $r->ce; 844 my $db = $r->db; 845 my $authz = $r->authz; 846 my $userID = $r->param("user"); 847 848 # we may already have the MergedSet from checking hide_work and 849 # hide_score in pre_header_initialize; check to see if that's true, 850 # and otherwise, get the set. 851 my %mergedSets = %{$self->{mergedSets}}; 852 my $uid = $TargetUser->user_id; 853 my $MergedSet; 854 my $versioned = 0; 855 if ( defined( $mergedSets{"$uid!$setID"} ) ) { 856 $MergedSet = $mergedSets{"$uid!$setID"}; 857 $versioned = ($setID =~ /,v(\d+)$/) ? $1 : 0; 858 } else { 859 if ( $setID =~ /(.+),v(\d+)$/ ) { 860 $setID = $1; 861 $versioned = $2; 862 } 863 if ( $versioned ) { 864 $MergedSet = $db->getMergedSetVersion($TargetUser->user_id, $setID, $versioned); 865 } else { 866 $MergedSet = $db->getMergedSet($TargetUser->user_id, $setID); # checked 867 } 868 } 869 # save versioned info for use in write_problem_tex 870 $self->{versioned} = $versioned; 871 872 unless ($MergedSet) { 873 $self->add_errors("Can't generate hardcopy for set ''".CGI::code(CGI::escapeHTML($setID)) 874 ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id)) 875 ."' -- set is not assigned to that user."); 876 return; 877 } 878 879 # see if the *real* user is allowed to access this problem set 880 if ($MergedSet->open_date > time and not $authz->hasPermissions($userID, "view_unopened_sets")) { 881 $self->add_errors("Can't generate hardcopy for set '".CGI::code(CGI::escapeHTML($setID)) 882 ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id)) 883 ."' -- set is not yet open."); 884 return; 885 } 886 if (not $MergedSet->published and not $authz->hasPermissions($userID, "view_unpublished_sets")) { 887 $self->addbadmessage("Can't generate hardcopy for set '".CGI::code(CGI::escapeHTML($setID)) 888 ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id)) 889 ."' -- set has not been published."); 890 return; 891 } 892 893 # get snippets 894 my $header = $MergedSet->hardcopy_header 895 ? $MergedSet->hardcopy_header 896 : $ce->{webworkFiles}->{hardcopySnippets}->{setHeader}; 897 my $footer = $ce->{webworkFiles}->{hardcopySnippets}->{setFooter}; 898 my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{problemDivider}; 899 900 # get list of problem IDs 901 # DBFIXME use ORDER BY in database 902 my @problemIDs = sort { $a <=> $b } $db->listUserProblems($MergedSet->user_id, $MergedSet->set_id); 903 904 # for versioned sets (gateways), we might have problems in a random 905 # order; reset the order of the problemIDs if this is the case 906 if ( defined( $MergedSet->problem_randorder ) && 907 $MergedSet->problem_randorder ) { 908 my @newOrder = (); 909 910 # to set the same order each time we set the random seed to the psvn, 911 # and to avoid messing with the system random number generator we use 912 # our own PGrandom object 913 my $pgrand = PGrandom->new(); 914 $pgrand->srand( $MergedSet->psvn ); 915 while ( @problemIDs ) { 916 my $i = int($pgrand->rand(scalar(@problemIDs))); 917 push( @newOrder, $problemIDs[$i] ); 918 splice(@problemIDs, $i, 1); 919 } 920 @problemIDs = @newOrder; 921 } 922 923 924 # write set header 925 $self->write_problem_tex($FH, $TargetUser, $MergedSet, 0, $header); # 0 => pg file specified directly 926 927 # write each problem 928 # for versioned problem sets (gateway tests) we like to include 929 # problem numbers 930 my $i = 1; 931 while (my $problemID = shift @problemIDs) { 932 $self->write_tex_file($FH, $divider); 933 $self->{versioned} = $i if $versioned; 934 $self->write_problem_tex($FH, $TargetUser, $MergedSet, $problemID); 935 $i++; 936 } 937 938 # write footer 939 $self->write_problem_tex($FH, $TargetUser, $MergedSet, 0, $footer); # 0 => pg file specified directly 940 } 941 942 sub write_problem_tex { 943 my ($self, $FH, $TargetUser, $MergedSet, $problemID, $pgFile) = @_; 944 my $r = $self->r; 945 my $ce = $r->ce; 946 my $db = $r->db; 947 my $authz = $r->authz; 948 my $userID = $r->param("user"); 949 my $versioned = $self->{versioned}; 950 my %canShowScore = %{$self->{canShowScore}}; 951 952 my @errors; 953 954 # get problem record 955 my $MergedProblem; 956 if ($problemID) { 957 # a non-zero problem ID was given -- load that problem 958 # we use $versioned to determine which merging routine to use 959 if ( $versioned ) { 960 $MergedProblem = $db->getMergedProblemVersion($MergedSet->user_id, $MergedSet->set_id, $MergedSet->version_id, $problemID); 961 962 } else { 963 $MergedProblem = $db->getMergedProblem($MergedSet->user_id, $MergedSet->set_id, $problemID); # checked 964 } 965 966 # handle nonexistent problem 967 unless ($MergedProblem) { 968 $self->add_errors("Can't generate hardcopy for problem '" 969 .CGI::code(CGI::escapeHTML($problemID))."' in set '" 970 .CGI::code(CGI::escapeHTML($MergedSet->set_id)) 971 ."' for user '".CGI::code(CGI::escapeHTML($MergedSet->user_id)) 972 ."' -- problem does not exist in that set or is not assigned to that user."); 973 return; 974 } 975 } elsif ($pgFile) { 976 # otherwise, we try an explicit PG file 977 $MergedProblem = $db->newUserProblem( 978 user_id => $MergedSet->user_id, 979 set_id => $MergedSet->set_id, 980 problem_id => 0, 981 source_file => $pgFile, 982 ); 983 die "newUserProblem failed -- WTF?" unless $MergedProblem; # this should never happen 984 } else { 985 # this shouldn't happen -- error out for real 986 die "write_problem_tex needs either a non-zero \$problemID or a \$pgFile"; 987 } 988 989 # figure out if we're allowed to get correct answers, hints, and solutions 990 # (eventually, we'd like to be able to use the same code as Problem) 991 my $versionName = $MergedSet->set_id . 992 (( $versioned ) ? ",v" . $MergedSet->version_id : ''); 993 my $showCorrectAnswers = $r->param("showCorrectAnswers") || 0; 994 my $showHints = $r->param("showHints") || 0; 995 my $showSolutions = $r->param("showSolutions") || 0; 996 unless( ( $authz->hasPermissions($userID, "show_correct_answers_before_answer_date") or 997 ( time > $MergedSet->answer_date or 998 ( $versioned && 999 $MergedProblem->num_correct + 1000 $MergedProblem->num_incorrect >= 1001 $MergedSet->attempts_per_version && 1002 $MergedSet->due_date == $MergedSet->answer_date ) ) ) && 1003 ( $canShowScore{$MergedSet->user_id . "!$versionName"} ) ) { 1004 $showCorrectAnswers = 0; 1005 $showSolutions = 0; 1006 } 1007 1008 # FIXME -- there can be a problem if the $siteDefaults{timezone} is not defined? Why is this? 1009 # why does it only occur with hardcopy? 1010 1011 # we need an additional translation option for versioned sets; also, 1012 # for versioned sets include old answers in the set if we're also 1013 # asking for the answers 1014 my $transOpts = 1015 { # translation options 1016 displayMode => "tex", 1017 showHints => $showHints ? 1 : 0, # insure that this value is numeric 1018 showSolutions => $showSolutions ? 1 : 0, # (or what? -sam) 1019 processAnswers => $showCorrectAnswers ? 1 : 0, 1020 permissionLevel => $db->getPermissionLevel($userID)->permission, 1021 }; 1022 my $formFields = { }; 1023 if ( $versioned && $MergedProblem->problem_id != 0 ) { 1024 $transOpts->{QUIZ_PREFIX} = 'Q' . sprintf("%04d",$MergedProblem->problem_id()) . '_'; 1025 if ( $showCorrectAnswers ) { 1026 my %oldAnswers = decodeAnswers($MergedProblem->last_answer); 1027 $formFields->{$_} = $oldAnswers{$_} foreach (keys %oldAnswers); 1028 print $FH "%% decoded old answers, saved. (keys = " . join(',', keys(%oldAnswers)) . "\n"; 1029 } 1030 } 1031 1032 # warn("problem ", $MergedProblem->problem_id, ": source = ", $MergedProblem->source_file, "\n"); 1033 1034 my $pg = WeBWorK::PG->new( 1035 $ce, 1036 $TargetUser, 1037 scalar($r->param('key')), # avoid multiple-values problem 1038 $MergedSet, 1039 $MergedProblem, 1040 $MergedSet->psvn, 1041 $formFields, # no form fields! 1042 $transOpts, 1043 ); 1044 1045 # only bother to generate this info if there were warnings or errors 1046 my $edit_url; 1047 my $problem_name; 1048 my $problem_desc; 1049 if ($pg->{warnings} ne "" or $pg->{flags}->{error_flag}) { 1050 my $edit_urlpath = $r->urlpath->newFromModule( 1051 "WeBWorK::ContentGenerator::Instructor::PGProblemEditor", 1052 courseID => $r->urlpath->arg("courseID"), 1053 setID => $MergedProblem->set_id, 1054 problemID => $MergedProblem->problem_id, 1055 ); 1056 1057 if ($MergedProblem->problem_id == 0) { 1058 # link for an fake problem (like a header file) 1059 $edit_url = $self->systemLink($edit_urlpath, 1060 params => { 1061 sourceFilePath => $MergedProblem->source_file, 1062 problemSeed => $MergedProblem->problem_seed, 1063 }, 1064 ); 1065 } else { 1066 # link for a real problem 1067 $edit_url = $self->systemLink($edit_urlpath); 1068 } 1069 1070 if ($MergedProblem->problem_id == 0) { 1071 $problem_name = "snippet"; 1072 $problem_desc = $problem_name." '".$MergedProblem->source_file 1073 ."' for set '".$MergedProblem->set_id."' and user '" 1074 .$MergedProblem->user_id."'"; 1075 } else { 1076 $problem_name = "problem"; 1077 $problem_desc = $problem_name." '".$MergedProblem->problem_id 1078 ."' in set '".$MergedProblem->set_id."' for user '" 1079 .$MergedProblem->user_id."'"; 1080 } 1081 } 1082 1083 # deal with PG warnings 1084 if ($pg->{warnings} ne "") { 1085 $self->add_errors(CGI::a({href=>$edit_url, target=>"WW_Editor"}, "[edit]") 1086 ." Warnings encountered while processing $problem_desc. " 1087 ."Error text:".CGI::br().CGI::pre(CGI::escapeHTML($pg->{warnings})) 1088 ); 1089 } 1090 1091 # deal with PG errors 1092 if ($pg->{flags}->{error_flag}) { 1093 $self->add_errors(CGI::a({href=>$edit_url, target=>"WW_Editor"}, "[edit]") 1094 ." Errors encountered while processing $problem_desc. " 1095 ."This $problem_name has been omitted from the hardcopy. " 1096 ."Error text:".CGI::br().CGI::pre(CGI::escapeHTML($pg->{errors})) 1097 ); 1098 return; 1099 } 1100 1101 # if we got here, there were no errors (because errors cause a return above) 1102 $self->{at_least_one_problem_rendered_without_error} = 1; 1103 1104 print $FH "{\\bf Problem $versioned.}\n" 1105 if ( $versioned && $MergedProblem->problem_id != 0 ); 1106 print $FH $pg->{body_text}; 1107 1108 my @ans_entry_order = defined($pg->{flags}->{ANSWER_ENTRY_ORDER}) ? @{$pg->{flags}->{ANSWER_ENTRY_ORDER}} : ( ); 1109 1110 # write the list of student answers if we're working with a versioned 1111 # set and showing the correct answers 1112 if ( $versioned && $showCorrectAnswers && 1113 $MergedProblem->problem_id != 0 && @ans_entry_order ) { 1114 my $stuAnswers = "\\par{\\small{\\it Answer submitted:}\n" . 1115 "\\vspace{-\\parskip}\\begin{itemize}\n"; 1116 for my $ansName ( @ans_entry_order ) { 1117 my $stuAns = $pg->{answers}->{$ansName}->{original_student_ans}; 1118 my $recScore = $pg->{state}->{recorded_score}; 1119 my $corrMsg = ''; 1120 if ( $recScore == 1 ) { 1121 $corrMsg = ' (correct)'; 1122 } elsif ( $recScore == 0 ) { 1123 $corrMsg = ' (incorrect)'; 1124 } else { 1125 $corrMsg = " (score $recScore)"; 1126 } 1127 $stuAnswers .= "\\item\\begin{verbatim}$stuAns$corrMsg\\end{verbatim}\n"; 1128 } 1129 1130 $stuAnswers .= "\\end{itemize}}\\par\n"; 1131 print $FH $stuAnswers; 1132 } 1133 1134 # write the list of correct answers is appropriate; ANSWER_ENTRY_ORDER 1135 # isn't defined for versioned sets? this seems odd FIXME GWCHANGE 1136 if ($showCorrectAnswers && $MergedProblem->problem_id != 0 && @ans_entry_order) { 1137 my $correctTeX = "\\par{\\small{\\it Correct Answers:}\n" 1138 . "\\vspace{-\\parskip}\\begin{itemize}\n"; 1139 1140 foreach my $ansName (@ans_entry_order) { 1141 my $correctAnswer = $pg->{answers}->{$ansName}->{correct_ans}; 1142 $correctTeX .= "\\item\\begin{verbatim}$correctAnswer\\end{verbatim}\n"; 1143 # FIXME: What about vectors (where TeX will complain about < and > outside of math mode)? 1144 } 1145 1146 $correctTeX .= "\\end{itemize}}\\par\n"; 1147 1148 print $FH $correctTeX; 1149 } 1150 } 1151 1152 sub write_tex_file { 1153 my ($self, $FH, $file) = @_; 1154 1155 my $tex = eval { readFile($file) }; 1156 if ($@) { 1157 $self->add_errors("Failed to include TeX file '".CGI::code(CGI::escapeHTML($file))."': " 1158 .CGI::escapeHTML($@)); 1159 } else { 1160 print $FH $tex; 1161 } 1162 } 1163 1164 ################################################################################ 1165 # utilities 1166 ################################################################################ 1167 1168 sub add_errors { 1169 my ($self, @errors) = @_; 1170 push @{$self->{hardcopy_errors}}, @errors; 1171 } 1172 1173 sub get_errors { 1174 my ($self) = @_; 1175 return $self->{hardcopy_errors} ? @{$self->{hardcopy_errors}} : (); 1176 } 1177 1178 sub get_errors_ref { 1179 my ($self) = @_; 1180 return $self->{hardcopy_errors}; 1181 } 1182 1183 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |