[system] / branches / rel-2-0-pr1-hardcopy-changes / webwork2 / lib / WeBWorK / ContentGenerator / Hardcopy.pm Repository:
ViewVC logotype

View of /branches/rel-2-0-pr1-hardcopy-changes/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 736 - (download) (as text) (annotate)
Tue Feb 18 06:51:49 2003 UTC (10 years, 3 months ago) by sh002i
File size: 17080 byte(s)
fixed some security checks in Hardcopy.
-sam

    1 ################################################################################
    2 # WeBWorK mod_perl (c) 2000-2002 WeBWorK Project
    3 # $Id$
    4 ################################################################################
    5 
    6 package WeBWorK::ContentGenerator::Hardcopy;
    7 
    8 =head1 NAME
    9 
   10 WeBWorK::ContentGenerator::Hardcopy - generate a PDF version of one or more
   11 problem sets.
   12 
   13 =cut
   14 
   15 use strict;
   16 use warnings;
   17 use base qw(WeBWorK::ContentGenerator);
   18 use CGI qw();
   19 use File::Path qw(rmtree);
   20 use File::Temp qw(tempdir);
   21 use WeBWorK::DB::Classlist;
   22 use WeBWorK::DB::WW;
   23 use WeBWorK::Form;
   24 use WeBWorK::Utils qw(readFile);
   25 
   26 sub go {
   27   my ($self, $singleSet) = @_;
   28 
   29   my $r = $self->{r};
   30   my $ce = $self->{courseEnvironment};
   31   my @sets = $r->param("hcSet");
   32   my @users = $r->param("hcUser");
   33 
   34   # add singleSet to the list of sets
   35   if (length $singleSet > 0) {
   36     $singleSet =~ s/^set//;
   37     unshift @sets, $singleSet unless grep { $_ eq $singleSet } @sets;
   38   }
   39 
   40   # default user is the effectiveUser
   41   unless (@users) {
   42     unshift @users, $r->param("effectiveUser");
   43   }
   44 
   45   $self->{cldb}   = WeBWorK::DB::Classlist->new($ce);
   46   $self->{authdb} = WeBWorK::DB::Auth->new($ce);
   47   $self->{wwdb}   = WeBWorK::DB::WW->new($ce);
   48   $self->{user}            = $self->{cldb}->getUser($r->param("user"));
   49   $self->{permissionLevel} = $self->{authdb}->getPermissions($r->param("user"));
   50   $self->{effectiveUser}   = $self->{cldb}->getUser($r->param("effectiveUser"));
   51   $self->{sets}  = \@sets;
   52   $self->{users} = \@users;
   53   $self->{errors}   = [];
   54   $self->{warnings} = [];
   55 
   56   # security checks - these have to be put somewhere
   57   my $multiSet = $self->{permissionLevel} > 0;
   58   my $multiUser = $self->{permissionLevel} > 0;
   59   if (@sets > 1 and not $multiSet) {
   60     $self->{generationError} = ["SIMPLE", "You are not permitted to generate hardcopy for multiple sets. Please select a single set and try again."];
   61   }
   62   if (@users > 1 and not $multiUser) {
   63     $self->{generationError} = ["SIMPLE", "You are not permitted to generate hardcopy for multiple users. Please select a single user and try again."];
   64   }
   65   if ($users[0] ne $self->{effectiveUser}->id and not $multiUser) {
   66     $self->{generationError} = ["SIMPLE", "You are not permitted to generate hardcopy for other users."];
   67   }
   68 
   69   unless ($self->{generationError}) {
   70     if ($r->param("generateHardcopy")) {
   71       my ($tempDir, $fileName) = eval { $self->generateHardcopy() };
   72       if ($@) {
   73         $self->{generationError} = $@;
   74       } else {
   75         my $filePath = "$tempDir/$fileName";
   76 
   77         $r->content_type("application/x-pdf");
   78         # as per RFC2183:
   79         $r->header_out("Content-Disposition", "attachment; filename=$fileName");
   80         $r->send_http_header();
   81 
   82         local *INPUTFILE;
   83         open INPUTFILE, "<", $filePath
   84           or die "Failed to read $filePath: $!";
   85         my $buf;
   86         while (read INPUTFILE, $buf, 16384) {
   87           print $buf;
   88         }
   89         close INPUTFILE;
   90 
   91         return;
   92       }
   93     }
   94   }
   95 
   96   $r->content_type("text/html");
   97   $r->send_http_header();
   98   $self->template($ce->{templates}->{system}, $singleSet);
   99 }
  100 
  101 # -----
  102 
  103 sub path {
  104   my ($self, undef, $args) = @_;
  105 
  106   my $ce = $self->{courseEnvironment};
  107   my $root = $ce->{webworkURLs}->{root};
  108   my $courseName = $ce->{courseName};
  109   return $self->pathMacro($args,
  110     "Home" => "$root",
  111     $courseName => "$root/$courseName",
  112     "Hardcopy Generator" => "",
  113   );
  114 }
  115 
  116 sub title {
  117   return "Hardcopy Generator";
  118 }
  119 
  120 sub body {
  121   my $self = shift;
  122 
  123   if ($self->{generationError}) {
  124     if (ref $self->{generationError} eq "ARRAY") {
  125       my ($disposition, @rest) = @{$self->{generationError}};
  126       if ($disposition eq "PGFAIL") {
  127         print $self->multiErrorOutput(@{$self->{errors}});
  128         return "";
  129       } elsif ($disposition eq "FAIL") {
  130         print $self->errorOutput(@rest);
  131         return "";
  132       } elsif ($disposition eq "RETRY") {
  133         print $self->errorOutput(@rest);
  134       } else { # a "simple" error
  135         print CGI::p(CGI::font({-color=>"red"}, @rest));
  136       }
  137     } else {
  138       # not something we were expecting...
  139       die $self->{generationError};
  140     }
  141   }
  142   $self->displayForm();
  143 }
  144 
  145 sub multiErrorOutput($@) {
  146   my ($self, @errors) = @_;
  147 
  148   print CGI::h2("Software Errors");
  149   print CGI::p(<<EOF);
  150 WeBWorK has encountered one or more software errors while attempting to process these sets.
  151 It is likely that there are error(s) in the problem itself.
  152 If you are a student, contact your professor to have the error(s) corrected.
  153 If you are a professor, please consut the error output below for more informaiton.
  154 EOF
  155   foreach my $error (@errors) {
  156     print CGI::h3("Set: ", $error->{set}, ", Problem: ", $error->{problem});
  157     print CGI::h4("Error messages"), CGI::blockquote(CGI::pre($error->{message}));
  158     print CGI::h4("Error context"), CGI::blockquote(CGI::pre($error->{context}));
  159   }
  160 }
  161 
  162 # -----
  163 
  164 sub displayForm($) {
  165   my $self = shift;
  166   my $r = $self->{r};
  167 
  168   print CGI::start_p(), "Select the problem sets for which to generate hardcopy versions.";
  169   if ($self->{permissionLevel} > 0) {
  170     print "You may also select multiple users from the users list. You will receive hardcopy for each (set, user) pair.";
  171   }
  172   print CGI::end_p();
  173 
  174   print CGI::start_form(-method=>"POST", -action=>$r->uri);
  175   print $self->hidden_authen_fields();
  176   print CGI::start_table({-width=>"100%"}), CGI::start_Tr({-valign=>"top"});
  177 
  178   my $multiSet = $self->{permissionLevel} > 0;
  179   my $multiUser = $self->{permissionLevel} > 0;
  180 
  181   # set selection menu
  182   {
  183     print CGI::start_td();
  184     print CGI::h3("Sets");
  185     print CGI::start_table();
  186     my @sets;
  187     push @sets, $self->{wwdb}->getSet($self->{effectiveUser}->id, $_)
  188       foreach ($self->{wwdb}->getSets($self->{effectiveUser}->id));
  189     @sets = sort { $a->id cmp $b->id } @sets;
  190     foreach my $set (@sets) {
  191       my $checked = grep { $_ eq $set->id } @{$self->{sets}};
  192       my $control;
  193       if (time < $set->open_date) {
  194         $control = "";
  195       } else {
  196         if ($multiSet) {
  197           $control = CGI::checkbox(
  198             -name=>"hcSet",
  199             -value=>$set->id,
  200             -label=>"",
  201             -checked=>$checked
  202           );
  203         } else {
  204           $control = CGI::radio_group(
  205             -name=>"hcSet",
  206             -values=>[$set->id],
  207             -default=>($checked ? $set->id : "-"),
  208             -labels=>{$set->id => ""}
  209           );
  210         }
  211       }
  212       print CGI::Tr(CGI::td([
  213         $control,
  214         $set->id,
  215       ]));
  216     }
  217     print CGI::end_table();
  218     print CGI::end_td();
  219   }
  220 
  221   # user selection menu
  222   if ($multiUser) {
  223     print CGI::start_td();
  224     print CGI::h3("Users");
  225     print CGI::start_table();
  226     #print CGI::Tr(
  227     # CGI::td(CGI::checkbox(-name=>"hcAllUsers", -value=>"1", -label=>"")),
  228     # CGI::td({-colspan=>"2"}, "All Users"),
  229     #);
  230     #print CGI::Tr(CGI::td({-colspan=>"3"}, "&nbsp;"));
  231     my @users;
  232     push @users, $self->{cldb}->getUser($_)
  233       foreach ($self->{cldb}->getUsers());
  234     @users = sort { $a->last_name cmp $b->last_name } @users;
  235     foreach my $user (@users) {
  236       my $checked = grep { $_ eq $user->id } @{$self->{users}};
  237       print CGI::Tr(CGI::td([
  238         CGI::checkbox(-name=>"hcUser", -value=>$user->id, -label=>"", -checked=>$checked),
  239         $user->id,
  240         $user->last_name.", ".$user->first_name,
  241       ]));
  242     }
  243     print CGI::end_table();
  244     print CGI::end_td();
  245   }
  246 
  247   print CGI::end_Tr(), CGI::end_table();
  248   print CGI::p({-align=>"center"},
  249     CGI::submit(-name=>"generateHardcopy", -label=>"Generate Hardcopy"));
  250   print CGI::end_form();
  251 
  252   return "";
  253 }
  254 
  255 sub generateHardcopy($) {
  256   my $self = shift;
  257   my @sets = @{$self->{sets}};
  258   my @users = @{$self->{users}};
  259   my $multiSet = $self->{permissionLevel} > 0;
  260   my $multiUser = $self->{permissionLevel} > 0;
  261   # sanity checks
  262   unless (@sets) {
  263     die ["RETRY", "No sets were specified."];
  264   }
  265   unless (@users) {
  266     die ["RETRY", "No users were specified."];
  267   }
  268 
  269   # determine where hardcopy is going to go
  270   #my $tempDir = $self->{courseEnvironment}->{courseDirs}->{html_temp} . "/hardcopy";
  271   my $tempDir = tempdir("webwork-hardcopy-XXXXXXXX", TMPDIR => 1);
  272 
  273   # make sure tempDir exists
  274   #unless (-e $tempDir) {
  275   # if (system "mkdir", "-p", $tempDir) {
  276   #   die ["FAIL", "Failed to mkdir $tempDir", $!];
  277   # }
  278   #}
  279 
  280   # determine name of PDF file
  281   my $courseName = $self->{courseEnvironment}->{courseName};
  282   my $fileNameSet = (@sets > 1 ? "multiset" : $sets[0]);
  283   my $fileNameUser = (@users > 1 ? "multiuser" : $users[0]);
  284   my $fileName = "$courseName.$fileNameUser.$fileNameSet.pdf";
  285 
  286   # for each user ... generate TeX for each set
  287   my $tex;
  288   foreach my $user (@users) {
  289     $tex .= $self->getMultiSetTeX(@sets);
  290   }
  291 
  292   # deal with PG errors
  293   if (@{$self->{errors}}) {
  294     die ["PGFAIL"];
  295   }
  296 
  297   # "try" to generate pdf
  298   eval { $self->latex2pdf($tex, $tempDir, $fileName) };
  299   if ($@) {
  300     die ["FAIL", "Failed to generate PDF from tex", $@];
  301   }
  302 
  303   return $tempDir, $fileName;
  304 }
  305 
  306 # -----
  307 
  308 sub latex2pdf {
  309   # this is a little ad-hoc function which I will replace with a LaTeX
  310   # module at some point (or put it in Utils).
  311   my ($self, $tex, $fileBase, $fileName) = @_;
  312   my $finalFile = "$fileBase/$fileName";
  313   my $ce = $self->{courseEnvironment};
  314 
  315   # create a temporary directory for tex to shit in
  316   my $wd = tempdir("webwork-hardcopy-XXXXXXXX", TMPDIR => 1);
  317   my $texFile = "$wd/hardcopy.tex";
  318   my $pdfFile = "$wd/hardcopy.pdf";
  319   my $logFile = "$wd/hardcopy.log";
  320 
  321   # write the tex file
  322   local *TEX;
  323   open TEX, ">", $texFile or die "Failed to open $texFile: $!\n";
  324   print TEX $tex;
  325   close TEX;
  326 
  327   # call pdflatex - we don't want to chdir in the mod_perl process, as
  328   # that might step on the feet of other things (esp. in Apache 2.0)
  329   my $pdflatex = $ce->{externalPrograms}->{pdflatex};
  330   system "cd $wd && $pdflatex $texFile" and die "Failed to call pdflatex: $!\n";
  331 
  332   if (-e $pdfFile) {
  333     # move resulting PDF file to appropriate location
  334     system "/bin/mv", $pdfFile, $finalFile and die "Failed to mv: $!\n";
  335   }
  336 
  337   # remove temporary directory
  338   rmtree($wd, 0, 1);
  339 
  340   -e $finalFile or die "Failed to create $finalFile for no apparent reason.\n";
  341 }
  342 
  343 # -----
  344 
  345 sub texBlockComment(@) { return "\n".("%"x80)."\n%% ".join("", @_)."\n".("%"x80)."\n\n"; }
  346 
  347 sub getMultiSetTeX {
  348   my ($self, @sets) = @_;
  349   my $ce = $self->{courseEnvironment};
  350   my $tex = "";
  351 
  352   # the document preamble
  353   $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{preamble});
  354 
  355   while (defined (my $setName = shift @sets)) {
  356     $tex .= $self->getSetTeX($setName);
  357     if (@sets) {
  358       # divide sets, but not after the last set
  359       $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{setDivider});
  360     }
  361   }
  362 
  363   # the document postamble
  364   $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{postamble});
  365 
  366   return $tex;
  367 }
  368 
  369 sub getSetTeX {
  370   my ($self, $setName) = @_;
  371   my $ce = $self->{courseEnvironment};
  372   my $wwdb = $self->{wwdb};
  373   my $effectiveUserName = $self->{effectiveUser}->id;
  374   my @problemNumbers = sort { $a <=> $b } $wwdb->getProblems($effectiveUserName, $setName);
  375 
  376   # get header and footer
  377   my $setHeader = $wwdb->getSet($effectiveUserName, $setName)->set_header
  378     || $ce->{webworkFiles}->{hardcopySnippets}->{setHeader};
  379   # database doesn't support the following yet :(
  380   #my $setFooter = $wwdb->getSet($effectiveUserName, $setName)->set_footer
  381   # || $ce->{webworkFiles}->{hardcopySnippets}->{setFooter};
  382   # so we don't allow per-set customization, which is probably okay :)
  383   my $setFooter = $ce->{webworkFiles}->{hardcopySnippets}->{setFooter};
  384 
  385   my $tex = "";
  386 
  387   # render header
  388   $tex .= texBlockComment("BEGIN $setName : $setHeader");
  389   $tex .= $self->getProblemTeX($setName, 0, $setHeader);
  390 
  391   # render each problem
  392   while (my $problemNumber = shift @problemNumbers) {
  393     $tex .= texBlockComment("BEGIN $setName : $problemNumber");
  394     $tex .= $self->getProblemTeX($setName, $problemNumber);
  395     if (@problemNumbers) {
  396       # divide problems, but not after the last problem
  397       $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{problemDivider});
  398     }
  399   }
  400 
  401   # render footer
  402   $tex .= texBlockComment("BEGIN $setName : $setFooter");
  403   $tex .= $self->getProblemTeX($setName, 0, $setFooter);
  404 
  405   return $tex;
  406 }
  407 
  408 sub getProblemTeX {
  409   my ($self, $setName, $problemNumber, $pgFile) = @_;
  410   my $r = $self->{r};
  411   my $ce = $self->{courseEnvironment};
  412 
  413   my $wwdb = $self->{wwdb};
  414   my $cldb = $self->{cldb};
  415   my $effectiveUser = $self->{effectiveUser};
  416   my $set  = $wwdb->getSet($effectiveUser->id, $setName);
  417   my $psvn = $wwdb->getPSVN($effectiveUser->id, $setName);
  418 
  419   # decide what to do about problem number
  420   my $problem;
  421   if ($problemNumber) {
  422     $problem = $wwdb->getProblem($effectiveUser->id, $setName, $problemNumber);
  423   } elsif ($pgFile) {
  424     $problem = WeBWorK::Problem->new(
  425       id => 0,
  426       set_id => $set->id,
  427       login_id => $effectiveUser->id,
  428       source_file => $pgFile,
  429       # the rest of Problem's fields are not needed, i think
  430     );
  431   }
  432 
  433   my $pg = WeBWorK::PG->new(
  434     $ce,
  435     $effectiveUser,
  436     $r->param('key'),
  437     $set,
  438     $problem,
  439     $psvn,
  440     {}, # no form fields!
  441     { # translation options
  442       displayMode     => "tex",
  443       showHints       => 0,
  444       showSolutions   => 0,
  445       processAnswers  => 0,
  446     },
  447   );
  448 
  449   if ($pg->{warnings} ne "") {
  450     push @{$self->{warnings}}, {
  451       set     => $setName,
  452       problem => $problemNumber,
  453       message => $pg->{warnings},
  454     };
  455   }
  456 
  457   if ($pg->{flags}->{error_flag}) {
  458     push @{$self->{errors}}, {
  459       set     => $setName,
  460       problem => $problemNumber,
  461       message => $pg->{errors},
  462       context => $pg->{body_text},
  463     };
  464     # if there was an error, body_text contains
  465     # the error context, not TeX code
  466     $pg->{body_text} = undef;
  467   }
  468 
  469   return $pg->{body_text};
  470 }
  471 
  472 sub texInclude {
  473   my ($self, $texFile) = @_;
  474   my $tex = "";
  475 
  476   $tex .= texBlockComment("BEGIN: $texFile");
  477   eval {
  478     $tex .= readFile($texFile)
  479   };
  480   if ($@) {
  481     $tex .= texBlockComment($@);
  482   }
  483 
  484   return $tex;
  485 }
  486 
  487 1;
  488 
  489 __END__
  490 
  491 sub body {
  492   my $self = shift;
  493 
  494   STUFF: {
  495     my $courseName = $self->{courseEnvironment}->{courseName};
  496     my $effectiveUserName = $self->{r}->param("effectiveUser");
  497     my @sets = @{$self->{sets}};
  498 
  499     unless (@sets) {
  500       print CGI::p("No problem sets were specified.");
  501       last STUFF;
  502     }
  503 
  504     # determine where hardcopy is going to go
  505     my $tempDir = $self->{courseEnvironment}->{courseDirs}->{html_temp}
  506       . "/hardcopy";
  507     my $tempURL = $self->{courseEnvironment}->{courseURLs}->{html_temp}
  508       . "/hardcopy";
  509 
  510     # make sure tempDir exists
  511     unless (-e $tempDir) {
  512       if (system "mkdir", "-p", $tempDir) {
  513         print CGI::p("An error occured while trying to generate your PDF hardcopy:");
  514         print CGI::blockquote(CGI::pre("Failed to mkdir $tempDir: $!\n"));
  515       }
  516     }
  517 
  518     # determine name of PDF file
  519     my $fileName;
  520     if (@sets > 1) {
  521       # multiset output
  522       $fileName = "$courseName.$effectiveUserName.multiset.pdf"
  523     } elsif (@sets == 1) {
  524       # only one set
  525       my $setName = $sets[0];
  526       $fileName = "$courseName.$effectiveUserName.$setName.pdf";
  527     } else {
  528       $fileName = "$courseName.$effectiveUserName.pdf";
  529     }
  530 
  531     # determine full URL
  532     my $fullURL = "$tempURL/$fileName";
  533 
  534     # generate TeX from sets
  535     my $tex = $self->getMultiSetTeX(@sets);
  536     #print CGI::pre($tex);
  537 
  538     # check for PG errors (fatal)
  539     if (@{$self->{errors}}) {
  540       my @errors = @{$self->{errors}};
  541       print CGI::h2("Software Errors");
  542       print CGI::p(<<EOF);
  543 WeBWorK has encountered one or more software errors while attempting to process these sets.
  544 It is likely that there are error(s) in the problem itself.
  545 If you are a student, contact your professor to have the error(s) corrected.
  546 If you are a professor, please consut the error output below for more informaiton.
  547 EOF
  548       foreach my $error (@errors) {
  549         print CGI::h3("Set: ", $error->{set}, ", Problem: ", $error->{problem});
  550         print CGI::h4("Error messages"), CGI::blockquote(CGI::pre($error->{message}));
  551         print CGI::h4("Error context"), CGI::blockquote(CGI::pre($error->{context}));
  552       }
  553 
  554       last STUFF;
  555     }
  556 
  557     # "try" to generate hardcopy
  558     eval { $self->latex2pdf($tex, $tempDir, $fileName) };
  559     if ($@) {
  560       print CGI::p("An error occured while trying to generate your PDF hardcopy:");
  561       print CGI::blockquote(CGI::pre($@));
  562       last STUFF;
  563     } else {
  564       print CGI::p({-align=>"center"},
  565         CGI::big(CGI::a({-href=>$fullURL}, "Download PDF Hardcopy"))
  566       );
  567     }
  568 
  569     # check for PG warnings (non-fatal)
  570     if (@{$self->{warnings}}) {
  571       my @warnings = @{$self->{warnings}};
  572       print CGI::h2("Software Warnings");
  573       print CGI::p(<<EOF);
  574 WeBWorK has encountered warnings while attempting to process these sets.
  575 It is likely that this indicates an error or ambiguity in the problem(s) themselves.
  576 If you are a student, contact your professor to have the problem(s) corrected.
  577 If you are a professor, please consut the error output below for more informaiton.
  578 EOF
  579       foreach my $warning (@warnings) {
  580         print CGI::h3("Set: ", $warning->{set}, ", Problem: ", $warning->{problem});
  581         print CGI::h4("Warning messages"), CGI::blockquote(CGI::pre($warning->{message}));
  582       }
  583     }
  584   }
  585 
  586   # feedback form
  587   my $ce = $self->{courseEnvironment};
  588   my $root = $ce->{webworkURLs}->{root};
  589   my $courseName = $ce->{courseName};
  590   my $feedbackURL = "$root/$courseName/feedback/";
  591   print
  592     CGI::startform("POST", $feedbackURL),
  593     $self->hidden_authen_fields,
  594     CGI::hidden("module", __PACKAGE__),
  595     CGI::p({-align=>"right"},
  596       CGI::submit(-name=>"feedbackForm", -label=>"Send Feedback")
  597     ),
  598     CGI::endform();
  599 
  600   return "";
  601 }

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9