diff --git a/.gitignore b/.gitignore index 50679cec15..d384242114 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ logs/* conf/*.conf conf/*.apache2-config htdocs/site_info.txt +bin/wwapache2ctl +webwork2.komodoproject #courses.dist/* diff --git a/bin/OPL-update b/bin/OPL-update index d8a0aedb01..b55f406195 100755 --- a/bin/OPL-update +++ b/bin/OPL-update @@ -1,4 +1,4 @@ -#!/usr/bin/perl +#!/usr/bin/env perl # This is the script formerly known as loadDB2, and then known as NPL-update. @@ -84,6 +84,7 @@ my $dbh = DBI->connect( my $passwd = $ce->{database_password}; my $user = $ce->{database_username}; my $libraryRoot = $ce->{problemLibrary}->{root}; +my $libraryVersion = $ce->{problemLibrary}->{version}; my $verbose = 0; my $cnt2 = 0; @@ -98,21 +99,14 @@ sub dbug { ##Figure out which set of tables to use my %tables; -if( -e "$libraryRoot/VERSION") { - require("$libraryRoot/VERSION"); - if($OPL_VERSION eq '2.5.0') { +if($libraryVersion eq '2.5') { %tables = %OPLtables; my $lib = 'OPL'; - print "Got OPLtables!\n"; - } else { - %tables = %NPLtables; - my $lib = 'NPL'; - print "Got NPLtables! (1)\n"; - } + warn "Library version is $libraryVersion; using OPLtables!\n"; } else { %tables = %NPLtables; my $lib = 'NPL'; - print "Got NPLtables! (2)\n"; + print "Library version is $libraryVersion; NPLtables! \n"; } @create_tables = ( @@ -422,6 +416,10 @@ sub pgfiles { my ($edition, $textauthor, $textsection, $textproblem, $tagged); %textinfo=(); my @textproblems = (-1); + if ($name =~ /swf$/) { + my $applet_file = basename($name); + symlink($name,$ce->{webworkDirs}->{htdocs}."/applets/".$applet_file); + } if ($name =~ /pg$/) { $pgfile = basename($name); $pgpath = dirname($name); diff --git a/bin/change_user_id b/bin/change_user_id new file mode 100755 index 0000000000..3cd0bde055 --- /dev/null +++ b/bin/change_user_id @@ -0,0 +1,111 @@ +#!/usr/bin/env perl +# +#Sometimes a webwork user id changes. This script transfers the webwork data for the old user_id to the new user_id +# Update database tables +#user +#permission +#password +#key +#set_user +#problem_user +#set_locations_user +#global_achievement_user +#achievement_user +# +# Update answer_log + +use strict; +use warnings; +use File::Copy; +use File::Basename; + +BEGIN { + die "WEBWORK_ROOT not found in environment.\n" + unless exists $ENV{WEBWORK_ROOT}; +} + +use lib "$ENV{WEBWORK_ROOT}/lib"; +use WeBWorK::CourseEnvironment; +use WeBWorK::DB; +use WeBWorK::Utils qw(runtime_use readFile cryptPassword); +use Data::Dumper; + +if((scalar(@ARGV) != 3)) { + print "\nSyntax is: change_user_id course_id old_user_id new_user_id"; + print "\n (e.g. newpassword MAT_123 jjones jsmith\n\n"; + exit(); +} + +my $courseID = shift; +my $old_user_id = shift; +my $new_user_id = shift; + +my $ce = WeBWorK::CourseEnvironment->new({ + webwork_dir => $ENV{WEBWORK_ROOT}, + courseName => $courseID +}); + +my $db = new WeBWorK::DB($ce->{dbLayout}); +die "Error: $old_user_id does not exist!" unless $db->existsUser($old_user_id); + +unless($db->existsUser($new_user_id)) { + my $user = $db->getUser($old_user_id); + $user->{user_id}=$new_user_id; + $user->{comment} = $user->{comment}."Record created from $old_user_id record"; + $db->addUser($user); +} + +unless($db->existsPassword($new_user_id)) { + my $password = $db->getPassword($old_user_id); + $password->{user_id} = $new_user_id; + $db->addPassword($password); +} + +unless($db->existsPermissionLevel($new_user_id)) { + my $permission = $db->getPermissionLevel($old_user_id); + $permission->{user_id} = $new_user_id; + $db->addPermissionLevel($permission); +} + + +my @old_user_sets = $db->listUserSets($old_user_id); +foreach(@old_user_sets) { + my $set_id = $_; + my $new_set = $db->newUserSet; + $new_set->user_id($new_user_id); + $new_set->set_id($set_id); + eval{$db->addUserSet($new_set)}; + my $old_set = $db->getUserSet($old_user_id,$set_id); + foreach(keys %$old_set) { + next if /user_id|set_id/; + $new_set->$_($old_set->$_); + } + + $db->putUserSet($new_set) unless $db->existsUserSet($new_user_id,$set_id); + my @global_problems = grep { defined $_} $db->getAllGlobalProblems($set_id); + foreach(@global_problems) { + if($db->existsUserProblem($old_user_id,$set_id,$_->{problem_id})) { + my $old_user_problem = $db->getUserProblem($old_user_id,$set_id,$_->{problem_id}); + my $new_user_problem = $db->newUserProblem; + $new_user_problem->user_id($new_user_id); + $new_user_problem->set_id($set_id); + $new_user_problem->problem_id($_->{problem_id}); + $db->addUserProblem($new_user_problem) unless $db->existsUserProblem($new_user_id,$set_id,$_->{problem_id}); + foreach(keys %$old_user_problem) { + next if /(user_id|set_id|problem_id)/; + $new_user_problem->$_($old_user_problem->$_); + } + $db->putUserProblem($new_user_problem); + } + } +} + +my $answer_log = $ce->{courseFiles}->{logs}->{answer_log}; +my $dirname = dirname($answer_log); +copy($answer_log,"$dirname/answer_log.bak"); +open(my $in,'<',"$dirname/answer_log.bak") or die "Can't open $dirname/answer_log.bak:$!"; +open(my $out,'>',$answer_log); +while(<$in>) { + s/$old_user_id/$new_user_id/g; + print $out $_; +} diff --git a/bin/check_modules.pl b/bin/check_modules.pl index fcca289b6b..c1b95fc31e 100755 --- a/bin/check_modules.pl +++ b/bin/check_modules.pl @@ -1,5 +1,5 @@ -#!/usr/bin/perl - +#!/usr/bin/env perl +# use strict; use warnings; @@ -84,6 +84,7 @@ Socket SQL::Abstract String::ShellQuote + Text::CSV Text::Wrap Tie::IxHash Time::HiRes diff --git a/conf/CONFIG-README b/conf/CONFIG-README deleted file mode 100644 index 47258870c7..0000000000 --- a/conf/CONFIG-README +++ /dev/null @@ -1,37 +0,0 @@ - -FIRST TIME RECONFIGURATION - -RENAME global.conf to global.save in order to make sure that the global.conf.dist is read. -Otherwise the global.conf file will be read instead of global.conf.dist. prelocal.conf and postlocal.conf -and the behavior will be the same as with the old system. - -COPY prelocal.conf.dist to prelocal.conf. -COPY postlocal.conf.dist to postlocal.conf. - -MODIFY prelocal.conf using the data from your global.conf file. -In particular you will need to fill in the server name, the -password for the database and any modifications you have made as to the -location of the temporary files directory. Notice that the location of the temporary files directory -is used to define several other related subdirectories, so this modification needs to be made -in prelocal.conf BEFORE the standard global.conf.dist file is read. - - -INSPECT and possibly modify postlocal.conf to add any further local modifications that you had -made to your global.conf file. - -Use the prelocal.conf and postlocal.conf files to make modifications. - -The prelocal.conf file is read before the global.conf.dist file is processed. Non-local variables in this file -are then available for use in global.conf.dist. - -The postlocal.conf file is read after the global.conf.dist file is processed and will overright configurations in global.conf.dist - -This new configuration system should greatly simplify the process of -updating webwork2 since it is less likely -that one will need to modify the config files when upgrading. Default configurations or permissions for -new features will be defined in global.conf.dist and will allow automatic upgrades. - -Overrides for these new features can be added later to postlocal.conf - - - diff --git a/conf/authen_CAS.conf.dist b/conf/authen_CAS.conf.dist new file mode 100644 index 0000000000..60f4c62e60 --- /dev/null +++ b/conf/authen_CAS.conf.dist @@ -0,0 +1,36 @@ +#!perl + +######################################################################################## +# authen_CAS.conf.dist +# Copy this file to authen_CAS.conf. Then configure it to match your server's CAS configuration. +# Then to activate add the following line to localOverrides.conf: +# include("conf/authen_CAS.conf") +######################################################################################## + +# Set CAS as the authentication module to use. +$authen{user_module} = { + "*" => "WeBWorK::Authen::CAS", +}; + +$authen{cas_options} = { + # Options to pass to the AuthCAS module. + # Note that this is (plain) AuthCAS, not Apache::AuthCAS + # or Apache2::AuthCAS. + # You need at least casUrl and CAFile; others can be set as well. + AuthCAS_opts => { + # URL of CAS server. Edit the host below. + casUrl => '', #e.g. 'https://auth.berkeley.edu/cas', + + # Path of certificate file for CAS server. + CAFile => '', #e.g. '/etc/pki/tls/certs/ca-bundle.crt', + }, + # There are no options specific to CAS at this time. If there were, + # though, they would go here. + + # For debugging: + #su_from => '8315', + su_to => '999999', +}; + + +1; #final line of the file to reassure perl that it was read properly. diff --git a/conf/defaults.config b/conf/defaults.config index 8529016d28..3afe7dcac6 100644 --- a/conf/defaults.config +++ b/conf/defaults.config @@ -261,8 +261,8 @@ $courseDirs{html_images} = "$courseDirs{html}/images"; # Location of web-accessible, course-specific temporary files, like static and # dynamically-generated PG graphics. -$courseDirs{html_temp} = "$courseDirs{html}/tmp"; -$courseURLs{html_temp} = "$courseURLs{html}/tmp"; +$courseDirs{html_temp} = "$webworkDirs{htdocs_temp}/$courseName"; +$courseURLs{html_temp} = "$webworkURLs{htdocs_temp}/$courseName"; # Location of course-specific logs, like the transaction log. $courseDirs{logs} = "$courseDirs{root}/logs"; @@ -938,9 +938,7 @@ ${pg}{modules} = [ [qw(Fraction)], [qw(Fun)], [qw(Hermite)], - [qw(Inequalities::common)], [qw(Label)], - [qw(LimitedPolynomial)], [qw(ChoiceList)], [qw(Match)], [qw(MatrixReal1)], # required by Matrix diff --git a/htdocs/themes/math2/codemirror2 b/htdocs/themes/math2/codemirror2 deleted file mode 160000 index 73edae2aaa..0000000000 --- a/htdocs/themes/math2/codemirror2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 73edae2aaabf867f45119804b7816a339762414e diff --git a/htdocs/themes/ubc/codemirror2 b/htdocs/themes/ubc/codemirror2 deleted file mode 160000 index 73edae2aaa..0000000000 --- a/htdocs/themes/ubc/codemirror2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 73edae2aaabf867f45119804b7816a339762414e diff --git a/lib/WeBWorK.pm b/lib/WeBWorK.pm index d7fc724447..759b8e540e 100644 --- a/lib/WeBWorK.pm +++ b/lib/WeBWorK.pm @@ -377,11 +377,6 @@ sub dispatch($) { writeTimingLogEntry($ce, "[".$r->uri."]", sprintf("runTime = %.3f sec", $cg_duration)." ".$ce->{dbLayoutName}, ""); debug("returning result: " . (defined $result ? $result : "UNDEF") . "\n"); - #@LimitedPolynomial::BOP::ISA; #FIXME this is needed to zero out - #@LimitedPolynomial::UOP::ISA; - #\@LimitedPolynomial::BOP::ISA and prevent error messages of the form - #[Sat May 15 14:23:08 2010] [warn] [client 127.0.0.1] [/webwork2/gage_course/test_set/6/] - #Can't locate package LimitedPolynomial::BOP for @LimitedPolynomial::BOP::add::ISA at /opt/webwork/webwork2/lib/Apache/WeBWorK.pm line 115., referer: http://localhost/webwork2/gage_course/test_set/6/ no one knows why return $result; } diff --git a/lib/WeBWorK/Authen/CAS.pm b/lib/WeBWorK/Authen/CAS.pm new file mode 100644 index 0000000000..c3ed38da15 --- /dev/null +++ b/lib/WeBWorK/Authen/CAS.pm @@ -0,0 +1,140 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2012 The WeBWorK Project, http://openwebwork.sf.net/ +# $CVSHeader: $ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. +################################################################################ + +package WeBWorK::Authen::CAS; +use base qw/WeBWorK::Authen/; + +use strict; +use warnings; +use AuthCAS; + +use WeBWorK::Debug; +#$WeBWorK::Debug::Enabled = 1; +#$WeBWorK::Debug::Logfile = "/opt/webwork/webwork2/logs/cas-debug.log"; +#$WeBWorK::Debug::AllowSubroutineOutput = "get_credentials"; + +sub get_credentials { + my ($self) = @_; + my $r = $self->{r}; + my $ce = $r->ce; + + # if we come in with a user_id, then we've already authenticated + # through the CAS. So just check the provided user and session key. + $self->{external_auth} = 1; + if (defined $r->param('key') && defined $r->param('user')) { + # These lines were copied from the superclass get_credentials. + $self->{session_key} = $r->param('key'); + $self->{user_id} = $r->param('user'); + $self->{login_type} = 'normal'; + $self->{credential_source} = 'params'; + debug("CAS params user '", $self->{user_id}, + "' key '", $self->{session_key}, "'"); + # Check session key and user here. Otherwise, a student can + # determine the enrollment status of any other student if + # they know the userid (which is public information at + # Berkeley). That would be a privacy violation. + my $Key = $r->db->getKey($self->{user_id}); + unless (defined $Key && $Key->key eq $self->{session_key}) { + debug('undefined or invalid session key: $Key->key = ', + defined $Key ? $Key->key : undef, ', user value = ', + $self->{session_key}); + $self->{error} = "Invalid session key"; + return 0; + } + return 1; + #debug("falling back to superclass get_credentials"); + #return $self->SUPER::get_credentials( @_ ); + } else { + #my $cas_url = $ce->{authen}{cas_options}{url}; + #my $cas_certs = $ce->{authen}{cas_options}{certs}; + #my $cas = new AuthCAS(casUrl => $cas_url, + # CAFile => $cas_certs); + my $cas = new AuthCAS( + %{ $ce->{authen}{cas_options}{AuthCAS_opts} }); + + my $service = $r->unparsed_uri(); + # Remove the "ticket=..." parameter that the CAS server added + # (Not sure if the second test is really needed.) + $service =~ s/[?&]ticket=[^&]*$// + or $service =~ s/([?&])ticket=[^&]*&/$1/; + $service = $ce->{apache_root_url} . $service; + debug("service = $service"); + my $ticket = $r->param('ticket'); + unless (defined $ticket) { + # there's no ticket, so redirect to get one + # + my $go_to = $cas->getServerLoginURL($service); + #$go_to = 'http://math.berkeley.edu/'; # for debugging + debug("no ticket. Redirecting to $go_to"); + $self->{redirect} = $go_to; + return 0; + } + # We have a ticket. Validate it. + my $user_id = $cas->validateST($service, $ticket); + if (!defined $user_id) { + my $err = $cas->get_errors(); + $err = '' unless defined $err; + $self->{error} = $err; + debug("ticket error $err"); + #return $self->SUPER::get_credentials( @_ ); + return 0; + } else { + debug("ticket is good, user is $user_id"); + if (defined $ce->{authen}{cas_options}{su_from} + && $user_id eq $ce->{authen}{cas_options}{su_from} + && defined $ce->{authen}{cas_options}{su_to}) { + $user_id = $ce->{authen}{cas_options}{su_to}; + debug("hackily changing user to $user_id"); + } + $self->{'user_id'} = $user_id; + $self->{r}->param('user', $user_id); + $self->{session_key} = undef; + $self->{password} = "not\tvalid"; + $self->{login_type} = 'normal'; + $self->{credential_source} = 'cas'; + return 1; + } + } +} + +# There's no need to provide site_checkPassword, since it's only accessed +# from checkPassword, which we're replacing. + +sub checkPassword { + my ( $self, $userID, $clearTextPassword ) = @_; + # if we got here, we know we've already successfully authenticated + # against the CAS + return 1; +} + +# Handle logout by redirecting to the relevant CAS url. + +sub logout_user { + my ($self) = @_; + + my $ce = $self->{r}->ce; + + # Using AuthCAS::getServerLogoutURL($service) would be overkill, + # and (more important) it would send us back here after logging out, + # so we'd end up back at the CAS login screen. + + my $go_to = $ce->{authen}{cas_options}{AuthCAS_opts}{casUrl} + . '/logout'; + debug("logging out. Redirecting to $go_to"); + $self->{redirect} = $go_to; +} + +1; diff --git a/lib/WeBWorK/ContentGenerator/Login.pm b/lib/WeBWorK/ContentGenerator/Login.pm index 80ea73c2a1..851913e228 100644 --- a/lib/WeBWorK/ContentGenerator/Login.pm +++ b/lib/WeBWorK/ContentGenerator/Login.pm @@ -117,6 +117,16 @@ sub links { return( @return); } +sub pre_header_initialize { + my ($self) = @_; + my $authen = $self->r->authen; + + if ( defined($authen->{redirect}) && $authen->{redirect} ) { + $self->reply_with_redirect($authen->{redirect}); + } +} + + sub body { my ($self) = @_; my $r = $self->r; diff --git a/lib/WeBWorK/ContentGenerator/Logout.pm b/lib/WeBWorK/ContentGenerator/Logout.pm index 4ea4cde833..643deed077 100644 --- a/lib/WeBWorK/ContentGenerator/Logout.pm +++ b/lib/WeBWorK/ContentGenerator/Logout.pm @@ -82,6 +82,9 @@ sub pre_header_initialize { } $self->{keyError} = $keyError; + # Do any special processing needed by external authentication + $authen->logout_user() if $authen->can('logout_user'); + # if we have an authen redirect, all of those errors may be # moot, but I think that's unavoidable (-glarose) if ( defined($authen->{redirect}) && $authen->{redirect} ) { diff --git a/lib/WeBWorK/ContentGenerator/Problem.pm b/lib/WeBWorK/ContentGenerator/Problem.pm index cc3f749922..05dd0c6554 100644 --- a/lib/WeBWorK/ContentGenerator/Problem.pm +++ b/lib/WeBWorK/ContentGenerator/Problem.pm @@ -472,6 +472,13 @@ sub previewCorrectAnswer { # Template escape implementations ################################################################################ +sub content { + my $self = shift; + my $result = $self->SUPER::content(@_); + $self->{pg}->free if $self->{pg}; # be sure to clean up PG environment when the page is done + return $result; +} + sub pre_header_initialize { my ($self) = @_; my $r = $self->r; diff --git a/lib/WeBWorK/DB.pm b/lib/WeBWorK/DB.pm index d118257e79..7e44575519 100644 --- a/lib/WeBWorK/DB.pm +++ b/lib/WeBWorK/DB.pm @@ -779,10 +779,10 @@ sub addKey { my ($userID, $proctorID) = ($1, $2); croak "addKey: user $userID not found" # unless $self->{user}->exists($userID); - unless $Key -> key eq "nonce" or $self->{user}->exists($Key->user_id); + unless $Key -> key eq "nonce" or $self->{user}->exists($userID); croak "addKey: proctor $proctorID not found" # unless $self->{user}->exists($proctorID); - unless $Key -> key eq "nonce" or $self->{user}->exists($Key->user_id); + unless $Key -> key eq "nonce" or $self->{user}->exists($proctorID); } else { croak "addKey: user ", $Key->user_id, " not found" # unless $self->{user}->exists($Key->user_id); diff --git a/lib/WeBWorK/File/Classlist.pm b/lib/WeBWorK/File/Classlist.pm index cd6312f085..0f28ba7904 100644 --- a/lib/WeBWorK/File/Classlist.pm +++ b/lib/WeBWorK/File/Classlist.pm @@ -26,6 +26,7 @@ WeBWorK::File::Classlist - parse and write classlist files. use strict; use warnings; use IO::File; +use Text::CSV; our $MIN_FIELDS = 9; our $MAX_FIELDS = 11; @@ -38,10 +39,13 @@ our @EXPORT = qw/parse_classlist write_classlist/; sub parse_classlist($) { my ($file) = @_; + use open qw( :encoding(UTF-8) :std ); # assume classlist is utf8 encoded my $fh = new IO::File($file, "<") or die "Failed to open classlist '$file' for reading: $!\n"; my (@records); + + my $csv = Text::CSV->new({ binary => 1 }); # binary for utf8 compat while (<$fh>) { chomp; @@ -50,7 +54,12 @@ sub parse_classlist($) { s/^\s*//; s/\s*$//; - my @fields = split /\s*,\s*/, $_, -1; # -1 == don't delete empty trailing fields + if (!$csv->parse($_)) { + warn "Unable to parse line $. of classlist '$file' as CSV."; + next; + } + my @fields = $csv->fields; + my $fields = @fields; if ($fields < $MIN_FIELDS) { warn "Skipped invalid line $. of classlist '$file': expected at least $MIN_FIELDS fields, got $fields fields.\n"; diff --git a/lib/WeBWorK/PG.pm b/lib/WeBWorK/PG.pm index dffa5107c1..651518ffe2 100644 --- a/lib/WeBWorK/PG.pm +++ b/lib/WeBWorK/PG.pm @@ -53,6 +53,19 @@ sub new { return $renderer->new(@_); } +sub free { + my $self = shift; + # + # If certain MathObjects (e.g. LimitedPolynomials) are left in the PG structure, then + # freeing them later can cause "Can't locate package ..." errors in the log during + # perl garbage collection. So free them here. + # + $self->{pgcore}{OUTPUT_ARRAY} = []; + $self->{answers} = {}; + undef $self->{translator}; + foreach (keys %{$self->{pgcore}{PG_ANSWERS_HASH}}) {undef $self->{pgcore}{PG_ANSWERS_HASH}{$_}} +} + sub defineProblemEnvir { my ( $self,