MDL-58010 user: allow to update only whitelisted user preferences

This commit is contained in:
Marina Glancy
2017-03-10 18:04:47 +00:00
committed by Dan Poltawski
parent 77f591831c
commit 6e65554ea1
18 changed files with 559 additions and 95 deletions
+3 -2
View File
@@ -26,6 +26,7 @@
require_once(__DIR__ . '/../config.php');
require_once('preferences_form.php');
require_once($CFG->dirroot.'/user/editlib.php');
$url = new moodle_url('/badges/preferences.php');
@@ -42,8 +43,8 @@ $mform = new badges_preferences_form();
$mform->set_data(array('badgeprivacysetting' => get_user_preferences('badgeprivacysetting')));
if (!$mform->is_cancelled() && $data = $mform->get_data()) {
$setting = $data->badgeprivacysetting;
set_user_preference('badgeprivacysetting', $setting);
useredit_update_user_preference(['id' => $USER->id,
'preference_badgeprivacysetting' => $data->badgeprivacysetting]);
}
if ($mform->is_cancelled()) {
+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');
}
+3 -1
View File
@@ -27,6 +27,7 @@
require_once('../config.php');
require_once($CFG->dirroot.'/blog/lib.php');
require_once('preferences_form.php');
require_once($CFG->dirroot.'/user/editlib.php');
$courseid = optional_param('courseid', SITEID, PARAM_INT);
$modid = optional_param('modid', null, PARAM_INT);
@@ -81,7 +82,8 @@ if (!$mform->is_cancelled() && $data = $mform->get_data()) {
if ($pagesize < 1) {
print_error('invalidpagesize');
}
set_user_preference('blogpagesize', $pagesize);
useredit_update_user_preference(['id' => $USER->id,
'preference_blogpagesize' => $pagesize]);
}
if ($mform->is_cancelled()) {
+22
View File
@@ -3449,3 +3449,25 @@ function calendar_get_calendar_context($subscription) {
}
return $context;
}
/**
* Implements callback user_preferences, whitelists preferences that users are allowed to update directly
*
* Used in {@see core_user::fill_preferences_cache()}, see also {@see useredit_update_user_preference()}
*
* @return array
*/
function core_calendar_user_preferences() {
$preferences = [];
$preferences['calendar_timeformat'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED, 'default' => '0',
'choices' => array('0', CALENDAR_TF_12, CALENDAR_TF_24)
);
$preferences['calendar_startwday'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0,
'choices' => array(0, 1, 2, 3, 4, 5, 6));
$preferences['calendar_maxevents'] = array('type' => PARAM_INT, 'choices' => range(1, 20));
$preferences['calendar_lookahead'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 365,
'choices' => array(365, 270, 180, 150, 120, 90, 60, 30, 21, 14, 7, 6, 5, 4, 3, 2, 1));
$preferences['calendar_persistflt'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0,
'choices' => array(0, 1));
return $preferences;
}
+1 -1
View File
@@ -1774,7 +1774,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'])) {
+220
View File
@@ -67,6 +67,9 @@ class core_user {
/** @var array store user fields properties cache. */
protected static $propertiescache = null;
/** @var array store user preferences cache. */
protected static $preferencescache = null;
/**
* Return user object from db or create noreply or support user,
* if userid matches corse_user::NOREPLY_USER or corse_user::SUPPORT_USER
@@ -644,4 +647,221 @@ class core_user {
return self::$propertiescache[$property]['default'];
}
/**
* Definition of updateable user preferences and rules for data and access validation.
*
* array(
* 'preferencename' => array( // Either exact preference name or a regular expression.
* 'null' => NULL_ALLOWED, // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOT_ALLOWED or NULL_ALLOWED.
* 'type' => PARAM_TYPE, // Expected parameter type of the user field - mandatory
* 'choices' => array(1, 2..) // An array of accepted values of the user field - optional
* 'default' => $CFG->setting // An default value for the field - optional
* 'isregex' => false/true // Whether the name of the preference is a regular expression (default false).
* 'permissioncallback' => callable // Function accepting arguments ($user, $preferencename) that checks if current user
* // is allowed to modify this preference for given user.
* // If not specified core_user::default_preference_permission_check() will be assumed.
* 'cleancallback' => callable // Custom callback for cleaning value if something more difficult than just type/choices is needed
* // accepts arguments ($value, $preferencename)
* )
* )
*
* @return void
*/
protected static function fill_preferences_cache() {
if (self::$preferencescache !== null) {
return;
}
// Array of user preferences and expected types/values.
// Every preference that can be updated directly by user should be added here.
$preferences = array();
$preferences['auth_forcepasswordchange'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'choices' => array(0, 1),
'permissioncallback' => function($user, $preferencename) {
global $USER;
$systemcontext = context_system::instance();
return ($USER->id != $user->id && (has_capability('moodle/user:update', $systemcontext) ||
($user->timecreated > time() - 10 && has_capability('moodle/user:create', $systemcontext))));
});
$preferences['usemodchooser'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
'choices' => array(0, 1));
$preferences['forum_markasreadonnotification'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
'choices' => array(0, 1));
$preferences['htmleditor'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED,
'cleancallback' => function($value, $preferencename) {
if (empty($value) || !array_key_exists($value, core_component::get_plugin_list('editor'))) {
return null;
}
return $value;
});
$preferences['badgeprivacysetting'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
'choices' => array(0, 1), 'permissioncallback' => function($user, $preferencename) {
global $CFG, $USER;
return !empty($CFG->enablebadges) && $user->id == $USER->id;
});
$preferences['blogpagesize'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 10,
'permissioncallback' => function($user, $preferencename) {
global $USER;
return $USER->id == $user->id && has_capability('moodle/blog:view', context_system::instance());
});
// Core components that may want to define their preferences.
// List of core components implementing callback is hardcoded here for performance reasons.
// TODO MDL-58184 cache list of core components implementing a function.
$corecomponents = ['core_message', 'core_calendar'];
foreach ($corecomponents as $component) {
if (($pluginpreferences = component_callback($component, 'user_preferences')) && is_array($pluginpreferences)) {
$preferences += $pluginpreferences;
}
}
// Plugins that may define their preferences.
if ($pluginsfunction = get_plugins_with_function('user_preferences')) {
foreach ($pluginsfunction as $plugintype => $plugins) {
foreach ($plugins as $function) {
if (($pluginpreferences = call_user_func($function)) && is_array($pluginpreferences)) {
$preferences += $pluginpreferences;
}
}
}
}
self::$preferencescache = $preferences;
}
/**
* Retrieves the preference definition
*
* @param string $preferencename
* @return array
*/
protected static function get_preference_definition($preferencename) {
self::fill_preferences_cache();
foreach (self::$preferencescache as $key => $preference) {
if (empty($preference['isregex'])) {
if ($key === $preferencename) {
return $preference;
}
} else {
if (preg_match($key, $preferencename)) {
return $preference;
}
}
}
throw new coding_exception('Invalid preference requested.');
}
/**
* Default callback used for checking if current user is allowed to change permission of user $user
*
* @param stdClass $user
* @param string $preferencename
* @return bool
*/
protected static function default_preference_permission_check($user, $preferencename) {
global $USER;
if (is_mnet_remote_user($user)) {
// Can't edit MNET user.
return false;
}
if ($user->id == $USER->id) {
// Editing own profile.
$systemcontext = context_system::instance();
return has_capability('moodle/user:editownprofile', $systemcontext);
} else {
// Teachers, parents, etc.
$personalcontext = context_user::instance($user->id);
if (!has_capability('moodle/user:editprofile', $personalcontext)) {
return false;
}
if (is_siteadmin($user->id) and !is_siteadmin($USER)) {
// Only admins may edit other admins.
return false;
}
return true;
}
}
/**
* Can current user edit preference of this/another user
*
* @param string $preferencename
* @param stdClass $user
* @return bool
*/
public static function can_edit_preference($preferencename, $user) {
if (!isloggedin() || isguestuser()) {
// Guests can not edit anything.
return false;
}
try {
$definition = self::get_preference_definition($preferencename);
} catch (coding_exception $e) {
return false;
}
if ($user->deleted || !context_user::instance($user->id, IGNORE_MISSING)) {
// User is deleted.
return false;
}
if (isset($definition['permissioncallback'])) {
$callback = $definition['permissioncallback'];
if (is_callable($callback)) {
return call_user_func_array($callback, [$user, $preferencename]);
} else {
throw new coding_exception('Permission callback for preference ' . s($preferencename) . ' is not callable');
return false;
}
} else {
return self::default_preference_permission_check($user, $preferencename);
}
}
/**
* Clean value of a user preference
*
* @param string $value the user preference value to be cleaned.
* @param string $preferencename the user preference name
* @return string the cleaned preference value
*/
public static function clean_preference($value, $preferencename) {
$definition = self::get_preference_definition($preferencename);
if (isset($definition['type']) && $value !== null) {
$value = clean_param($value, $definition['type']);
}
if (isset($definition['cleancallback'])) {
$callback = $definition['cleancallback'];
if (is_callable($callback)) {
return $callback($value, $preferencename);
} else {
throw new coding_exception('Clean callback for preference ' . s($preferencename) . ' is not callable');
}
} else if ($value === null && (!isset($definition['null']) || $definition['null'] == NULL_ALLOWED)) {
return null;
} else if (isset($definition['choices'])) {
if (!in_array($value, $definition['choices'])) {
if (isset($definition['default'])) {
return $definition['default'];
} else {
$first = reset($definition['choices']);
return $first;
}
} else {
return $value;
}
} else {
if ($value === null) {
return isset($definition['default']) ? $definition['default'] : '';
}
return $value;
}
}
}
+54
View File
@@ -1864,6 +1864,8 @@ function mark_user_preferences_changed($userid) {
*
* If a $user object is submitted it's 'preference' property is used for the preferences cache.
*
* When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
*
* @package core
* @category preference
* @access public
@@ -9702,6 +9704,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
@@ -3512,4 +3512,34 @@ class core_moodlelib_testcase extends advanced_testcase {
$CFG->mailprefix = 'mdl-';
$this->assertTrue(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)));
}
}
+1 -6
View File
@@ -2201,12 +2201,7 @@ class core_message_external extends external_api {
)
);
if (empty($params['userid'])) {
$params['userid'] = $USER->id;
}
$user = core_user::get_user($params['userid'], '*', MUST_EXIST);
core_user::require_active_user($user);
$user = self::validate_preferences_permissions($params['userid']);
$processor = get_message_processor($name);
$preferences = [];
+66
View File
@@ -1034,3 +1034,69 @@ function message_output_fragment_processor_settings($args = []) {
return $renderer->render_from_template('core_message/preferences_processor', $processoroutput->export_for_template($renderer));
}
/**
* Checks if current user is allowed to edit messaging preferences of another user
*
* @param stdClass $user user whose preferences we are updating
* @return bool
*/
function core_message_can_edit_message_profile($user) {
global $USER;
if ($user->id == $USER->id) {
return has_capability('moodle/user:editownmessageprofile', context_system::instance());
} else {
$personalcontext = context_user::instance($user->id);
if (!has_capability('moodle/user:editmessageprofile', $personalcontext)) {
return false;
}
if (isguestuser($user)) {
return false;
}
// No editing of admins by non-admins.
if (is_siteadmin($user) and !is_siteadmin($USER)) {
return false;
}
return true;
}
}
/**
* Implements callback user_preferences, whitelists preferences that users are allowed to update directly
*
* Used in {@see core_user::fill_preferences_cache()}, see also {@see useredit_update_user_preference()}
*
* @return array
*/
function core_message_user_preferences() {
$preferences = [];
$preferences['message_blocknoncontacts'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0,
'choices' => array(0, 1));
$preferences['/^message_provider_([\w\d_]*)_logged(in|off)$/'] = array('isregex' => true, 'type' => PARAM_NOTAGS,
'null' => NULL_NOT_ALLOWED, 'default' => 'none',
'permissioncallback' => function ($user, $preferencename) {
global $CFG;
require_once($CFG->libdir.'/messagelib.php');
if (core_message_can_edit_message_profile($user) &&
preg_match('/^message_provider_([\w\d_]*)_logged(in|off)$/', $preferencename, $matches)) {
$providers = message_get_providers_for_user($user->id);
foreach ($providers as $provider) {
if ($matches[1] === $provider->component . '_' . $provider->name) {
return true;
}
}
}
return false;
},
'cleancallback' => function ($value, $preferencename) {
if ($value === 'none' || empty($value)) {
return 'none';
}
$parts = explode('/,/', $value);
$processors = array_keys(get_message_processors());
array_filter($parts, function($v) use ($processors) {return in_array($v, $processors);});
return $parts ? join(',', $parts) : 'none';
});
return $preferences;
}
@@ -164,10 +164,13 @@ class message_output_email extends message_output {
global $CFG;
if (isset($form->email_email)) {
$preferences['message_processor_email_email'] = $form->email_email;
$preferences['message_processor_email_email'] = clean_param($form->email_email, PARAM_EMAIL);
}
if (isset($form->preference_mailcharset)) {
$preferences['mailcharset'] = $form->preference_mailcharset;
if (!array_key_exists($preferences['mailcharset'], get_list_of_charsets())) {
$preferences['mailcharset'] = '0';
}
}
if (isset($form->mailformat) && isset($form->userid)) {
require_once($CFG->dirroot.'/user/lib.php');
+8 -24
View File
@@ -58,30 +58,14 @@ if ($calendarform->is_cancelled()) {
} else if ($calendarform->is_submitted() && $calendarform->is_validated() && confirm_sesskey()) {
$data = $calendarform->get_data();
// Time format.
if ($data->timeformat != CALENDAR_TF_12 && $data->timeformat != CALENDAR_TF_24) {
$data->timeformat = '';
}
set_user_preference('calendar_timeformat', $data->timeformat);
// Start weekday.
$data->startwday = intval($data->startwday);
if ($data->startwday < 0 || $data->startwday > 6) {
$data->startwday = abs($data->startwday % 7);
}
set_user_preference('calendar_startwday', $data->startwday);
// Calendar events.
if (intval($data->maxevents) >= 1) {
set_user_preference('calendar_maxevents', $data->maxevents);
}
// Calendar lookahead.
if (intval($data->lookahead) >= 1) {
set_user_preference('calendar_lookahead', $data->lookahead);
}
set_user_preference('calendar_persistflt', intval($data->persistflt));
$usernew = ['id' => $USER->id,
'preference_calendar_timeformat' => $data->timeformat,
'preference_calendar_startwday' => $data->startwday,
'preference_calendar_maxevents' => $data->maxevents,
'preference_calendar_lookahead' => $data->lookahead,
'preference_calendar_persistflt' => $data->persistflt
];
useredit_update_user_preference($usernew);
// Calendar type.
$calendartype = $data->calendartype;
+2 -1
View File
@@ -41,7 +41,8 @@ $redirect = new moodle_url("/user/preferences.php", array('userid' => $user->id)
if ($courseform->is_cancelled()) {
redirect($redirect);
} else if ($data = $courseform->get_data()) {
set_user_preference('usemodchooser', $data->enableactivitychooser, $user);
useredit_update_user_preference(['id' => $user->id,
'preference_usemodchooser' => $data->enableactivitychooser]);
redirect($redirect);
}
+9 -7
View File
@@ -199,14 +199,16 @@ if ($usernew = $userform->get_data()) {
// Other users require a confirmation email.
if (isset($usernew->email) and $user->email != $usernew->email && !has_capability('moodle/user:update', $systemcontext)) {
$a = new stdClass();
$a->newemail = $usernew->preference_newemail = $usernew->email;
$usernew->preference_newemailkey = random_string(20);
$usernew->preference_newemailattemptsleft = 3;
$emailchangedkey = random_string(20);
set_user_preference('newemail', $usernew->email, $user->id);
set_user_preference('newemailkey', $emailchangedkey, $user->id);
set_user_preference('newemailattemptsleft', 3, $user->id);
$a->newemail = $emailchanged = $usernew->email;
$a->oldemail = $usernew->email = $user->email;
$emailchangedhtml = $OUTPUT->box(get_string('auth_changingemailaddress', 'auth', $a), 'generalbox', 'notice');
$emailchangedhtml .= $OUTPUT->continue_button($returnurl);
$emailchanged = true;
}
}
@@ -254,14 +256,14 @@ if ($usernew = $userform->get_data()) {
\core\event\user_updated::create_from_userid($user->id)->trigger();
// If email was changed and confirmation is required, send confirmation email now to the new address.
if ($emailchanged && $CFG->emailchangeconfirmation) {
if ($emailchanged !== false && $CFG->emailchangeconfirmation) {
$tempuser = $DB->get_record('user', array('id' => $user->id), '*', MUST_EXIST);
$tempuser->email = $usernew->preference_newemail;
$tempuser->email = $emailchanged;
$supportuser = core_user::get_support_user();
$a = new stdClass();
$a->url = $CFG->wwwroot . '/user/emailupdate.php?key=' . $usernew->preference_newemailkey . '&id=' . $user->id;
$a->url = $CFG->wwwroot . '/user/emailupdate.php?key=' . $emailchangedkey . '&id=' . $user->id;
$a->site = format_string($SITE->fullname, true, array('context' => context_course::instance(SITEID)));
$a->fullname = fullname($tempuser, true);
$a->supportemail = $supportuser->email;
+25 -3
View File
@@ -147,16 +147,38 @@ function useredit_load_preferences(&$user, $reload=true) {
}
/**
* Updates the user preferences for teh given user.
* Updates the user preferences for the given user
*
* @param stdClass|array $usernew
* Only preference that can be updated directly will be updated here. This method is called from various WS
* updating users and should be used when updating user details. Plugins may whitelist preferences that can
* be updated by defining 'user_preferences' callback, {@see core_user::fill_preferences_cache()}
*
* Some parts of code may use user preference table to store internal data, in these cases it is acceptable
* to call set_user_preference()
*
* @param stdClass|array $usernew object or array that has user preferences as attributes with keys starting with preference_
*/
function useredit_update_user_preference($usernew) {
global $USER;
$ua = (array)$usernew;
if (is_object($usernew) && isset($usernew->id) && isset($usernew->deleted) && isset($usernew->confirmed)) {
// This is already a full user object, maybe not completely full but these fields are enough.
$user = $usernew;
} else if (empty($ua['id']) || $ua['id'] == $USER->id) {
// We are updating current user.
$user = $USER;
} else {
// Retrieve user object.
$user = core_user::get_user($ua['id'], '*', MUST_EXIST);
}
foreach ($ua as $key => $value) {
if (strpos($key, 'preference_') === 0) {
$name = substr($key, strlen('preference_'));
set_user_preference($name, $value, $usernew->id);
if (core_user::can_edit_preference($name, $user)) {
$value = core_user::clean_preference($value, $name);
set_user_preference($name, $value, $user->id);
}
}
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ if ($editorform->is_cancelled()) {
$user->preference_htmleditor = $data->preference_htmleditor;
useredit_update_user_preference($user, false, false);
useredit_update_user_preference($user);
// Trigger event.
\core\event\user_updated::create_from_userid($user->id)->trigger();
+38 -31
View File
@@ -229,9 +229,11 @@ class core_user_external extends external_api {
// Preferences.
if (!empty($user['preferences'])) {
$userpref = (object)$user;
foreach ($user['preferences'] as $preference) {
set_user_preference($preference['type'], $preference['value'], $user['id']);
$userpref->{'preference_'.$preference['type']} = $preference['value'];
}
useredit_update_user_preference($userpref);
}
$userids[] = array('id' => $user['id'], 'username' => $user['username']);
@@ -359,6 +361,8 @@ class core_user_external extends external_api {
global $USER, $CFG;
require_once($CFG->dirroot . '/user/lib.php');
require_once($CFG->dirroot . '/user/editlib.php');
require_once($CFG->dirroot . '/message/lib.php');
if (empty($userid)) {
$userid = $USER->id;
@@ -373,38 +377,29 @@ class core_user_external extends external_api {
);
self::validate_parameters(self::update_user_preferences_parameters(), $params);
if ($userid == $USER->id) {
require_capability('moodle/user:editownmessageprofile', $systemcontext);
} else {
$personalcontext = context_user::instance($userid);
require_capability('moodle/user:editmessageprofile', $personalcontext);
// No editing of guest user account.
if (isguestuser($userid)) {
print_error('guestnoeditmessageother', 'message');
}
// No editing of admins by non-admins.
if (is_siteadmin($userid) and !is_siteadmin($USER)) {
print_error('useradmineditadmin');
}
}
// Preferences.
if (!empty($preferences)) {
$userpref = ['id' => $userid];
foreach ($preferences as $preference) {
set_user_preference($preference['type'], $preference['value'], $userid);
$userpref['preference_' . $preference['type']] = $preference['value'];
}
useredit_update_user_preference($userpref);
}
// Check if they want to update the email.
if ($emailstop !== null) {
$user = new stdClass();
$user->id = $userid;
$user->emailstop = $emailstop;
user_update_user($user);
$otheruser = ($userid == $USER->id) ? $USER : core_user::get_user($userid, '*', MUST_EXIST);
core_user::require_active_user($otheruser);
if (core_message_can_edit_message_profile($otheruser) && $otheruser->emailstop != $emailstop) {
$user = new stdClass();
$user->id = $userid;
$user->emailstop = $emailstop;
user_update_user($user);
// Update the $USER if we should.
if ($userid == $USER->id) {
$USER->emailstop = $emailstop;
// Update the $USER if we should.
if ($userid == $USER->id) {
$USER->emailstop = $emailstop;
}
}
}
@@ -521,6 +516,7 @@ class core_user_external extends external_api {
global $CFG, $DB, $USER;
require_once($CFG->dirroot."/user/lib.php");
require_once($CFG->dirroot."/user/profile/lib.php"); // Required for customfields related function.
require_once($CFG->dirroot.'/user/editlib.php');
// Ensure the current user is allowed to run this function.
$context = context_system::instance();
@@ -582,9 +578,11 @@ class core_user_external extends external_api {
// Preferences.
if (!empty($user['preferences'])) {
$userpref = clone($existinguser);
foreach ($user['preferences'] as $preference) {
set_user_preference($preference['type'], $preference['value'], $user['id']);
$userpref->{'preference_'.$preference['type']} = $preference['value'];
}
useredit_update_user_preference($userpref);
}
if (isset($user['suspended']) and $user['suspended']) {
\core\session\manager::kill_user_sessions($user['id']);
@@ -1710,7 +1708,6 @@ class core_user_external extends external_api {
$context = context_system::instance();
self::validate_context($context);
require_capability('moodle/site:config', $context);
$userscache = array();
foreach ($params['preferences'] as $pref) {
@@ -1734,11 +1731,21 @@ class core_user_external extends external_api {
}
try {
set_user_preference($pref['name'], $pref['value'], $user);
$saved[] = array(
'name' => $pref['name'],
'userid' => $user->id,
);
if (core_user::can_edit_preference($pref['name'], $user)) {
$value = core_user::clean_preference($pref['value'], $pref['name']);
set_user_preference($pref['name'], $value, $user->id);
$saved[] = array(
'name' => $pref['name'],
'userid' => $user->id,
);
} else {
$warnings[] = array(
'item' => 'user',
'itemid' => $user->id,
'warningcode' => 'nopermission',
'message' => 'You are not allowed to change the preference '.s($pref['name']).' for user '.$user->id
);
}
} catch (Exception $e) {
$warnings[] = array(
'item' => 'user',
+71 -16
View File
@@ -494,11 +494,19 @@ 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();
$roleid = $this->assignUserCapability('moodle/user:create', $context->id);
$this->assignUserCapability('moodle/user:editprofile', $context->id, $roleid);
// Call the external function.
$createdusers = core_user_external::create_users(array($user1));
@@ -519,6 +527,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
@@ -599,11 +609,19 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
'description' => 'This is a description for user 1',
'city' => 'Perth',
'userpicture' => $draftid,
'country' => 'AU'
'country' => 'AU',
'preferences' => [[
'type' => 'htmleditor',
'value' => 'atto'
], [
'type' => 'invialidpreference',
'value' => 'abcd'
]]
);
$context = context_system::instance();
$roleid = $this->assignUserCapability('moodle/user:update', $context->id);
$this->assignUserCapability('moodle/user:editprofile', $context->id, $roleid);
// Check we can't update deleted users, guest users, site admin.
$user2 = $user3 = $user4 = $user1;
@@ -636,6 +654,8 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
$this->assertEquals($dbuser->city, $user1['city']);
$this->assertEquals($dbuser->country, $user1['country']);
$this->assertNotEquals(0, $dbuser->picture, 'Picture must be set to the new icon itemid for this user');
$this->assertEquals('atto', get_user_preferences('htmleditor', null, $dbuser));
$this->assertEquals(null, get_user_preferences('invalidpreference', null, $dbuser));
// Confirm no picture change when parameter is not supplied.
unset($user1['userpicture']);
@@ -952,13 +972,13 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
$this->setAdminUser();
$preferences = array(
array(
'name' => 'some_random_pref',
'value' => 'abc',
'name' => 'htmleditor',
'value' => 'atto',
'userid' => $user1->id,
),
array(
'name' => 'some_random_pref',
'value' => 'def',
'name' => 'htmleditor',
'value' => 'tinymce',
'userid' => $user2->id,
)
);
@@ -969,10 +989,39 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
$this->assertCount(2, $result['saved']);
// Get preference from DB to avoid cache.
$this->assertEquals('abc', $DB->get_field('user_preferences', 'value',
array('userid' => $user1->id, 'name' => 'some_random_pref')));
$this->assertEquals('def', $DB->get_field('user_preferences', 'value',
array('userid' => $user2->id, 'name' => 'some_random_pref')));
$this->assertEquals('atto', $DB->get_field('user_preferences', 'value',
array('userid' => $user1->id, 'name' => 'htmleditor')));
$this->assertEquals('tinymce', $DB->get_field('user_preferences', 'value',
array('userid' => $user2->id, 'name' => 'htmleditor')));
}
/**
* Test set_user_preferences
*/
public function test_set_user_preferences_save_invalid_pref() {
global $DB;
$this->resetAfterTest(true);
$user1 = self::getDataGenerator()->create_user();
// Save users preferences.
$this->setAdminUser();
$preferences = array(
array(
'name' => 'some_random_pref',
'value' => 'abc',
'userid' => $user1->id,
),
);
$result = core_user_external::set_user_preferences($preferences);
$result = external_api::clean_returnvalue(core_user_external::set_user_preferences_returns(), $result);
$this->assertCount(1, $result['warnings']);
$this->assertCount(0, $result['saved']);
$this->assertEquals('nopermission', $result['warnings'][0]['warningcode']);
// Nothing was written to DB.
$this->assertEmpty($DB->count_records('user_preferences', array('name' => 'some_random_pref')));
}
/**
@@ -1002,7 +1051,7 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
* Test set_user_preferences using an invalid preference
*/
public function test_set_user_preferences_invalid_preference() {
global $USER;
global $USER, $DB;
$this->resetAfterTest(true);
// Create a very long value.
@@ -1017,15 +1066,17 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
$result = core_user_external::set_user_preferences($preferences);
$result = external_api::clean_returnvalue(core_user_external::set_user_preferences_returns(), $result);
$this->assertCount(1, $result['warnings']);
$this->assertCount(0, $result['saved']);
$this->assertEquals('errorsavingpreference', $result['warnings'][0]['warningcode']);
$this->assertCount(0, $result['warnings']);
$this->assertCount(1, $result['saved']);
// Cleaned valud of the preference was saved.
$this->assertEquals(1, $DB->get_field('user_preferences', 'value',
array('userid' => $USER->id, 'name' => 'calendar_maxevents')));
}
/**
* Test set_user_preferences for other user not being admin
*/
public function test_set_user_preferences_other_user_not_being_admin() {
public function test_set_user_preferences_capability() {
$this->resetAfterTest(true);
$user1 = self::getDataGenerator()->create_user();
@@ -1040,8 +1091,12 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
)
);
$this->expectException('required_capability_exception');
$result = core_user_external::set_user_preferences($preferences);
$this->assertCount(1, $result['warnings']);
$this->assertCount(0, $result['saved']);
$this->assertEquals('nopermission', $result['warnings'][0]['warningcode']);
$this->assertEquals($user2->id, $result['warnings'][0]['itemid']);
}
/**