diff --git a/courses.dist/modelCourse/templates/achievements/achievement_readme.txt b/courses.dist/modelCourse/templates/achievements/achievement_readme.txt index 302168d49f..ef0f07b16b 100644 --- a/courses.dist/modelCourse/templates/achievements/achievement_readme.txt +++ b/courses.dist/modelCourse/templates/achievements/achievement_readme.txt @@ -70,7 +70,9 @@ $userAchievements: $localData: This is a hash which stores data for this user and achievement (changes to this variable *will* be saved). This hash will persist - from evaluation to evaluation. You can store whatever you like in here + from evaluation to evaluation. You can store numbers, strings, and array + or hash references containing those in here (the data is saved as JSON, + so other values such as objects or code references will not be saved), and it can be accessed next time this evaluator is run. Two things to keep in mind. First, The data in this hash will *not* be accessible by other achievements. Second, the first time a variable is accessed it @@ -79,10 +81,11 @@ $localData: $globalData: This is a hash which stores data for all achievements (changes to this variable *will* be saved). This hash will persist from evaluation to - evaluation and, like $localData, you can store whatever you like in here. - This data will be accessible from *every* achievement and is unique to - the user. There are three variables stored in this hash that are - maintained by the system and should not be modified. + evaluation and, like $localData, you can store numbers, strings, and array + or hash references containing those in here. This data will be accessible + from *every* achievement and is unique to the user. There are three + variables stored in this hash that are maintained by the system and should + not be modified. - $globalData->{completeSets}: This is the number of sets which the student has earned 100% on. - $globalData->{completedSetIds}: diff --git a/lib/WeBWorK/AchievementEvaluator.pm b/lib/WeBWorK/AchievementEvaluator.pm index bf4d0cf9de..4d71546aba 100644 --- a/lib/WeBWorK/AchievementEvaluator.pm +++ b/lib/WeBWorK/AchievementEvaluator.pm @@ -9,7 +9,7 @@ use Mojo::Base 'Exporter', -signatures; use DateTime; -use WeBWorK::Utils qw(sortAchievements nfreeze_base64 thaw_base64); +use WeBWorK::Utils qw(sortAchievements encodeAchievementData decodeAchievementData); use WeBWorK::Utils::ProblemProcessing qw(compute_unreduced_score); use WeBWorK::Utils::Tags; use WeBWorK::WWSafe; @@ -84,10 +84,7 @@ sub checkForAchievements ($problem_in, $c, %options) { # Methods allowed in the safe container. $compartment->permit(qw(time localtime)); - # Thaw_Base64 globalData hash. - if ($globalUserAchievement->frozen_hash) { - $globalData = thaw_base64($globalUserAchievement->frozen_hash); - } + $globalData = decodeAchievementData($globalUserAchievement->frozen_hash); # Generate hash of user achievements: foreach my $achievement (@achievements) { @@ -186,10 +183,7 @@ sub checkForAchievements ($problem_in, $c, %options) { my $setType = $set->assignment_type; next unless $achievement->assignment_type =~ /$setType/; - # Thaw_base64 localData hash. - if ($userAchievement->frozen_hash) { - $localData = thaw_base64($userAchievement->frozen_hash); - } + $localData = decodeAchievementData($userAchievement->frozen_hash); # Recover counter information (for progress bar achievements). $counter = $userAchievement->counter; @@ -203,7 +197,7 @@ sub checkForAchievements ($problem_in, $c, %options) { $source = <$SOURCE>; close($SOURCE); } else { - warn('Couldnt find achievement evaluator $sourceFilePath'); + warn "Could not find achievement evaluator $sourceFilePath"; next; } @@ -250,15 +244,15 @@ sub checkForAchievements ($problem_in, $c, %options) { ) if ($ce->{mail}{achievementEmailFrom} && $achievement->email_template); } - # Update counter, nfreeze_base64 localData and store. + # Update counter, encode localData and store. $userAchievement->counter($counter); - $userAchievement->frozen_hash(nfreeze_base64($localData)); + $userAchievement->frozen_hash(encodeAchievementData($localData)); $db->putUserAchievement($userAchievement); } # End for loop. - # nfreeze_base64 globalData and store. - $globalUserAchievement->frozen_hash(nfreeze_base64($globalData)); + # Encode globalData and store. + $globalUserAchievement->frozen_hash(encodeAchievementData($globalData)); $db->putGlobalUserAchievement($globalUserAchievement); if (@$cheevoMessage) { diff --git a/lib/WeBWorK/AchievementItems.pm b/lib/WeBWorK/AchievementItems.pm index 45ae51a334..e208f470b2 100644 --- a/lib/WeBWorK/AchievementItems.pm +++ b/lib/WeBWorK/AchievementItems.pm @@ -1,7 +1,7 @@ package WeBWorK::AchievementItems; use Mojo::Base -signatures; -use WeBWorK::Utils qw(nfreeze_base64 thaw_base64); +use WeBWorK::Utils qw(encodeAchievementData decodeAchievementData); # List of available achievement items. Make sure to add any new items to this list. Furthermore, the elements in this # list have to match the class name of the achievement item classes loaded below. @@ -63,7 +63,7 @@ sub UserItems ($c, $userName, $set, $records) { my $globalUserAchievement = $c->{globalData} // $db->getGlobalUserAchievement($userName); return unless $globalUserAchievement && $globalUserAchievement->frozen_hash; - my $globalData = thaw_base64($globalUserAchievement->frozen_hash); + my $globalData = decodeAchievementData($globalUserAchievement->frozen_hash); my $use_item_id = $c->param('use_achievement_item_id') // ''; my @items; @@ -88,7 +88,7 @@ sub UserItems ($c, $userName, $set, $records) { if ($message) { $globalData->{$item}--; $achievementItem->{count}--; - $globalUserAchievement->frozen_hash(nfreeze_base64($globalData)); + $globalUserAchievement->frozen_hash(encodeAchievementData($globalData)); $db->putGlobalUserAchievement($globalUserAchievement); $c->addgoodmessage($c->maketext('[_1] successfully used. [_2]', $achievementItem->name, $message)); } diff --git a/lib/WeBWorK/ContentGenerator/Achievements.pm b/lib/WeBWorK/ContentGenerator/Achievements.pm index 8ff834a36d..b8ccafba71 100644 --- a/lib/WeBWorK/ContentGenerator/Achievements.pm +++ b/lib/WeBWorK/ContentGenerator/Achievements.pm @@ -9,7 +9,7 @@ This produces a list of earned achievements for each student. =cut -use WeBWorK::Utils qw(sortAchievements thaw_base64); +use WeBWorK::Utils qw(sortAchievements decodeAchievementData); use WeBWorK::AchievementItems; sub initialize ($c) { @@ -37,9 +37,7 @@ sub getAchievementLevelData ($c) { if ($achievement) { if ($c->{globalData}->next_level_points) { - # Get prev_level_points from the globalData frozen_hash in the database. - my $globalData = $c->{globalData}->frozen_hash ? thaw_base64($c->{globalData}->frozen_hash) : {}; - my $prev_level = $globalData->{prev_level_points} || 0; + my $prev_level = decodeAchievementData($c->{globalData}->frozen_hash)->{prev_level_points} || 0; $level_goal = $c->{globalData}->next_level_points - $prev_level; $level_progress = $c->{globalData}->achievement_points - $prev_level; $level_progress = 0 if $level_progress < 0; diff --git a/lib/WeBWorK/Utils.pm b/lib/WeBWorK/Utils.pm index 592653b4d7..9195feb6c7 100644 --- a/lib/WeBWorK/Utils.pm +++ b/lib/WeBWorK/Utils.pm @@ -5,7 +5,7 @@ use Email::Address::XS; use Email::Sender::Transport::SMTP; use Mojo::JSON qw(from_json to_json); use Mojo::Util qw(b64_encode b64_decode encode decode); -use Storable qw(nfreeze thaw); +use Storable qw(thaw); our @EXPORT_OK = qw( runtime_use @@ -15,8 +15,8 @@ our @EXPORT_OK = qw( decodeAnswers encode_utf8_base64 decode_utf8_base64 - nfreeze_base64 - thaw_base64 + encodeAchievementData + decodeAchievementData min max wwRound @@ -98,22 +98,23 @@ sub decode_utf8_base64 ($in) { return decode('UTF-8', b64_decode($in)); } -sub nfreeze_base64 ($in) { - return b64_encode(nfreeze($in)); +sub encodeAchievementData ($data) { + return encode_utf8_base64(to_json($data)); } -sub thaw_base64 ($string) { - my $result; +sub decodeAchievementData ($serialized) { + return {} unless $serialized; - eval { $result = thaw(b64_decode($string)); }; + my $bytes = b64_decode($serialized); + # Data stored by older versions of WeBWorK was encoded with Storable::nfreeze. + my $data = eval { $bytes =~ /^\{/ ? from_json(decode('UTF-8', $bytes)) : thaw($bytes) }; - if ($@) { + if ($@ || ref($data) ne 'HASH') { warn('Deleting corrupted achievement data.'); return {}; - } else { - return $result; } + return $data; } sub min (@items) { @@ -661,19 +662,26 @@ Usage: C Base 64 decodes, and then UTF-8 decodes the input and returns the result. -=head2 nfreeze_base64 +=head2 encodeAchievementData + +Usage: C -Usage: C +JSON encodes the achievement data hash reference C<$data>, and then UTF-8 and +base 64 encodes the result. The base 64 encoding keeps the stored value ASCII, +which avoids database driver differences in the handling of UTF-8 data in the +binary C column. -This C encodes and then base 64 encodes C<$in> and returns -the result. +=head2 decodeAchievementData -=head2 thaw_base64 +Usage: C -Usage: C +Returns the achievement data hash reference decoded from C<$serialized>. -This base 64 decodes and then C decodes C<$in> and returns -result. +This method detects if C<$serialized> is a JSON encoded hash (encoded with +C), or is a L encoded hash (the old +method), and decodes using the appropriate method. An empty hash reference is +returned if C<$serialized> is empty, cannot be decoded, or does not decode to a +hash reference. =head2 min diff --git a/templates/HelpFiles/InstructorAchievementEditor.html.ep b/templates/HelpFiles/InstructorAchievementEditor.html.ep index 49e9f7dd7b..c1215c0827 100644 --- a/templates/HelpFiles/InstructorAchievementEditor.html.ep +++ b/templates/HelpFiles/InstructorAchievementEditor.html.ep @@ -52,11 +52,14 @@
  • $localData: <%= maketext('A hash reference which saves data for this user and this achievement. This hash is persistent ' - . 'between evaluations and changes to this variable will be saved in the database.') =%> + . 'between evaluations and changes to this variable will be saved in the database. The data is saved ' + . 'as JSON, so only numbers, strings, and array or hash references containing those can be stored. ' + . 'Other values such as objects or code references will not be saved.') =%>
  • $globalData: <%= maketext('A hash reference which saves global data for this user. This hash is persistent between ' - . 'evaluations and changes to this variable will be saved in the database.') =%> + . 'evaluations and changes to this variable will be saved in the database. Like $localData, only ' + . 'numbers, strings, and array or hash references containing those can be stored.') =%>