Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion lib/WeBWorK/ContentGenerator/CourseAdmin.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,6 @@ sub do_unarchive_course ($c) {

unarchiveCourse(
newCourseID => $new_courseID,
oldCourseID => $unarchive_courseID =~ s/\.tar\.gz$//r,
archivePath => "$ce->{webworkDirs}{courses}/$ce->{admin_course_id}/archives/$unarchive_courseID",
ce => $ce,
);
Expand Down
56 changes: 35 additions & 21 deletions lib/WeBWorK/Utils/CourseManagement.pm
Original file line number Diff line number Diff line change
Expand Up @@ -920,20 +920,20 @@ sub _archiveCourse_remove_dump_dir {

%options must contain:

oldCourseID => $oldCourseID,
archivePath => $archivePath,
ce => $ce,

%options may also contain:

newCourseID => $newCourseID,

Restores course $oldCourseID from a gzipped tar archive (.tar.gz) located at
$archivePath. After unarchiving, the course database is restored from a
subdirectory of the course's DATA directory.
Restores the course contained in the gzipped tar archive (.tar.gz) located at
$archivePath. The ID of the course being restored is taken from the archive's
top-level directory, not from the file name. After unarchiving, the course
database is restored from a subdirectory of the course's DATA directory.

If $newCourseID is defined and differs from $oldCourseID, the course is renamed
after unarchiving.
If $newCourseID is defined and differs from the archived course ID, the course
is renamed after unarchiving.

$ce is a WeBWorK::CourseEnvironment object that describes the some course's
environment. (Usually this would be the admin course.) This is used to access
Expand All @@ -946,10 +946,9 @@ If an error occurs, an exception is thrown.
sub unarchiveCourse {
my (%options) = @_;

my $newCourseID = $options{newCourseID};
my $currCourseID = $options{oldCourseID};
my $archivePath = $options{archivePath};
my $ce = $options{ce};
my $newCourseID = $options{newCourseID};
my $archivePath = $options{archivePath};
my $ce = $options{ce};

my $coursesDir = $ce->{webworkDirs}{courses};

Expand All @@ -962,17 +961,32 @@ sub unarchiveCourse {
croak "New course ID cannot exceed " . $ce->{maxCourseIdLength} . " characters."
if (length($newCourseID) > $ce->{maxCourseIdLength});

##### step 1: move a conflicting course away #####

# if this function returns undef, it means there was no course in the way
my $restoreCourseData = _unarchiveCourse_move_away($ce, $currCourseID);

##### step 2: crack open the tarball #####
##### step 1: open the tarball and determine the archived course ID #####

my $arch = Archive::Tar->new($archivePath);
die "The tar file $archivePath is not valid." unless $arch;
$arch->setcwd($coursesDir);

# Archive::Tar extracts to the directory name stored in the archive, so the
# source course ID must come from there and not from the caller-supplied name
# -- otherwise a renamed .tar.gz restores files under one name while the
# database dump is sought under another.
my %top_level;
for my $file ($arch->get_files) {
(my $first = $file->full_path) =~ s{/.*}{}s;
$top_level{$first} = 1 if length $first;
}
die "The archive $archivePath does not contain a single top-level course directory.\n"
unless keys %top_level == 1;
my ($currCourseID) = keys %top_level;

##### step 2: move a conflicting course away #####

# if this function returns undef, it means there was no course in the way
my $restoreCourseData = _unarchiveCourse_move_away($ce, $currCourseID);

##### step 3: crack open the tarball #####

# Secure extract mode refuses symbolic/hard links whose targets leave the
# course directory (CVE-2026-42496/-42497), which the standard template links
# do. Extract the files under secure mode, then recreate the symbolic links.
Expand All @@ -992,7 +1006,7 @@ sub unarchiveCourse {
symlink($symlink->linkname, $link_path) unless -e $link_path;
}

##### step 3: read the course environment for this course #####
##### step 4: read the course environment for this course #####

my $ce2 = WeBWorK::CourseEnvironment->new({ get_SeedCE($ce), courseName => $currCourseID });

Expand All @@ -1001,7 +1015,7 @@ sub unarchiveCourse {
my $data_dir = $ce2->{courseDirs}{DATA};
my $dump_dir = "$data_dir/mysqldump";

##### step 4: restore the database tables #####
##### step 5: restore the database tables #####

my $no_database;
my $restore_db_result = 1;
Expand All @@ -1018,7 +1032,7 @@ sub unarchiveCourse {
warn "database restore of course '$currCourseID' failed: the course will probably not be usable.\n";
}

##### step 5: delete dump_dir #####
##### step 6: delete dump_dir #####

_archiveCourse_remove_dump_dir($ce, $dump_dir) if -e $dump_dir;

Expand Down Expand Up @@ -1049,7 +1063,7 @@ sub unarchiveCourse {
}
}

##### step 6: rename course #####
##### step 7: rename course #####

if (defined $newCourseID && $newCourseID ne $currCourseID) {
renameCourse(
Expand All @@ -1060,7 +1074,7 @@ sub unarchiveCourse {
);
}

##### step 7: return conflicting course to its rightful place #####
##### step 8: return conflicting course to its rightful place #####

_unarchiveCourse_move_back($restoreCourseData);
}
Expand Down