MDL-58010 user: allow to update only whitelisted preferences

This commit is contained in:
Marina Glancy
2017-03-10 18:10:44 +00:00
committed by Dan Poltawski
parent 10d80cd64c
commit 0833e7cfc7
6 changed files with 136 additions and 7 deletions
+1 -1
View File
@@ -86,7 +86,7 @@ function block_course_overview_get_myorder() {
// If preference was not found, look in the old location and convert if found.
$order = array();
if ($value = get_user_preferences('course_overview_course_order')) {
$order = unserialize($value);
$order = unserialize_array($value);
block_course_overview_update_myorder($order);
unset_user_preference('course_overview_course_order');
}
+1 -1
View File
@@ -1758,7 +1758,7 @@ class grade_report_grader extends grade_report {
// Try looking for old location of user setting that used to store all courses in one serialized user preference.
if (($oldcollapsedpref = get_user_preferences('grade_report_grader_collapsed_categories')) !== null) {
if ($collapsedall = @unserialize($oldcollapsedpref)) {
if ($collapsedall = unserialize_array($oldcollapsedpref)) {
// We found the old-style preference, filter out only categories that belong to this course and update the prefs.
$collapsed = static::filter_collapsed_categories($courseid, $collapsedall);
if (!empty($collapsed['aggregatesonly']) || !empty($collapsed['gradesonly'])) {
+52
View File
@@ -9512,6 +9512,58 @@ function get_course_display_name_for_list($course) {
}
}
/**
* Safe analogue of unserialize() that can only parse arrays
*
* Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
* Note: If any string (key or value) has semicolon (;) as part of the string parsing will fail.
* This is a simple method to substitute unnecessary unserialize() in code and not intended to cover all possible cases.
*
* @param string $expression
* @return array|bool either parsed array or false if parsing was impossible.
*/
function unserialize_array($expression) {
$subs = [];
// Find nested arrays, parse them and store in $subs , substitute with special string.
while (preg_match('/([\^;\}])(a:\d+:\{[^\{\}]*\})/', $expression, $matches) && strlen($matches[2]) < strlen($expression)) {
$key = '--SUB' . count($subs) . '--';
$subs[$key] = unserialize_array($matches[2]);
if ($subs[$key] === false) {
return false;
}
$expression = str_replace($matches[2], $key . ';', $expression);
}
// Check the expression is an array.
if (!preg_match('/^a:(\d+):\{([^\}]*)\}$/', $expression, $matches1)) {
return false;
}
// Get the size and elements of an array (key;value;key;value;....).
$parts = explode(';', $matches1[2]);
$size = intval($matches1[1]);
if (count($parts) < $size * 2 + 1) {
return false;
}
// Analyze each part and make sure it is an integer or string or a substitute.
$value = [];
for ($i = 0; $i < $size * 2; $i++) {
if (preg_match('/^i:(\d+)$/', $parts[$i], $matches2)) {
$parts[$i] = (int)$matches2[1];
} else if (preg_match('/^s:(\d+):"(.*)"$/', $parts[$i], $matches3) && strlen($matches3[2]) == (int)$matches3[1]) {
$parts[$i] = $matches3[2];
} else if (preg_match('/^--SUB\d+--$/', $parts[$i])) {
$parts[$i] = $subs[$parts[$i]];
} else {
return false;
}
}
// Combine keys and values.
for ($i = 0; $i < $size * 2; $i += 2) {
$value[$parts[$i]] = $parts[$i+1];
}
return $value;
}
/**
* The lang_string class
*
+30
View File
@@ -3036,4 +3036,34 @@ class core_moodlelib_testcase extends advanced_testcase {
$CFG->mailprefix = 'mdl-';
$this->assertEquals(1, validate_email(generate_email_processing_address(23, $modargs)));
}
/**
* Test safe method unserialize_array().
*/
public function test_unserialize_array() {
$a = [1, 2, 3];
$this->assertEquals($a, unserialize_array(serialize($a)));
$this->assertEquals($a, unserialize_array(serialize($a)));
$a = ['a' => 1, 2 => 2, 'b' => 'cde'];
$this->assertEquals($a, unserialize_array(serialize($a)));
$this->assertEquals($a, unserialize_array(serialize($a)));
$a = ['a' => 1, 2 => 2, 'b' => 'c"d"e'];
$this->assertEquals($a, unserialize_array(serialize($a)));
$a = ['a' => 1, 2 => ['c' => 'd', 'e' => 'f'], 'b' => 'cde'];
$this->assertEquals($a, unserialize_array(serialize($a)));
// Can not unserialize if any string contains semicolons.
$a = ['a' => 1, 2 => 2, 'b' => 'c"d";e'];
$this->assertEquals(false, unserialize_array(serialize($a)));
// Can not unserialize if there are any objects.
$a = (object)['a' => 1, 2 => 2, 'b' => 'cde'];
$this->assertEquals(false, unserialize_array(serialize($a)));
$a = ['a' => 1, 2 => 2, 'b' => (object)['a' => 'cde']];
$this->assertEquals(false, unserialize_array(serialize($a)));
// Array used in the grader report.
$a = array('aggregatesonly' => [51, 34], 'gradesonly' => [21, 45, 78]);
$this->assertEquals($a, unserialize_array(serialize($a)));
}
}
+32 -3
View File
@@ -217,8 +217,8 @@ class core_user_external extends external_api {
profile_save_data((object) $user);
}
$userobject = (object)$user;
if ($createpassword) {
$userobject = (object)$user;
setnew_password_and_mail($userobject);
unset_user_preference('create_password', $userobject);
set_user_preference('auth_forcepasswordchange', 1, $userobject);
@@ -230,7 +230,7 @@ class core_user_external extends external_api {
// Preferences.
if (!empty($user['preferences'])) {
foreach ($user['preferences'] as $preference) {
set_user_preference($preference['type'], $preference['value'], $user['id']);
self::set_user_preference($preference['type'], $preference['value'], $userobject);
}
}
@@ -459,7 +459,7 @@ class core_user_external extends external_api {
// Preferences.
if (!empty($user['preferences'])) {
foreach ($user['preferences'] as $preference) {
set_user_preference($preference['type'], $preference['value'], $user['id']);
self::set_user_preference($preference['type'], $preference['value'], $existinguser);
}
}
}
@@ -1453,6 +1453,35 @@ class core_user_external extends external_api {
);
}
/**
* Validates preference value and updates the user preference
*
* @param string $name
* @param string $value
* @param stdClass $user
*/
protected static function set_user_preference($name, $value, $user) {
$preferences = array(
'auth_forcepasswordchange' => PARAM_BOOL,
'htmleditor' => PARAM_COMPONENT,
'usemodchooser' => PARAM_BOOL,
'badgeprivacysetting' => PARAM_BOOL,
'blogpagesize' => PARAM_INT,
'forum_markasreadonnotification' => PARAM_INT,
'calendar_timeformat' => PARAM_NOTAGS,
'calendar_startwday' => PARAM_INT,
'calendar_maxevents' => PARAM_INT,
'calendar_lookahead' => PARAM_INT,
'calendar_persistflt' => PARAM_INT
);
if (isset($preferences[$name])) {
$value = clean_param($value, $preferences[$name]);
if ($preferences[$name] == PARAM_BOOL) {
$value = (int)$value;
}
set_user_preference($name, $value, $user);
}
}
}
/**
+20 -2
View File
@@ -497,7 +497,14 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
'email' => '[email protected]',
'description' => 'This is a description for user 1',
'city' => 'Perth',
'country' => 'au'
'country' => 'au',
'preferences' => [[
'type' => 'htmleditor',
'value' => 'atto'
], [
'type' => 'invalidpreference',
'value' => 'abcd'
]]
);
$context = context_system::instance();
@@ -522,6 +529,8 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
$this->assertEquals($dbuser->description, $user1['description']);
$this->assertEquals($dbuser->city, $user1['city']);
$this->assertEquals($dbuser->country, $user1['country']);
$this->assertEquals('atto', get_user_preferences('htmleditor', null, $dbuser));
$this->assertEquals(null, get_user_preferences('invalidpreference', null, $dbuser));
}
// Call without required capability
@@ -676,7 +685,14 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
'email' => '[email protected]',
'description' => 'This is a description for user 1',
'city' => 'Perth',
'country' => 'au'
'country' => 'au',
'preferences' => [[
'type' => 'htmleditor',
'value' => 'atto'
], [
'type' => 'invalidpreference',
'value' => 'abcd'
]]
);
$context = context_system::instance();
@@ -712,6 +728,8 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
$this->assertEquals($dbuser->description, $user1['description']);
$this->assertEquals($dbuser->city, $user1['city']);
$this->assertEquals($dbuser->country, $user1['country']);
$this->assertEquals('atto', get_user_preferences('htmleditor', null, $dbuser));
$this->assertEquals(null, get_user_preferences('invalidpreference', null, $dbuser));
// Call without required capability.
$this->unassignUserCapability('moodle/user:update', $context->id, $roleid);