���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/public_html/user.tar
���ѧ٧ѧ�
editor.php 0000644 00000005023 15151162244 0006544 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Allows you to edit a users editor preferences * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once('../config.php'); require_once($CFG->libdir.'/gdlib.php'); require_once($CFG->dirroot.'/user/editor_form.php'); require_once($CFG->dirroot.'/user/editlib.php'); require_once($CFG->dirroot.'/user/lib.php'); $userid = optional_param('id', $USER->id, PARAM_INT); // User id. $courseid = optional_param('course', SITEID, PARAM_INT); // Course id (defaults to Site). $PAGE->set_url('/user/editor.php', array('id' => $userid, 'course' => $courseid)); list($user, $course) = useredit_setup_preference_page($userid, $courseid); // Create form. $editorform = new user_edit_editor_form(); $user->preference_htmleditor = get_user_preferences( 'htmleditor', '', $user->id); $editorform->set_data($user); $redirect = new moodle_url("/user/preferences.php", array('userid' => $user->id)); if ($editorform->is_cancelled()) { redirect($redirect); } else if ($data = $editorform->get_data()) { $user->preference_htmleditor = $data->preference_htmleditor; useredit_update_user_preference($user); // Trigger event. \core\event\user_updated::create_from_userid($user->id)->trigger(); redirect($redirect, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } // Display page header. $streditmyeditor = get_string('editorpreferences'); $userfullname = fullname($user, true); $PAGE->navbar->includesettingsbase = true; $PAGE->add_body_class('limitedwidth'); $PAGE->set_title("$course->shortname: $streditmyeditor"); $PAGE->set_heading($userfullname); echo $OUTPUT->header(); echo $OUTPUT->heading($streditmyeditor); // Finally display THE form. $editorform->display(); // And proper footer. echo $OUTPUT->footer(); files.php 0000644 00000003471 15151162244 0006365 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Manage files in folder in private area. * * @package core_user * @category files * @copyright 2010 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../config.php'); require_login(); if (isguestuser()) { die(); } $context = context_user::instance($USER->id); require_capability('moodle/user:manageownfiles', $context); $title = get_string('privatefiles'); $PAGE->set_url('/user/files.php'); $PAGE->set_context($context); $PAGE->set_title($title); $PAGE->set_heading(fullname($USER)); $PAGE->set_pagelayout('standard'); $PAGE->add_body_class('limitedwidth'); $PAGE->set_pagetype('user-files'); echo $OUTPUT->header(); echo $OUTPUT->heading($title); echo $OUTPUT->box_start('generalbox'); echo html_writer::start_div('', ['id' => 'userfilesform']); $form = new \core_user\form\private_files(); $form->set_data_for_dynamic_submission(); $form->display(); echo html_writer::end_div(); $PAGE->requires->js_call_amd('core_user/private_files', 'initDynamicForm', ['#userfilesform', \core_user\form\private_files::class]); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); editadvanced.php 0000644 00000033220 15151162244 0007671 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Allows you to edit a users profile * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once('../config.php'); require_once($CFG->libdir.'/gdlib.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/user/editadvanced_form.php'); require_once($CFG->dirroot.'/user/editlib.php'); require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); require_once($CFG->dirroot.'/webservice/lib.php'); $id = optional_param('id', $USER->id, PARAM_INT); // User id; -1 if creating new user. $course = optional_param('course', SITEID, PARAM_INT); // Course id (defaults to Site). $returnto = optional_param('returnto', null, PARAM_ALPHA); // Code determining where to return to after save. $PAGE->set_url('/user/editadvanced.php', array('course' => $course, 'id' => $id)); $course = $DB->get_record('course', array('id' => $course), '*', MUST_EXIST); if (!empty($USER->newadminuser)) { // Ignore double clicks, we must finish all operations before cancelling request. ignore_user_abort(true); $PAGE->set_course($SITE); $PAGE->set_pagelayout('maintenance'); } else { if ($course->id == SITEID) { require_login(); $PAGE->set_context(context_system::instance()); } else { require_login($course); } $PAGE->set_pagelayout('admin'); $PAGE->add_body_class('limitedwidth'); } if ($course->id == SITEID) { $coursecontext = context_system::instance(); // SYSTEM context. } else { $coursecontext = context_course::instance($course->id); // Course context. } $systemcontext = context_system::instance(); if ($id == -1) { // Creating new user. $user = new stdClass(); $user->id = -1; $user->auth = 'manual'; $user->confirmed = 1; $user->deleted = 0; $user->timezone = '99'; require_capability('moodle/user:create', $systemcontext); admin_externalpage_setup('addnewuser', '', array('id' => -1)); $PAGE->set_primary_active_tab('siteadminnode'); $PAGE->navbar->add(get_string('addnewuser', 'moodle'), $PAGE->url); } else { // Editing existing user. require_capability('moodle/user:update', $systemcontext); $user = $DB->get_record('user', array('id' => $id), '*', MUST_EXIST); $PAGE->set_context(context_user::instance($user->id)); $PAGE->navbar->includesettingsbase = true; if ($user->id != $USER->id) { $PAGE->navigation->extend_for_user($user); } else { if ($node = $PAGE->navigation->find('myprofile', navigation_node::TYPE_ROOTNODE)) { $node->force_open(); } } } // Remote users cannot be edited. if ($user->id != -1 and is_mnet_remote_user($user)) { redirect($CFG->wwwroot . "/user/view.php?id=$id&course={$course->id}"); } if ($user->id != $USER->id and is_siteadmin($user) and !is_siteadmin($USER)) { // Only admins may edit other admins. throw new \moodle_exception('useradmineditadmin'); } if (isguestuser($user->id)) { // The real guest user can not be edited. throw new \moodle_exception('guestnoeditprofileother'); } if ($user->deleted) { echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('userdeleted')); echo $OUTPUT->footer(); die; } // Load user preferences. useredit_load_preferences($user); // Load custom profile fields data. profile_load_data($user); // User interests. $user->interests = core_tag_tag::get_item_tags_array('core', 'user', $id); if ($user->id !== -1) { $usercontext = context_user::instance($user->id); $editoroptions = array( 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $CFG->maxbytes, 'trusttext' => false, 'forcehttps' => false, 'context' => $usercontext ); $user = file_prepare_standard_editor($user, 'description', $editoroptions, $usercontext, 'user', 'profile', 0); } else { $usercontext = null; // This is a new user, we don't want to add files here. $editoroptions = array( 'maxfiles' => 0, 'maxbytes' => 0, 'trusttext' => false, 'forcehttps' => false, 'context' => $coursecontext ); } // Prepare filemanager draft area. $draftitemid = 0; $filemanagercontext = $editoroptions['context']; $filemanageroptions = array('maxbytes' => $CFG->maxbytes, 'subdirs' => 0, 'maxfiles' => 1, 'accepted_types' => 'optimised_image'); file_prepare_draft_area($draftitemid, $filemanagercontext->id, 'user', 'newicon', 0, $filemanageroptions); $user->imagefile = $draftitemid; // Create form. $userform = new user_editadvanced_form(new moodle_url($PAGE->url, array('returnto' => $returnto)), array( 'editoroptions' => $editoroptions, 'filemanageroptions' => $filemanageroptions, 'user' => $user)); // Deciding where to send the user back in most cases. if ($returnto === 'profile') { if ($course->id != SITEID) { $returnurl = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id)); } else { $returnurl = new moodle_url('/user/profile.php', array('id' => $user->id)); } } else if ($user->id === -1) { $returnurl = new moodle_url("/admin/user.php"); } else { $returnurl = new moodle_url('/user/preferences.php', array('userid' => $user->id)); } if ($userform->is_cancelled()) { redirect($returnurl); } else if ($usernew = $userform->get_data()) { $usercreated = false; if (empty($usernew->auth)) { // User editing self. $authplugin = get_auth_plugin($user->auth); unset($usernew->auth); // Can not change/remove. } else { $authplugin = get_auth_plugin($usernew->auth); } $usernew->timemodified = time(); $createpassword = false; if ($usernew->id == -1) { unset($usernew->id); $createpassword = !empty($usernew->createpassword); unset($usernew->createpassword); $usernew = file_postupdate_standard_editor($usernew, 'description', $editoroptions, null, 'user', 'profile', null); $usernew->mnethostid = $CFG->mnet_localhost_id; // Always local user. $usernew->confirmed = 1; $usernew->timecreated = time(); if ($authplugin->is_internal()) { if ($createpassword or empty($usernew->newpassword)) { $usernew->password = ''; } else { $usernew->password = hash_internal_user_password($usernew->newpassword); } } else { $usernew->password = AUTH_PASSWORD_NOT_CACHED; } $usernew->id = user_create_user($usernew, false, false); if (!$authplugin->is_internal() and $authplugin->can_change_password() and !empty($usernew->newpassword)) { if (!$authplugin->user_update_password($usernew, $usernew->newpassword)) { // Do not stop here, we need to finish user creation. debugging(get_string('cannotupdatepasswordonextauth', 'error', $usernew->auth), DEBUG_NONE); } } $usercreated = true; } else { $usernew = file_postupdate_standard_editor($usernew, 'description', $editoroptions, $usercontext, 'user', 'profile', 0); // Pass a true old $user here. if (!$authplugin->user_update($user, $usernew)) { // Auth update failed. throw new \moodle_exception('cannotupdateuseronexauth', '', '', $user->auth); } user_update_user($usernew, false, false); // Set new password if specified. if (!empty($usernew->newpassword)) { if ($authplugin->can_change_password()) { if (!$authplugin->user_update_password($usernew, $usernew->newpassword)) { throw new \moodle_exception('cannotupdatepasswordonextauth', '', '', $usernew->auth); } unset_user_preference('create_password', $usernew); // Prevent cron from generating the password. if (!empty($CFG->passwordchangelogout)) { // We can use SID of other user safely here because they are unique, // the problem here is we do not want to logout admin here when changing own password. \core\session\manager::kill_user_sessions($usernew->id, session_id()); } if (!empty($usernew->signoutofotherservices)) { webservice::delete_user_ws_tokens($usernew->id); } } } // Force logout if user just suspended. if (isset($usernew->suspended) and $usernew->suspended and !$user->suspended) { \core\session\manager::kill_user_sessions($user->id); } } $usercontext = context_user::instance($usernew->id); // Update preferences. useredit_update_user_preference($usernew); // Update tags. if (empty($USER->newadminuser) && isset($usernew->interests)) { useredit_update_interests($usernew, $usernew->interests); } // Update user picture. if (empty($USER->newadminuser)) { core_user::update_picture($usernew, $filemanageroptions); } // Update mail bounces. useredit_update_bounces($user, $usernew); // Update forum track preference. useredit_update_trackforums($user, $usernew); // Save custom profile fields data. profile_save_data($usernew); // Reload from db. $usernew = $DB->get_record('user', array('id' => $usernew->id)); if ($createpassword) { setnew_password_and_mail($usernew); unset_user_preference('create_password', $usernew); set_user_preference('auth_forcepasswordchange', 1, $usernew); } // Trigger update/create event, after all fields are stored. if ($usercreated) { \core\event\user_created::create_from_userid($usernew->id)->trigger(); } else { \core\event\user_updated::create_from_userid($usernew->id)->trigger(); } if ($user->id == $USER->id) { // Override old $USER session variable. foreach ((array)$usernew as $variable => $value) { if ($variable === 'description' or $variable === 'password') { // These are not set for security nad perf reasons. continue; } $USER->$variable = $value; } // Preload custom fields. profile_load_custom_fields($USER); if (!empty($USER->newadminuser)) { unset($USER->newadminuser); // Apply defaults again - some of them might depend on admin user info, backup, roles, etc. admin_apply_default_settings(null, false); // Admin account is fully configured - set flag here in case the redirect does not work. unset_config('adminsetuppending'); // Redirect to admin/ to continue with installation. redirect("$CFG->wwwroot/$CFG->admin/"); } else if (empty($SITE->fullname)) { // Somebody double clicked when editing admin user during install. redirect("$CFG->wwwroot/$CFG->admin/"); } else { redirect($returnurl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } } else if ($returnto === 'profile') { \core\session\manager::gc(); // Remove stale sessions. redirect($returnurl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } else { \core\session\manager::gc(); // Remove stale sessions. redirect("$CFG->wwwroot/$CFG->admin/user.php", get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } // Never reached.. } // Display page header. if ($user->id == -1 or ($user->id != $USER->id)) { if ($user->id == -1) { echo $OUTPUT->header(); } else { $streditmyprofile = get_string('editmyprofile'); $userfullname = fullname($user, true); $PAGE->set_heading($userfullname); $PAGE->set_title("$course->shortname: $streditmyprofile - $userfullname"); echo $OUTPUT->header(); echo $OUTPUT->heading($userfullname); } } else if (!empty($USER->newadminuser)) { $strinstallation = get_string('installation', 'install'); $strprimaryadminsetup = get_string('primaryadminsetup'); $PAGE->navbar->add($strprimaryadminsetup); $PAGE->set_title($strinstallation); $PAGE->set_heading($strinstallation); $PAGE->set_cacheable(false); echo $OUTPUT->header(); echo $OUTPUT->box(get_string('configintroadmin', 'admin'), 'generalbox boxwidthnormal boxaligncenter'); echo '<br />'; } else { $streditmyprofile = get_string('editmyprofile'); $strparticipants = get_string('participants'); $strnewuser = get_string('newuser'); $userfullname = fullname($user, true); $PAGE->set_title("$course->shortname: $streditmyprofile"); $PAGE->set_heading($userfullname); echo $OUTPUT->header(); echo $OUTPUT->heading($streditmyprofile); } // Finally display THE form. $userform->display(); // And proper footer. echo $OUTPUT->footer(); edit_form.php 0000644 00000023036 15151162244 0007232 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form to edit a users profile * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. } require_once($CFG->dirroot.'/lib/formslib.php'); /** * Class user_edit_form. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_edit_form extends moodleform { /** * Define the form. */ public function definition () { global $CFG, $COURSE, $USER; $mform = $this->_form; $editoroptions = null; $filemanageroptions = null; $usernotfullysetup = user_not_fully_set_up($USER); if (!is_array($this->_customdata)) { throw new coding_exception('invalid custom data for user_edit_form'); } $editoroptions = $this->_customdata['editoroptions']; $filemanageroptions = $this->_customdata['filemanageroptions']; $user = $this->_customdata['user']; $userid = $user->id; if (empty($user->country)) { // We must unset the value here so $CFG->country can be used as default one. unset($user->country); } // Accessibility: "Required" is bad legend text. $strgeneral = get_string('general'); $strrequired = get_string('required'); // Add some extra hidden fields. $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'course', $COURSE->id); $mform->setType('course', PARAM_INT); // Print the required moodle fields first. $mform->addElement('header', 'moodle', $strgeneral); // Shared fields. useredit_shared_definition($mform, $editoroptions, $filemanageroptions, $user); // Extra settigs. if (!empty($CFG->disableuserimages) || $usernotfullysetup) { $mform->removeElement('deletepicture'); $mform->removeElement('imagefile'); $mform->removeElement('imagealt'); } // If the user isn't fully set up, let them know that they will be able to change // their profile picture once their profile is complete. if ($usernotfullysetup) { $userpicturewarning = $mform->createElement('warning', 'userpicturewarning', 'notifymessage', get_string('newpictureusernotsetup')); $enabledusernamefields = useredit_get_enabled_name_fields(); if ($mform->elementExists('moodle_additional_names')) { $mform->insertElementBefore($userpicturewarning, 'moodle_additional_names'); } else if ($mform->elementExists('moodle_interests')) { $mform->insertElementBefore($userpicturewarning, 'moodle_interests'); } else { $mform->insertElementBefore($userpicturewarning, 'moodle_optional'); } // This is expected to exist when the form is submitted. $imagefile = $mform->createElement('hidden', 'imagefile'); $mform->insertElementBefore($imagefile, 'userpicturewarning'); } // Next the customisable profile fields. profile_definition($mform, $userid); $this->add_action_buttons(true, get_string('updatemyprofile')); $this->set_data($user); } /** * Extend the form definition after the data has been parsed. */ public function definition_after_data() { global $CFG, $DB, $OUTPUT; $mform = $this->_form; $userid = $mform->getElementValue('id'); // Trim required name fields. foreach (useredit_get_required_name_fields() as $field) { $mform->applyFilter($field, 'trim'); } if ($user = $DB->get_record('user', array('id' => $userid))) { // Remove description. if (empty($user->description) && !empty($CFG->profilesforenrolledusersonly) && !$DB->record_exists('role_assignments', array('userid' => $userid))) { $mform->removeElement('description_editor'); } // Print picture. $context = context_user::instance($user->id, MUST_EXIST); $fs = get_file_storage(); $hasuploadedpicture = ($fs->file_exists($context->id, 'user', 'icon', 0, '/', 'f2.png') || $fs->file_exists($context->id, 'user', 'icon', 0, '/', 'f2.jpg')); if (!empty($user->picture) && $hasuploadedpicture) { $imagevalue = $OUTPUT->user_picture($user, array('courseid' => SITEID, 'size' => 64)); } else { $imagevalue = get_string('none'); } $imageelement = $mform->getElement('currentpicture'); $imageelement->setValue($imagevalue); if ($mform->elementExists('deletepicture') && !$hasuploadedpicture) { $mform->removeElement('deletepicture'); } // Disable fields that are locked by auth plugins. $fields = get_user_fieldnames(); $authplugin = get_auth_plugin($user->auth); $customfields = $authplugin->get_custom_user_profile_fields(); $customfieldsdata = profile_user_record($userid, false); $fields = array_merge($fields, $customfields); foreach ($fields as $field) { if ($field === 'description') { // Hard coded hack for description field. See MDL-37704 for details. $formfield = 'description_editor'; } else { $formfield = $field; } if (!$mform->elementExists($formfield)) { continue; } // Get the original value for the field. if (in_array($field, $customfields)) { $key = str_replace('profile_field_', '', $field); $value = isset($customfieldsdata->{$key}) ? $customfieldsdata->{$key} : ''; } else { $value = $user->{$field}; } $configvariable = 'field_lock_' . $field; if (isset($authplugin->config->{$configvariable})) { if ($authplugin->config->{$configvariable} === 'locked') { $mform->hardFreeze($formfield); $mform->setConstant($formfield, $value); } else if ($authplugin->config->{$configvariable} === 'unlockedifempty' and $value != '') { $mform->hardFreeze($formfield); $mform->setConstant($formfield, $value); } } } // Next the customisable profile fields. profile_definition_after_data($mform, $user->id); } else { profile_definition_after_data($mform, 0); } } /** * Validate incoming form data. * @param array $usernew * @param array $files * @return array */ public function validation($usernew, $files) { global $CFG, $DB; $errors = parent::validation($usernew, $files); $usernew = (object)$usernew; $user = $DB->get_record('user', array('id' => $usernew->id)); // Validate email. if (!isset($usernew->email)) { // Mail not confirmed yet. } else if (!validate_email($usernew->email)) { $errors['email'] = get_string('invalidemail'); } else if (($usernew->email !== $user->email) && empty($CFG->allowaccountssameemail)) { // Make a case-insensitive query for the given email address. $select = $DB->sql_equal('email', ':email', false) . ' AND mnethostid = :mnethostid AND id <> :userid'; $params = array( 'email' => $usernew->email, 'mnethostid' => $CFG->mnet_localhost_id, 'userid' => $usernew->id ); // If there are other user(s) that already have the same email, show an error. if ($DB->record_exists_select('user', $select, $params)) { $errors['email'] = get_string('emailexists'); } } if (isset($usernew->email) and $usernew->email === $user->email and over_bounce_threshold($user)) { $errors['email'] = get_string('toomanybounces'); } if (isset($usernew->email) and !empty($CFG->verifychangedemail) and !isset($errors['email']) and !has_capability('moodle/user:update', context_system::instance())) { $errorstr = email_is_not_allowed($usernew->email); if ($errorstr !== false) { $errors['email'] = $errorstr; } } // Next the customisable profile fields. $errors += profile_validation($usernew, $files); return $errors; } } preferences.php 0000644 00000006100 15151162244 0007554 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Preferences. * * @package core_user * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../config.php'); require_once($CFG->libdir . '/navigationlib.php'); require_login(null, false); if (isguestuser()) { throw new require_login_exception('Guests are not allowed here.'); } $userid = optional_param('userid', $USER->id, PARAM_INT); $currentuser = $userid == $USER->id; // Check that the user is a valid user. $user = core_user::get_user($userid); if (!$user || !core_user::is_real_user($userid)) { throw new moodle_exception('invaliduser', 'error'); } $PAGE->set_context(context_user::instance($userid)); $PAGE->set_url('/user/preferences.php', array('userid' => $userid)); $PAGE->set_pagelayout('admin'); $PAGE->add_body_class('limitedwidth'); $PAGE->set_pagetype('user-preferences'); $PAGE->set_title(get_string('preferences')); $PAGE->set_heading(fullname($user)); if (!$currentuser) { $PAGE->navigation->extend_for_user($user); // Need to check that settings exist. if ($settings = $PAGE->settingsnav->find('userviewingsettings' . $user->id, null)) { $settings->make_active(); } $url = new moodle_url('/user/preferences.php', array('userid' => $userid)); $navbar = $PAGE->navbar->add(get_string('preferences', 'moodle'), $url); // Show an error if there are no preferences that this user has access to. if (!$PAGE->settingsnav->can_view_user_preferences($userid)) { throw new moodle_exception('cannotedituserpreferences', 'error'); } } else { // Shutdown the users node in the navigation menu. $usernode = $PAGE->navigation->find('users', null); $usernode->make_inactive(); $settings = $PAGE->settingsnav->find('usercurrentsettings', null); $settings->make_active(); } // Identifying the nodes. $groups = array(); $orphans = array(); foreach ($settings->children as $setting) { if ($setting->has_children()) { $groups[] = new preferences_group($setting->get_content(), $setting->children); } else { $orphans[] = $setting; } } if (!empty($orphans)) { $groups[] = new preferences_group(get_string('miscellaneous'), $orphans); } $preferences = new preferences_groups($groups); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('preferences')); echo $OUTPUT->render($preferences); echo $OUTPUT->footer(); edit.php 0000644 00000026632 15151162244 0006214 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Allows you to edit a users profile * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once('../config.php'); require_once($CFG->libdir.'/gdlib.php'); require_once($CFG->dirroot.'/user/edit_form.php'); require_once($CFG->dirroot.'/user/editlib.php'); require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); $userid = optional_param('id', $USER->id, PARAM_INT); // User id. $course = optional_param('course', SITEID, PARAM_INT); // Course id (defaults to Site). $returnto = optional_param('returnto', null, PARAM_ALPHA); // Code determining where to return to after save. $cancelemailchange = optional_param('cancelemailchange', 0, PARAM_INT); // Course id (defaults to Site). $PAGE->set_url('/user/edit.php', array('course' => $course, 'id' => $userid)); if (!$course = $DB->get_record('course', array('id' => $course))) { throw new \moodle_exception('invalidcourseid'); } if ($course->id != SITEID) { require_login($course); } else if (!isloggedin()) { if (empty($SESSION->wantsurl)) { $SESSION->wantsurl = $CFG->wwwroot.'/user/edit.php'; } redirect(get_login_url()); } else { $PAGE->set_context(context_system::instance()); } // Guest can not edit. if (isguestuser()) { throw new \moodle_exception('guestnoeditprofile'); } // The user profile we are editing. if (!$user = $DB->get_record('user', array('id' => $userid))) { throw new \moodle_exception('invaliduserid'); } // Guest can not be edited. if (isguestuser($user)) { throw new \moodle_exception('guestnoeditprofile'); } // User interests separated by commas. $user->interests = core_tag_tag::get_item_tags_array('core', 'user', $user->id); // Remote users cannot be edited. Note we have to perform the strict user_not_fully_set_up() check. // Otherwise the remote user could end up in endless loop between user/view.php and here. // Required custom fields are not supported in MNet environment anyway. if (is_mnet_remote_user($user)) { if (user_not_fully_set_up($user, true)) { $hostwwwroot = $DB->get_field('mnet_host', 'wwwroot', array('id' => $user->mnethostid)); throw new \moodle_exception('usernotfullysetup', 'mnet', '', $hostwwwroot); } redirect($CFG->wwwroot . "/user/view.php?course={$course->id}"); } // Load the appropriate auth plugin. $userauth = get_auth_plugin($user->auth); if (!$userauth->can_edit_profile()) { throw new \moodle_exception('noprofileedit', 'auth'); } if ($editurl = $userauth->edit_profile_url()) { // This internal script not used. redirect($editurl); } if ($course->id == SITEID) { $coursecontext = context_system::instance(); // SYSTEM context. } else { $coursecontext = context_course::instance($course->id); // Course context. } $systemcontext = context_system::instance(); $personalcontext = context_user::instance($user->id); // Check access control. if ($user->id == $USER->id) { // Editing own profile - require_login() MUST NOT be used here, it would result in infinite loop! if (!has_capability('moodle/user:editownprofile', $systemcontext)) { throw new \moodle_exception('cannotedityourprofile'); } } else { // Teachers, parents, etc. require_capability('moodle/user:editprofile', $personalcontext); // No editing of guest user account. if (isguestuser($user->id)) { throw new \moodle_exception('guestnoeditprofileother'); } // No editing of primary admin! if (is_siteadmin($user) and !is_siteadmin($USER)) { // Only admins may edit other admins. throw new \moodle_exception('useradmineditadmin'); } } if ($user->deleted) { echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('userdeleted')); echo $OUTPUT->footer(); die; } $PAGE->set_pagelayout('admin'); $PAGE->add_body_class('limitedwidth'); $PAGE->set_context($personalcontext); if ($USER->id != $user->id) { $PAGE->navigation->extend_for_user($user); } else { if ($node = $PAGE->navigation->find('myprofile', navigation_node::TYPE_ROOTNODE)) { $node->force_open(); } } // Process email change cancellation. if ($cancelemailchange) { cancel_email_update($user->id); } // Load user preferences. useredit_load_preferences($user); // Load custom profile fields data. profile_load_data($user); // Prepare the editor and create form. $editoroptions = array( 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $CFG->maxbytes, 'trusttext' => false, 'forcehttps' => false, 'context' => $personalcontext ); $user = file_prepare_standard_editor($user, 'description', $editoroptions, $personalcontext, 'user', 'profile', 0); // Prepare filemanager draft area. $draftitemid = 0; $filemanagercontext = $editoroptions['context']; $filemanageroptions = array('maxbytes' => $CFG->maxbytes, 'subdirs' => 0, 'maxfiles' => 1, 'accepted_types' => 'optimised_image'); file_prepare_draft_area($draftitemid, $filemanagercontext->id, 'user', 'newicon', 0, $filemanageroptions); $user->imagefile = $draftitemid; // Create form. $userform = new user_edit_form(new moodle_url($PAGE->url, array('returnto' => $returnto)), array( 'editoroptions' => $editoroptions, 'filemanageroptions' => $filemanageroptions, 'user' => $user)); $emailchanged = false; // Deciding where to send the user back in most cases. if ($returnto === 'profile') { if ($course->id != SITEID) { $returnurl = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id)); } else { $returnurl = new moodle_url('/user/profile.php', array('id' => $user->id)); } } else { $returnurl = new moodle_url('/user/preferences.php', array('userid' => $user->id)); } if ($userform->is_cancelled()) { redirect($returnurl); } else if ($usernew = $userform->get_data()) { $emailchangedhtml = ''; if ($CFG->emailchangeconfirmation) { // Users with 'moodle/user:update' can change their email address immediately. // Other users require a confirmation email. if (isset($usernew->email) and $user->email != $usernew->email && !has_capability('moodle/user:update', $systemcontext)) { $a = new stdClass(); $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); } } $authplugin = get_auth_plugin($user->auth); $usernew->timemodified = time(); // Description editor element may not exist! if (isset($usernew->description_editor) && isset($usernew->description_editor['format'])) { $usernew = file_postupdate_standard_editor($usernew, 'description', $editoroptions, $personalcontext, 'user', 'profile', 0); } // Pass a true old $user here. if (!$authplugin->user_update($user, $usernew)) { // Auth update failed. throw new \moodle_exception('cannotupdateprofile'); } // Update user with new profile data. user_update_user($usernew, false, false); // Update preferences. useredit_update_user_preference($usernew); // Update interests. if (isset($usernew->interests)) { useredit_update_interests($usernew, $usernew->interests); } // Update user picture. if (empty($CFG->disableuserimages)) { core_user::update_picture($usernew, $filemanageroptions); } // Update mail bounces. useredit_update_bounces($user, $usernew); // Update forum track preference. useredit_update_trackforums($user, $usernew); // Save custom profile fields data. profile_save_data($usernew); // Trigger event. \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 !== false && $CFG->emailchangeconfirmation) { $tempuser = $DB->get_record('user', array('id' => $user->id), '*', MUST_EXIST); $tempuser->email = $emailchanged; $a = new stdClass(); $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 = $OUTPUT->supportemail(); $emailupdatemessage = get_string('emailupdatemessage', 'auth', $a); $emailupdatetitle = get_string('emailupdatetitle', 'auth', $a); // Email confirmation directly rather than using messaging so they will definitely get an email. $noreplyuser = core_user::get_noreply_user(); if (!$mailresults = email_to_user($tempuser, $noreplyuser, $emailupdatetitle, $emailupdatemessage)) { die("could not send email!"); } } // Reload from db, we need new full name on this page if we do not redirect. $user = $DB->get_record('user', array('id' => $user->id), '*', MUST_EXIST); if ($USER->id == $user->id) { // Override old $USER session variable if needed. foreach ((array)$user as $variable => $value) { if ($variable === 'description' or $variable === 'password') { // These are not set for security nad perf reasons. continue; } $USER->$variable = $value; } // Preload custom fields. profile_load_custom_fields($USER); } if (is_siteadmin() and empty($SITE->shortname)) { // Fresh cli install - we need to finish site settings. redirect(new moodle_url('/admin/index.php')); } if (!$emailchanged || !$CFG->emailchangeconfirmation) { redirect($returnurl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } } // Display page header. $streditmyprofile = get_string('editmyprofile'); $strparticipants = get_string('participants'); $userfullname = fullname($user, true); $PAGE->set_title("$course->shortname: $streditmyprofile"); $PAGE->set_heading($userfullname); echo $OUTPUT->header(); echo $OUTPUT->heading($userfullname); if ($emailchanged) { echo $emailchangedhtml; } else { // Finally display THE form. $userform->display(); } // And proper footer. echo $OUTPUT->footer(); action_redir.php 0000644 00000026754 15151162244 0007736 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Wrapper script redirecting user operations to correct destination. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once("../config.php"); require_once($CFG->dirroot . '/course/lib.php'); $formaction = required_param('formaction', PARAM_LOCALURL); $id = required_param('id', PARAM_INT); $PAGE->set_url('/user/action_redir.php', array('formaction' => $formaction, 'id' => $id)); list($formaction) = explode('?', $formaction, 2); // This page now only handles the bulk enrolment change actions, other actions are done with ajax. $actions = array('bulkchange.php'); if (array_search($formaction, $actions) === false) { throw new \moodle_exception('unknownuseraction'); } if (!confirm_sesskey()) { throw new \moodle_exception('confirmsesskeybad'); } if ($formaction == 'bulkchange.php') { // Backwards compatibility for enrolment plugins bulk change functionality. // This awful code is adapting from the participant page with it's param names and values // to the values expected by the bulk enrolment changes forms. $formaction = required_param('formaction', PARAM_URL); require_once($CFG->dirroot . '/enrol/locallib.php'); $url = new moodle_url($formaction); // Get the enrolment plugin type and bulk action from the url. $plugin = $url->param('plugin'); $operationname = $url->param('operation'); $dataformat = $url->param('dataformat'); $course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); $context = context_course::instance($id); $PAGE->set_context($context); $userids = optional_param_array('userid', array(), PARAM_INT); $default = new moodle_url('/user/index.php', ['id' => $course->id]); $returnurl = new moodle_url(optional_param('returnto', $default, PARAM_LOCALURL)); if (empty($userids)) { $userids = optional_param_array('bulkuser', array(), PARAM_INT); } if (empty($userids)) { // The first time list hack. if (empty($userids) and $post = data_submitted()) { foreach ($post as $k => $v) { if (preg_match('/^user(\d+)$/', $k, $m)) { $userids[] = $m[1]; } } } } if (empty($plugin) AND $operationname == 'download_participants') { // Check permissions. $pagecontext = ($course->id == SITEID) ? context_system::instance() : $context; if (course_can_view_participants($pagecontext)) { $plugins = core_plugin_manager::instance()->get_plugins_of_type('dataformat'); if (isset($plugins[$dataformat])) { if ($plugins[$dataformat]->is_enabled()) { if (empty($userids)) { redirect($returnurl, get_string('noselectedusers', 'bulkusers')); } $columnnames = array( 'firstname' => get_string('firstname'), 'lastname' => get_string('lastname'), ); // Get the list of fields we have to hide. $hiddenfields = []; if (!has_capability('moodle/course:viewhiddenuserfields', $context)) { $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); } // Retrieve all identity fields required for users. $userfieldsapi = \core_user\fields::for_identity($context); $userfields = $userfieldsapi->get_sql('u', true); $identityfields = array_keys($userfields->mappings); foreach ($identityfields as $field) { $columnnames[$field] = \core_user\fields::get_display_name($field); } // Ensure users are enrolled in this course context, further limiting them by selected userids. [$enrolledsql, $enrolledparams] = get_enrolled_sql($context); [$useridsql, $useridparams] = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'userid'); [$userordersql, $userorderparams] = users_order_by_sql('u', null, $context); $params = array_merge($userfields->params, $enrolledparams, $useridparams, $userorderparams); // If user can only view their own groups then they can only export users from those groups too. $groupmode = groups_get_course_groupmode($course); if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) { $groups = groups_get_all_groups($course->id, $USER->id, 0, 'g.id'); $groupids = array_column($groups, 'id'); [$groupmembersql, $groupmemberparams] = groups_get_members_ids_sql($groupids, $context); $params = array_merge($params, $groupmemberparams); $groupmemberjoin = "JOIN ({$groupmembersql}) jg ON jg.id = u.id"; } else { $groupmemberjoin = ''; } // Add column for groups if the user can view them. if (!isset($hiddenfields['groups'])) { $columnnames['groupnames'] = get_string('groups'); $userfields->selects .= ', gcn.groupnames'; [$groupconcatnamesql, $groupconcatnameparams] = groups_get_names_concat_sql($course->id); $groupconcatjoin = "LEFT JOIN ({$groupconcatnamesql}) gcn ON gcn.userid = u.id"; $params = array_merge($params, $groupconcatnameparams); } else { $groupconcatjoin = ''; } $sql = "SELECT u.firstname, u.lastname {$userfields->selects} FROM {user} u {$userfields->joins} JOIN ({$enrolledsql}) je ON je.id = u.id {$groupmemberjoin} {$groupconcatjoin} WHERE u.id {$useridsql} ORDER BY {$userordersql}"; $rs = $DB->get_recordset_sql($sql, $params); // Provide callback to pre-process all records ensuring user identity fields are escaped if HTML supported. \core\dataformat::download_data( 'courseid_' . $course->id . '_participants', $dataformat, $columnnames, $rs, function(stdClass $record, bool $supportshtml) use ($identityfields): stdClass { if ($supportshtml) { foreach ($identityfields as $identityfield) { $record->{$identityfield} = s($record->{$identityfield}); } } return $record; } ); $rs->close(); } } } } else { $instances = enrol_get_instances($course->id, false); $instance = false; foreach ($instances as $oneinstance) { if ($oneinstance->enrol == $plugin) { $instance = $oneinstance; break; } } if (!$instance) { throw new \moodle_exception('errorwithbulkoperation', 'enrol'); } $manager = new course_enrolment_manager($PAGE, $course, $instance->id); $plugins = $manager->get_enrolment_plugins(); if (!isset($plugins[$plugin])) { throw new \moodle_exception('errorwithbulkoperation', 'enrol'); } $plugin = $plugins[$plugin]; $operations = $plugin->get_bulk_operations($manager); if (!isset($operations[$operationname])) { throw new \moodle_exception('errorwithbulkoperation', 'enrol'); } $operation = $operations[$operationname]; if (empty($userids)) { redirect($returnurl, get_string('noselectedusers', 'bulkusers')); } $users = $manager->get_users_enrolments($userids); $removed = array_diff($userids, array_keys($users)); if (!empty($removed)) { // This manager does not filter by enrolment method - so we can get the removed users details. $removedmanager = new course_enrolment_manager($PAGE, $course); $removedusers = $removedmanager->get_users_enrolments($removed); foreach ($removedusers as $removeduser) { $msg = get_string('userremovedfromselectiona', 'enrol', fullname($removeduser)); \core\notification::warning($msg); } } // We may have users from any kind of enrolment, we need to filter for the enrolment plugin matching the bulk action. $matchesplugin = function($user) use ($plugin) { foreach ($user->enrolments as $enrolment) { if ($enrolment->enrolmentplugin->get_name() == $plugin->get_name()) { return true; } } return false; }; $filteredusers = array_filter($users, $matchesplugin); if (empty($filteredusers)) { redirect($returnurl, get_string('noselectedusers', 'bulkusers')); } $users = $filteredusers; // Get the form for the bulk operation. $mform = $operation->get_form($PAGE->url, array('users' => $users)); // If the mform is false then attempt an immediate process. This may be an immediate action that // doesn't require user input OR confirmation.... who know what but maybe one day. if ($mform === false) { if ($operation->process($manager, $users, new stdClass)) { redirect($returnurl); } else { throw new \moodle_exception('errorwithbulkoperation', 'enrol'); } } // Check if the bulk operation has been cancelled. if ($mform->is_cancelled()) { redirect($returnurl); } if ($mform->is_submitted() && $mform->is_validated() && confirm_sesskey()) { if ($operation->process($manager, $users, $mform->get_data())) { redirect($returnurl); } } $pagetitle = get_string('bulkuseroperation', 'enrol'); $PAGE->set_title($pagetitle); $PAGE->set_heading($pagetitle); echo $OUTPUT->header(); echo $OUTPUT->heading($operation->get_title()); $mform->display(); echo $OUTPUT->footer(); exit(); } } else { throw new coding_exception('invalidaction'); } profile/definelib.php 0000644 00000054310 15151162244 0010642 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the profile_define_base class. * * @package core_user * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_define_base * * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_define_base { /** * Prints out the form snippet for creating or editing a profile field * @param MoodleQuickForm $form instance of the moodleform class */ public function define_form(&$form) { $form->addElement('header', '_commonsettings', get_string('profilecommonsettings', 'admin')); $this->define_form_common($form); $form->addElement('header', '_specificsettings', get_string('profilespecificsettings', 'admin')); $this->define_form_specific($form); } /** * Prints out the form snippet for the part of creating or editing a profile field common to all data types. * * @param MoodleQuickForm $form instance of the moodleform class */ public function define_form_common(&$form) { $strrequired = get_string('required'); // Accepted values for 'shortname' would follow [a-zA-Z0-9_] pattern, // but we are accepting any PARAM_TEXT value here, // and checking [a-zA-Z0-9_] pattern in define_validate_common() function to throw an error when needed. $form->addElement('text', 'shortname', get_string('profileshortname', 'admin'), 'maxlength="100" size="25"'); $form->addRule('shortname', $strrequired, 'required', null, 'client'); $form->setType('shortname', PARAM_TEXT); $form->addElement('text', 'name', get_string('profilename', 'admin'), 'size="50"'); $form->addRule('name', $strrequired, 'required', null, 'client'); $form->setType('name', PARAM_TEXT); $form->addElement('editor', 'description', get_string('profiledescription', 'admin'), null, null); $form->addElement('selectyesno', 'required', get_string('profilerequired', 'admin')); $form->addElement('selectyesno', 'locked', get_string('profilelocked', 'admin')); $form->addElement('selectyesno', 'forceunique', get_string('profileforceunique', 'admin')); $form->addElement('selectyesno', 'signup', get_string('profilesignup', 'admin')); $choices = array(); $choices[PROFILE_VISIBLE_NONE] = get_string('profilevisiblenone', 'admin'); $choices[PROFILE_VISIBLE_PRIVATE] = get_string('profilevisibleprivate', 'admin'); $choices[PROFILE_VISIBLE_TEACHERS] = get_string('profilevisibleteachers', 'admin'); $choices[PROFILE_VISIBLE_ALL] = get_string('profilevisibleall', 'admin'); $form->addElement('select', 'visible', get_string('profilevisible', 'admin'), $choices); $form->addHelpButton('visible', 'profilevisible', 'admin'); $form->setDefault('visible', PROFILE_VISIBLE_ALL); $choices = profile_list_categories(); $form->addElement('select', 'categoryid', get_string('profilecategory', 'admin'), $choices); } /** * Prints out the form snippet for the part of creating or editing a profile field specific to the current data type. * @param MoodleQuickForm $form instance of the moodleform class */ public function define_form_specific($form) { // Do nothing - overwrite if necessary. } /** * Validate the data from the add/edit profile field form. * * Generally this method should not be overwritten by child classes. * * @param stdClass|array $data from the add/edit profile field form * @param array $files * @return array associative array of error messages */ public function define_validate($data, $files) { $data = (object)$data; $err = array(); $err += $this->define_validate_common($data, $files); $err += $this->define_validate_specific($data, $files); return $err; } /** * Validate the data from the add/edit profile field form that is common to all data types. * * Generally this method should not be overwritten by child classes. * * @param stdClass|array $data from the add/edit profile field form * @param array $files * @return array associative array of error messages */ public function define_validate_common($data, $files) { global $DB; $err = array(); // Check the shortname was not truncated by cleaning. if (empty($data->shortname)) { $err['shortname'] = get_string('required'); } else { // Check allowed pattern (numbers, letters and underscore). if (!preg_match('/^[a-zA-Z0-9_]+$/', $data->shortname)) { $err['shortname'] = get_string('profileshortnameinvalid', 'admin'); } else { // Fetch field-record from DB. $field = profile_get_custom_field_data_by_shortname($data->shortname); // Check the shortname is unique. if ($field and $field->id <> $data->id) { $err['shortname'] = get_string('profileshortnamenotunique', 'admin'); } // NOTE: since 2.0 the shortname may collide with existing fields in $USER because we load these fields into // $USER->profile array instead. } } // No further checks necessary as the form class will take care of it. return $err; } /** * Validate the data from the add/edit profile field form * that is specific to the current data type * @param array $data * @param array $files * @return array associative array of error messages */ public function define_validate_specific($data, $files) { // Do nothing - overwrite if necessary. return array(); } /** * Alter form based on submitted or existing data * @param MoodleQuickForm $mform */ public function define_after_data(&$mform) { // Do nothing - overwrite if necessary. } /** * Add a new profile field or save changes to current field * @param array|stdClass $data from the add/edit profile field form */ public function define_save($data) { global $DB; $data = $this->define_save_preprocess($data); // Hook for child classes. $old = false; if (!empty($data->id)) { $old = $DB->get_record('user_info_field', array('id' => (int)$data->id)); } // Check to see if the category has changed. if (!$old or $old->categoryid != $data->categoryid) { $data->sortorder = $DB->count_records('user_info_field', array('categoryid' => $data->categoryid)) + 1; } if (empty($data->id)) { unset($data->id); $data->id = $DB->insert_record('user_info_field', $data); } else { $DB->update_record('user_info_field', $data); } $field = $DB->get_record('user_info_field', array('id' => $data->id)); if ($old) { \core\event\user_info_field_updated::create_from_field($field)->trigger(); } else { \core\event\user_info_field_created::create_from_field($field)->trigger(); } profile_purge_user_fields_cache(); } /** * Preprocess data from the add/edit profile field form before it is saved. * * This method is a hook for the child classes to overwrite. * * @param array|stdClass $data from the add/edit profile field form * @return array|stdClass processed data object */ public function define_save_preprocess($data) { // Do nothing - overwrite if necessary. return $data; } /** * Provides a method by which we can allow the default data in profile_define_* to use an editor * * This should return an array of editor names (which will need to be formatted/cleaned) * * @return array */ public function define_editors() { return array(); } } /** * Reorder the profile fields within a given category starting at the field at the given startorder. */ function profile_reorder_fields() { global $DB; if ($categories = $DB->get_records('user_info_category')) { foreach ($categories as $category) { $i = 1; if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) { foreach ($fields as $field) { $f = new stdClass(); $f->id = $field->id; $f->sortorder = $i++; $DB->update_record('user_info_field', $f); } } } profile_purge_user_fields_cache(); } } /** * Reorder the profile categoriess starting at the category at the given startorder. */ function profile_reorder_categories() { global $DB; $i = 1; if ($categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) { foreach ($categories as $cat) { $c = new stdClass(); $c->id = $cat->id; $c->sortorder = $i++; $DB->update_record('user_info_category', $c); } profile_purge_user_fields_cache(); } } /** * Delete a profile category * @param int $id of the category to be deleted * @return bool success of operation */ function profile_delete_category($id) { global $DB; // Retrieve the category. if (!$category = $DB->get_record('user_info_category', array('id' => $id))) { throw new \moodle_exception('invalidcategoryid'); } if (!$categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) { throw new \moodle_exception('nocate', 'debug'); } unset($categories[$category->id]); if (!count($categories)) { return false; // We can not delete the last category. } // Does the category contain any fields. if ($DB->count_records('user_info_field', array('categoryid' => $category->id))) { if (array_key_exists($category->sortorder - 1, $categories)) { $newcategory = $categories[$category->sortorder - 1]; } else if (array_key_exists($category->sortorder + 1, $categories)) { $newcategory = $categories[$category->sortorder + 1]; } else { $newcategory = reset($categories); // Get first category if sortorder broken. } $sortorder = $DB->count_records('user_info_field', array('categoryid' => $newcategory->id)) + 1; if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) { foreach ($fields as $field) { $f = new stdClass(); $f->id = $field->id; $f->sortorder = $sortorder++; $f->categoryid = $newcategory->id; if ($DB->update_record('user_info_field', $f)) { $field->sortorder = $f->sortorder; $field->categoryid = $f->categoryid; \core\event\user_info_field_updated::create_from_field($field)->trigger(); } } } } // Finally we get to delete the category. $DB->delete_records('user_info_category', array('id' => $category->id)); profile_reorder_categories(); \core\event\user_info_category_deleted::create_from_category($category)->trigger(); profile_purge_user_fields_cache(); return true; } /** * Deletes a profile field. * @param int $id */ function profile_delete_field($id) { global $DB; // Remove any user data associated with this field. if (!$DB->delete_records('user_info_data', array('fieldid' => $id))) { throw new \moodle_exception('cannotdeletecustomfield'); } // Note: Any availability conditions that depend on this field will remain, // but show the field as missing until manually corrected to something else. // Need to rebuild course cache to update the info. rebuild_course_cache(0, true); // Prior to the delete, pull the record for the event. $field = $DB->get_record('user_info_field', array('id' => $id)); // Try to remove the record from the database. $DB->delete_records('user_info_field', array('id' => $id)); \core\event\user_info_field_deleted::create_from_field($field)->trigger(); profile_purge_user_fields_cache(); // Reorder the remaining fields in the same category. profile_reorder_fields(); } /** * Change the sort order of a field * * @param int $id of the field * @param string $move direction of move * @return bool success of operation */ function profile_move_field($id, $move) { global $DB; // Get the field object. if (!$field = $DB->get_record('user_info_field', array('id' => $id))) { return false; } // Count the number of fields in this category. $fieldcount = $DB->count_records('user_info_field', array('categoryid' => $field->categoryid)); // Calculate the new sortorder. if ( ($move == 'up') and ($field->sortorder > 1)) { $neworder = $field->sortorder - 1; } else if (($move == 'down') and ($field->sortorder < $fieldcount)) { $neworder = $field->sortorder + 1; } else { return false; } // Retrieve the field object that is currently residing in the new position. $params = array('categoryid' => $field->categoryid, 'sortorder' => $neworder); if ($swapfield = $DB->get_record('user_info_field', $params)) { // Swap the sortorders. $swapfield->sortorder = $field->sortorder; $field->sortorder = $neworder; // Update the field records. $DB->update_record('user_info_field', $field); $DB->update_record('user_info_field', $swapfield); \core\event\user_info_field_updated::create_from_field($field)->trigger(); \core\event\user_info_field_updated::create_from_field($swapfield)->trigger(); } profile_reorder_fields(); return true; } /** * Change the sort order of a category. * * @param int $id of the category * @param string $move direction of move * @return bool success of operation */ function profile_move_category($id, $move) { global $DB; // Get the category object. if (!($category = $DB->get_record('user_info_category', array('id' => $id)))) { return false; } // Count the number of categories. $categorycount = $DB->count_records('user_info_category'); // Calculate the new sortorder. if (($move == 'up') and ($category->sortorder > 1)) { $neworder = $category->sortorder - 1; } else if (($move == 'down') and ($category->sortorder < $categorycount)) { $neworder = $category->sortorder + 1; } else { return false; } // Retrieve the category object that is currently residing in the new position. if ($swapcategory = $DB->get_record('user_info_category', array('sortorder' => $neworder))) { // Swap the sortorders. $swapcategory->sortorder = $category->sortorder; $category->sortorder = $neworder; // Update the category records. $DB->update_record('user_info_category', $category); $DB->update_record('user_info_category', $swapcategory); \core\event\user_info_category_updated::create_from_category($category)->trigger(); \core\event\user_info_category_updated::create_from_category($swapcategory)->trigger(); profile_purge_user_fields_cache(); return true; } return false; } /** * Retrieve a list of all the available data types * @return array a list of the datatypes suitable to use in a select statement */ function profile_list_datatypes() { $datatypes = array(); $plugins = core_component::get_plugin_list('profilefield'); foreach ($plugins as $type => $unused) { $datatypes[$type] = get_string('pluginname', 'profilefield_'.$type); } asort($datatypes); return $datatypes; } /** * Retrieve a list of categories and ids suitable for use in a form * @return array */ function profile_list_categories() { global $DB; $categories = $DB->get_records_menu('user_info_category', null, 'sortorder ASC', 'id, name'); return array_map('format_string', $categories); } /** * Create or update a profile category * * @param stdClass $data */ function profile_save_category(stdClass $data): void { global $DB; if (empty($data->id)) { unset($data->id); $data->sortorder = $DB->count_records('user_info_category') + 1; $data->id = $DB->insert_record('user_info_category', $data, true); $createdcategory = $DB->get_record('user_info_category', array('id' => $data->id)); \core\event\user_info_category_created::create_from_category($createdcategory)->trigger(); } else { $DB->update_record('user_info_category', $data); $updatedcateogry = $DB->get_record('user_info_category', array('id' => $data->id)); \core\event\user_info_category_updated::create_from_category($updatedcateogry)->trigger(); } profile_reorder_categories(); profile_purge_user_fields_cache(); } /** * Edit a category * * @deprecated since Moodle 3.11 MDL-71051 - please do not use this function any more. * @todo MDL-71413 This will be deleted in Moodle 4.3. * @see profile_save_category() * * @param int $id * @param string $redirect */ function profile_edit_category($id, $redirect) { global $DB, $OUTPUT, $CFG; debugging('Function profile_edit_category() is deprecated without replacement, see also profile_save_category()', DEBUG_DEVELOPER); $categoryform = new \core_user\form\profile_category_form(); if ($category = $DB->get_record('user_info_category', array('id' => $id))) { $categoryform->set_data($category); } if ($categoryform->is_cancelled()) { redirect($redirect); } else { if ($data = $categoryform->get_data()) { profile_save_category($data); redirect($redirect); } if (empty($id)) { $strheading = get_string('profilecreatenewcategory', 'admin'); } else { $strheading = get_string('profileeditcategory', 'admin', format_string($category->name)); } // Print the page. echo $OUTPUT->header(); echo $OUTPUT->heading($strheading); $categoryform->display(); echo $OUTPUT->footer(); die; } } /** * Save updated field definition or create a new field * * @param stdClass $data data from the form profile_field_form * @param array $editors editors for this form field type */ function profile_save_field(stdClass $data, array $editors): void { global $CFG; require_once($CFG->dirroot.'/user/profile/field/'.$data->datatype.'/define.class.php'); $newfield = 'profile_define_'.$data->datatype; /** @var profile_define_base $formfield */ $formfield = new $newfield(); // Collect the description and format back into the proper data structure from the editor. // Note: This field will ALWAYS be an editor. $data->descriptionformat = $data->description['format']; $data->description = $data->description['text']; // Check whether the default data is an editor, this is (currently) only the textarea field type. if (is_array($data->defaultdata) && array_key_exists('text', $data->defaultdata)) { // Collect the default data and format back into the proper data structure from the editor. $data->defaultdataformat = $data->defaultdata['format']; $data->defaultdata = $data->defaultdata['text']; } // Convert the data format for. if (is_array($editors)) { foreach ($editors as $editor) { if (isset($field->$editor)) { $field->{$editor.'format'} = $field->{$editor}['format']; $field->$editor = $field->{$editor}['text']; } } } $formfield->define_save($data); profile_reorder_fields(); profile_reorder_categories(); } /** * Edit a profile field. * * @deprecated since Moodle 3.11 MDL-71051 - please do not use this function any more. * @todo MDL-71413 This will be deleted in Moodle 4.3. * @see profile_save_field() * * @param int $id * @param string $datatype * @param string $redirect */ function profile_edit_field($id, $datatype, $redirect) { global $OUTPUT, $PAGE; debugging('Function profile_edit_field() is deprecated without replacement, see also profile_save_field()', DEBUG_DEVELOPER); $fieldform = new \core_user\form\profile_field_form(); $fieldform->set_data_for_dynamic_submission(); if ($fieldform->is_cancelled()) { redirect($redirect); } else { if ($data = $fieldform->get_data()) { profile_save_field($data, $fieldform->editors()); redirect($redirect); } $datatypes = profile_list_datatypes(); if (empty($id)) { $strheading = get_string('profilecreatenewfield', 'admin', $datatypes[$datatype]); } else { $strheading = get_string('profileeditfield', 'admin', format_string($fieldform->get_field_record()->name)); } // Print the page. $PAGE->navbar->add($strheading); echo $OUTPUT->header(); echo $OUTPUT->heading($strheading); $fieldform->display(); echo $OUTPUT->footer(); die; } } /** * Purge the cache for the user profile fields */ function profile_purge_user_fields_cache() { $cache = \cache::make_from_params(cache_store::MODE_REQUEST, 'core_profile', 'customfields', [], ['simplekeys' => true, 'simpledata' => true]); $cache->purge(); } profile/field/social/field.class.php 0000644 00000005216 15151162244 0013446 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Social profile field define. * * @package profilefield_social * @copyright 2020 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_field_social. * * @copyright 2020 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_field_social extends profile_field_base { /** * Adds elements for this field type to the edit form. * @param moodleform $mform */ public function edit_field_add($mform) { $mform->addElement('text', $this->inputname, $this->field->name, null, null); if ($this->field->param1 === 'url') { $mform->setType($this->inputname, PARAM_URL); } else { $mform->setType($this->inputname, PARAM_NOTAGS); } } /** * alter the fieldname to be fetched from the language file. * * @param stdClass $field */ public function set_field($field) { $networks = profilefield_social\helper::get_networks(); $field->name = $networks[$field->name]; parent::set_field($field); } /** * Display the data for this field * @return string */ public function display_data() { $network = $this->field->param1; $networkurls = profilefield_social\helper::get_network_urls(); if (array_key_exists($network, $networkurls)) { $pattern = ['%%ENCODED%%', '%%PLAIN%%']; $data = [rawurlencode($this->data), $this->data]; return str_replace($pattern, $data, $networkurls[$network]); } return $this->data; } /** * Return the field type and null properties. * This will be used for validating the data submitted by a user. * * @return array the param type and null property * @since Moodle 3.2 */ public function get_field_properties() { return array(PARAM_TEXT, NULL_NOT_ALLOWED); } } profile/field/social/tests/privacy/provider_test.php 0000644 00000030743 15151162244 0016772 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Base class for unit tests for profilefield_social. * * @package profilefield_social * @copyright 2020 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_social\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\tests\provider_testcase; use profilefield_social\privacy\provider; use core_privacy\local\request\approved_userlist; /** * Unit tests for user\profile\field\social\classes\privacy\provider.php * * @copyright 2020 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { $this->resetAfterTest(true); } /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'social'); // Create a user. $user = $this->getDataGenerator()->create_user(); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // Get the field that was created. $userfielddata = $DB->get_records('user_info_data', array('userid' => $user->id)); // Confirm we got the right number of user field data. $this->assertCount(1, $userfielddata); $context = \context_user::instance($user->id); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertEquals($context, $contextlist->current()); } /** * Test that data is exported correctly for this plugin. */ public function test_export_user_data() { // Create profile category. $categoryid = $this->add_profile_category(); // Create social profile field. $socialprofilefieldid = $this->add_profile_field($categoryid, 'social'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add social user info data. $this->add_user_info_data($user->id, $socialprofilefieldid, '#icq-1294923'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($user->id, $context, 'profilefield_social'); $data = $writer->get_data([get_string('pluginname', 'profilefield_social')]); $this->assertCount(3, (array) $data); $this->assertEquals('icq', $data->name); $this->assertEquals('', $data->description); $this->assertEquals('#icq-1294923', $data->data); } /** * Test that user data is deleted using the context. */ public function test_delete_data_for_all_users_in_context() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create social profile field. $socialprofilefieldid = $this->add_profile_field($categoryid, 'social'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add social user info data. $this->add_user_info_data($user->id, $socialprofilefieldid, '#icq-1294923'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); provider::delete_data_for_all_users_in_context($context); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('#icq-1294923', reset($userinfodata)->data); } /** * Test that user data is deleted for this user. */ public function test_delete_data_for_user() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create social profile field. $socialprofilefieldid = $this->add_profile_field($categoryid, 'social'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add social user info data. $this->add_user_info_data($user->id, $socialprofilefieldid, '#icq-1294923'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'profilefield_social', [$context->id]); provider::delete_data_for_user($approvedlist); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('#icq-1294923', reset($userinfodata)->data); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context() { $this->resetAfterTest(); $component = 'profilefield_social'; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'social'); // Create a user. $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (related data still haven't been created). $userlist = new \core_privacy\local\request\userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users() { $this->resetAfterTest(); $component = 'profilefield_social'; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'social'); // Create user1. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); $this->add_user_info_data($user1->id, $profilefieldid, 'test data'); $this->add_user_info_data($user2->id, $profilefieldid, 'test data'); // The list of users for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); } /** * Add dummy user info data. * * @param int $userid The ID of the user * @param int $fieldid The ID of the field * @param string $data The data */ private function add_user_info_data($userid, $fieldid, $data) { global $DB; $userinfodata = array( 'userid' => $userid, 'fieldid' => $fieldid, 'data' => $data, 'dataformat' => 0 ); $DB->insert_record('user_info_data', $userinfodata); } /** * Add dummy profile category. * * @return int The ID of the profile category */ private function add_profile_category() { $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); return $cat->id; } /** * Add dummy profile field. * * @param int $categoryid The ID of the profile category * @param string $datatype The datatype of the profile field * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { $data = $this->getDataGenerator()->create_custom_profile_field([ 'datatype' => $datatype, 'shortname' => 'icq', 'name' => 'icq', 'description' => '', 'categoryid' => $categoryid, ]); return $data->id; } } profile/field/social/tests/behat/social_profile_field.feature 0000644 00000002065 15151162244 0020504 0 ustar 00 @core @core_user Feature: Social profile fields can not have a duplicate shortname. In order edit social profile fields properly As an admin I should not be able to create duplicate shortnames for social profile fields. @javascript Scenario: Verify you can edit social profile fields. Given I log in as "admin" When I navigate to "Users > Accounts > User profile fields" in site administration And I click on "Create a new profile field" "link" And I click on "Social" "link" And I set the following fields to these values: | Network type | Yahoo ID | | Short name | yahoo | And I click on "Save changes" "button" And I click on "Create a new profile field" "link" And I click on "Social" "link" And I set the following fields to these values: | Network type | Yahoo ID | | Short name | yahoo | And I click on "Save changes" "button" Then I should see "This short name is already in use" profile/field/social/upgradelib.php 0000644 00000015441 15151162244 0013376 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains upgrade and install functions for the social profile fields. * * @package profilefield_social * @copyright 2020 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once("$CFG->dirroot/user/profile/definelib.php"); /** * Create the default category for custom profile fields if it does not exist yet. * * @return int Category ID for social user profile category. */ function user_profile_social_create_info_category(): int { global $DB; $categories = $DB->get_records('user_info_category', null, 'sortorder ASC'); // Check that we have at least one category defined. if (empty($categories)) { $defaultcategory = (object) [ 'name' => get_string('profiledefaultcategory', 'admin'), 'sortorder' => 1 ]; return $DB->insert_record('user_info_category', $defaultcategory); } else { return (int)$DB->get_field_sql('SELECT min(id) from {user_info_category}'); } } /** * Called on upgrade to create new profile fields based on the old user table columns * for icq, msn, aim, skype and url. * * @param string $social Social profile field. */ function user_profile_social_moveto_profilefield($social) { global $DB; $users = $DB->get_records_select('user', "$social IS NOT NULL AND $social != ''"); if (count($users)) { $profilefield = user_profile_social_create_profilefield($social); foreach ($users as $user) { $userinfodata = [ 'userid' => $user->id, 'fieldid' => $profilefield->id, 'data' => $user->$social, 'dataformat' => 0 ]; $user->$social = ''; $DB->update_record('user', $user); $DB->insert_record('user_info_data', $userinfodata); } } } /** * Create an new custom social profile field if it does not exist * * @param string $social Social profile field. * @return object DB record or social profield field. */ function user_profile_social_create_profilefield($social) { global $DB, $CFG; $hiddenfields = explode(',', $CFG->hiddenuserfields); $confignames = [ 'url' => 'webpage', 'icq' => 'icqnumber', 'skype' => 'skypeid', 'yahoo' => 'yahooid', 'aim' => 'aimid', 'msn' => 'msnid', ]; $visible = (in_array($confignames[$social], $hiddenfields)) ? 3 : 2; $categoryid = user_profile_social_create_info_category(); $newfield = (object)[ 'shortname' => $social, 'name' => $social, 'datatype' => 'social', 'description' => '', 'descriptionformat' => 1, 'categoryid' => $categoryid, 'required' => 0, 'locked' => 0, 'visible' => $visible, 'forceunique' => 0, 'signup' => 0, 'defaultdata' => '', 'defaultdataformat' => 0, 'param1' => $social ]; $profilefield = $DB->get_record_sql( 'SELECT * FROM {user_info_field} WHERE datatype = :datatype AND ' . $DB->sql_compare_text('param1') . ' = ' . $DB->sql_compare_text(':social', 40), ['datatype' => 'social', 'social' => $social]); if (!$profilefield) { // Find a new unique shortname. $count = 0; $shortname = $newfield->shortname; while ($field = $DB->get_record('user_info_field', array('shortname' => $shortname))) { $count++; $shortname = $newfield->shortname . '_' . $count; } $newfield->shortname = $shortname; $profileclass = new profile_define_base(); $profileclass->define_save($newfield); profile_reorder_fields(); $profilefield = $DB->get_record_sql( 'SELECT * FROM {user_info_field} WHERE datatype = :datatype AND ' . $DB->sql_compare_text('param1') . ' = ' . $DB->sql_compare_text(':social', 40), ['datatype' => 'social', 'social' => $social]); } if (!$profilefield) { throw new moodle_exception('upgradeerror', 'admin', 'could not create new social profile field'); } return $profilefield; } /** * Update the module availability configuration for all course modules * */ function user_profile_social_update_module_availability() { global $DB; // Use transaction to improve performance if there are many individual database updates. $transaction = $DB->start_delegated_transaction(); // Query all the course_modules entries that have availability set. $rs = $DB->get_recordset_select('course_modules', 'availability IS NOT NULL', [], '', 'id, availability'); foreach ($rs as $mod) { if (isset($mod->availability)) { $availability = json_decode($mod->availability); if (!is_null($availability)) { user_profile_social_update_availability_structure($availability); $newavailability = json_encode($availability); if ($newavailability !== $mod->availability) { $mod->availability = json_encode($availability); $DB->update_record('course_modules', $mod); } } } } $rs->close(); $transaction->allow_commit(); } /** * Loop through the availability info and change all move standard profile * fields for icq, skype, aim, yahoo, msn and url to be custom profile fields. * @param mixed $structure The availability object. */ function user_profile_social_update_availability_structure(&$structure) { if (is_array($structure)) { foreach ($structure as $st) { user_profile_social_update_availability_structure($st); } } foreach ($structure as $key => $value) { if ($key === 'c' && is_array($value)) { user_profile_social_update_availability_structure($value); } if ($key === 'type' && $value === 'profile') { if (isset($structure->sf) && in_array($structure->sf, ['icq', 'skype', 'aim', 'yahoo', 'msn', 'url'])) { $structure->cf = $structure->sf; unset($structure->sf); } } } } profile/field/social/define.class.php 0000644 00000006532 15151162244 0013617 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Textarea profile field define. * * @package profilefield_social * @copyright 2020 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_define_social. * * @copyright 2020 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_define_social extends profile_define_base { /** * Prints out the form snippet for the part of creating or editing a profile field common to all data types. * * @param moodleform $form instance of the moodleform class */ public function define_form_common(&$form) { $availablenetworks = profilefield_social\helper::get_networks(); $networks = array_merge(['0' => get_string('select')], $availablenetworks); $form->addElement('hidden', 'defaultdata', ''); $form->setType('defaultdata', PARAM_TEXT); $form->addElement('select', 'param1', get_string('networktype', 'profilefield_social'), $networks); $form->addRule('param1', get_string('required'), 'required', null, 'client'); $form->setType('param1', PARAM_TEXT); parent::define_form_common($form); $form->removeElement('name'); } /** * Alter form based on submitted or existing data. * * @param moodleform $form */ public function define_after_data(&$form) { if (isset($form->_defaultValues['name'])) { $form->setDefault('param1', $form->_defaultValues['name']); } } /** * Validate the data from the add/edit profile field form that is common to all data types. * * Generally this method should not be overwritten by child classes. * * @param stdClass|array $data from the add/edit profile field form * @param array $files * @return array associative array of error messages */ public function define_validate_common($data, $files) { $err = parent::define_validate_common($data, $files); $networks = profilefield_social\helper::get_networks(); if (empty($data->param1) || !array_key_exists($data->param1, $networks)) { $err['param1'] = get_string('invalidnetwork', 'profilefield_social'); } return $err; } /** * Preprocess data from the add/edit profile field form before it is saved. * * This method is a hook for the child classes to overwrite. * * @param array|stdClass $data from the add/edit profile field form * @return array|stdClass processed data object */ public function define_save_preprocess($data) { $data->name = $data->param1; return $data; } } profile/field/social/version.php 0000644 00000002042 15151162244 0012736 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version information for the social profile field. * * @package profilefield_social * @copyright 2020 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; $plugin->requires = 2022111800; $plugin->component = 'profilefield_social'; profile/field/social/classes/privacy/provider.php 0000644 00000017215 15151162244 0016225 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy class for requesting user data. * * @package profilefield_social * @copyright 2020 Bas Brands * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_social\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Privacy class for requesting user data. * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { return $collection->add_database_table('user_info_data', [ 'userid' => 'privacy:metadata:profile_field_social:userid', 'fieldid' => 'privacy:metadata:profile_field_social:fieldid', 'data' => 'privacy:metadata:profile_field_social:data', 'dataformat' => 'privacy:metadata:profile_field_social:dataformat' ], 'privacy:metadata:profile_field_social:tableexplanation'); } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : contextlist { $sql = "SELECT ctx.id FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id JOIN {context} ctx ON ctx.instanceid = uda.userid AND ctx.contextlevel = :contextlevel WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'contextlevel' => CONTEXT_USER, 'datatype' => 'social' ]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT uda.userid FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $context->instanceid, 'datatype' => 'social' ]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { $results = static::get_records($user->id); foreach ($results as $result) { $data = (object) [ 'name' => $result->name, 'description' => $result->description, 'data' => $result->data ]; \core_privacy\local\request\writer::with_context($context)->export_data([ get_string('pluginname', 'profilefield_social')], $data); } } } } /** * Delete all user data which matches the specified context. * * @param context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { // Delete data only for user context. if ($context->contextlevel == CONTEXT_USER) { static::delete_data($context->instanceid); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_data($context->instanceid); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { static::delete_data($context->instanceid); } } } /** * Delete data related to a userid. * * @param int $userid The user ID */ protected static function delete_data($userid) { global $DB; $params = [ 'userid' => $userid, 'datatype' => 'social' ]; $DB->delete_records_select('user_info_data', "fieldid IN ( SELECT id FROM {user_info_field} WHERE datatype = :datatype) AND userid = :userid", $params); } /** * Get records related to this plugin and user. * * @param int $userid The user ID * @return array An array of records. */ protected static function get_records($userid) { global $DB; $sql = "SELECT * FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'datatype' => 'social' ]; return $DB->get_records_sql($sql, $params); } } profile/field/social/classes/helper.php 0000644 00000004672 15151162244 0014200 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class profilefield_social\networks * * @package profilefield_social * @copyright 2020 Bas Brands * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_social; /** * helper class for social profile fields. * * @copyright 2020 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Get the available social networks * * @return array list of social networks. */ public static function get_networks(): array { return [ 'icq' => get_string('icqnumber', 'profilefield_social'), 'msn' => get_string('msnid', 'profilefield_social'), 'aim' => get_string('aimid', 'profilefield_social'), 'yahoo' => get_string('yahooid', 'profilefield_social'), 'skype' => get_string('skypeid', 'profilefield_social'), 'url' => get_string('webpage', 'profilefield_social'), ]; } /** * Get the translated fieldname string for a network. * * @param string $fieldname Network short name. * @return string network name. */ public static function get_fieldname(string $fieldname): string { $networks = self::get_networks(); return $networks[$fieldname]; } /** * Get the available network url formats. * * @return array list network url strings. */ public static function get_network_urls(): array { return [ 'skype' => '<a href="skype:%%ENCODED%%?call">%%PLAIN%%</a>', 'icq' => '<a href="http://www.icq.com/whitepages/cmd.php?uin=%%ENCODED%%&action=message">%%PLAIN%%</a>', 'url' => '<a href="%%PLAIN%%">%%PLAIN%%</a>' ]; } } profile/field/social/lang/en/profilefield_social.php 0000644 00000003442 15151162244 0016577 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'profilefield_social' * * @package profilefield_social * @copyright 2020 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['invalidnetwork'] = 'Invalid social network'; $string['networkinuse'] = 'Social network has already been added'; $string['pluginname'] = 'Social'; $string['networktype'] = 'Network type'; $string['privacy:metadata:profile_field_social:data'] = 'Text area user profile field user data'; $string['privacy:metadata:profile_field_social:dataformat'] = 'The format of Text area user profile field user data'; $string['privacy:metadata:profile_field_social:fieldid'] = 'The ID of the profile field'; $string['privacy:metadata:profile_field_social:tableexplanation'] = 'Additional profile data'; $string['privacy:metadata:profile_field_social:userid'] = 'The ID of the user whose data is stored by the social user profile field'; $string['aimid'] = 'AIM ID'; $string['yahooid'] = 'Yahoo ID'; $string['skypeid'] = 'Skype ID'; $string['icqnumber'] = 'ICQ number'; $string['msnid'] = 'MSN ID'; $string['webpage'] = 'Web page'; profile/field/textarea/field.class.php 0000644 00000006002 15151162244 0014003 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Textarea profile field define. * * @package profilefield_textarea * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_field_textarea. * * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_field_textarea extends profile_field_base { /** * Adds elements for this field type to the edit form. * @param moodleform $mform */ public function edit_field_add($mform) { // Create the form field. $mform->addElement('editor', $this->inputname, format_string($this->field->name), null, null); $mform->setType($this->inputname, PARAM_RAW); // We MUST clean this before display! } /** * Overwrite base class method, data in this field type is potentially too large to be included in the user object. * @return bool */ public function is_user_object_data() { return false; } /** * Process incoming data for the field. * @param stdClass $data * @param stdClass $datarecord * @return mixed|stdClass */ public function edit_save_data_preprocess($data, $datarecord) { if (is_array($data)) { $datarecord->dataformat = $data['format']; $data = $data['text']; } return $data; } /** * Load user data for this profile field, ready for editing. * @param stdClass $user */ public function edit_load_user_data($user) { if ($this->data !== null) { $this->data = clean_text($this->data, $this->dataformat); $user->{$this->inputname} = array('text' => $this->data, 'format' => $this->dataformat); } } /** * Display the data for this field * @return string */ public function display_data() { return format_text($this->data, $this->dataformat, array('overflowdiv' => true)); } /** * Return the field type and null properties. * This will be used for validating the data submitted by a user. * * @return array the param type and null property * @since Moodle 3.2 */ public function get_field_properties() { return array(PARAM_RAW, NULL_NOT_ALLOWED); } } profile/field/textarea/tests/privacy/provider_test.php 0000644 00000031145 15151162244 0017332 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Base class for unit tests for profilefield_textarea. * * @package profilefield_textarea * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_textarea\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\tests\provider_testcase; use profilefield_textarea\privacy\provider; use core_privacy\local\request\approved_userlist; /** * Unit tests for user\profile\field\textarea\classes\privacy\provider.php * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { $this->resetAfterTest(true); } /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'textarea'); // Create a user. $user = $this->getDataGenerator()->create_user(); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // Get the field that was created. $userfielddata = $DB->get_records('user_info_data', array('userid' => $user->id)); // Confirm we got the right number of user field data. $this->assertCount(1, $userfielddata); $context = \context_user::instance($user->id); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertEquals($context, $contextlist->current()); } /** * Test that data is exported correctly for this plugin. */ public function test_export_user_data() { // Create profile category. $categoryid = $this->add_profile_category(); // Create textarea profile field. $textareaprofilefieldid = $this->add_profile_field($categoryid, 'textarea'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add textarea user info data. $this->add_user_info_data($user->id, $textareaprofilefieldid, 'test textarea'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($user->id, $context, 'profilefield_textarea'); $data = $writer->get_data([get_string('pluginname', 'profilefield_textarea')]); $this->assertCount(3, (array) $data); $this->assertEquals('Test field', $data->name); $this->assertEquals('This is a test.', $data->description); $this->assertEquals('test textarea', $data->data); } /** * Test that user data is deleted using the context. */ public function test_delete_data_for_all_users_in_context() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create textarea profile field. $textareaprofilefieldid = $this->add_profile_field($categoryid, 'textarea'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add textarea user info data. $this->add_user_info_data($user->id, $textareaprofilefieldid, 'test textarea'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); provider::delete_data_for_all_users_in_context($context); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('test textarea', reset($userinfodata)->data); } /** * Test that user data is deleted for this user. */ public function test_delete_data_for_user() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create textarea profile field. $textareaprofilefieldid = $this->add_profile_field($categoryid, 'textarea'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add textarea user info data. $this->add_user_info_data($user->id, $textareaprofilefieldid, 'test textarea'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'profilefield_textarea', [$context->id]); provider::delete_data_for_user($approvedlist); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('test textarea', reset($userinfodata)->data); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context() { $this->resetAfterTest(); $component = 'profilefield_textarea'; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'textarea'); // Create a user. $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (related data still haven't been created). $userlist = new \core_privacy\local\request\userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users() { $this->resetAfterTest(); $component = 'profilefield_textarea'; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'textarea'); // Create user1. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); $this->add_user_info_data($user1->id, $profilefieldid, 'test data'); $this->add_user_info_data($user2->id, $profilefieldid, 'test data'); // The list of users for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); } /** * Add dummy user info data. * * @param int $userid The ID of the user * @param int $fieldid The ID of the field * @param string $data The data */ private function add_user_info_data($userid, $fieldid, $data) { global $DB; $userinfodata = array( 'userid' => $userid, 'fieldid' => $fieldid, 'data' => $data, 'dataformat' => 0 ); $DB->insert_record('user_info_data', $userinfodata); } /** * Add dummy profile category. * * @return int The ID of the profile category */ private function add_profile_category() { $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); return $cat->id; } /** * Add dummy profile field. * * @param int $categoryid The ID of the profile category * @param string $datatype The datatype of the profile field * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { $data = $this->getDataGenerator()->create_custom_profile_field([ 'datatype' => $datatype, 'shortname' => 'tstField', 'name' => 'Test field', 'description' => 'This is a test.', 'categoryid' => $categoryid, ]); return $data->id; } } profile/field/textarea/define.class.php 0000644 00000003360 15151162244 0014156 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Textarea profile field define. * * @package profilefield_textarea * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_define_textarea. * * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_define_textarea extends profile_define_base { /** * Add elements for creating/editing a textarea profile field. * @param moodleform $form */ public function define_form_specific($form) { // Default data. $form->addElement('editor', 'defaultdata', get_string('profiledefaultdata', 'admin')); $form->setType('defaultdata', PARAM_RAW); // We have to trust person with capability to edit this default description. } /** * Returns an array of editors used when defining this type of profile field. * @return array */ public function define_editors() { return array('defaultdata'); } } profile/field/textarea/version.php 0000644 00000002317 15151162244 0013306 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version information for the textarea profile field. * * @package profilefield_textarea * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'profilefield_textarea'; // Full name of the plugin (used for diagnostics) profile/field/textarea/classes/privacy/provider.php 0000644 00000017276 15151162244 0016577 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy class for requesting user data. * * @package profilefield_textarea * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_textarea\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Privacy class for requesting user data. * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { return $collection->add_database_table('user_info_data', [ 'userid' => 'privacy:metadata:profile_field_textarea:userid', 'fieldid' => 'privacy:metadata:profile_field_textarea:fieldid', 'data' => 'privacy:metadata:profile_field_textarea:data', 'dataformat' => 'privacy:metadata:profile_field_textarea:dataformat' ], 'privacy:metadata:profile_field_textarea:tableexplanation'); } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : contextlist { $sql = "SELECT ctx.id FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id JOIN {context} ctx ON ctx.instanceid = uda.userid AND ctx.contextlevel = :contextlevel WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'contextlevel' => CONTEXT_USER, 'datatype' => 'textarea' ]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT uda.userid FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $context->instanceid, 'datatype' => 'textarea' ]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { $results = static::get_records($user->id); foreach ($results as $result) { $data = (object) [ 'name' => $result->name, 'description' => $result->description, 'data' => $result->data ]; \core_privacy\local\request\writer::with_context($context)->export_data([ get_string('pluginname', 'profilefield_textarea')], $data); } } } } /** * Delete all user data which matches the specified context. * * @param context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { // Delete data only for user context. if ($context->contextlevel == CONTEXT_USER) { static::delete_data($context->instanceid); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_data($context->instanceid); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { static::delete_data($context->instanceid); } } } /** * Delete data related to a userid. * * @param int $userid The user ID */ protected static function delete_data($userid) { global $DB; $params = [ 'userid' => $userid, 'datatype' => 'textarea' ]; $DB->delete_records_select('user_info_data', "fieldid IN ( SELECT id FROM {user_info_field} WHERE datatype = :datatype) AND userid = :userid", $params); } /** * Get records related to this plugin and user. * * @param int $userid The user ID * @return array An array of records. */ protected static function get_records($userid) { global $DB; $sql = "SELECT * FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'datatype' => 'textarea' ]; return $DB->get_records_sql($sql, $params); } } profile/field/textarea/lang/en/profilefield_textarea.php 0000644 00000003024 15151162244 0017501 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'profilefield_textarea', language 'en', branch 'MOODLE_20_STABLE' * * @package profilefield_textarea * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Text area'; $string['privacy:metadata:profile_field_textarea:userid'] = 'The ID of the user whose data is stored by the Text area user profile field'; $string['privacy:metadata:profile_field_textarea:fieldid'] = 'The ID of the profile field'; $string['privacy:metadata:profile_field_textarea:data'] = 'Text area user profile field user data'; $string['privacy:metadata:profile_field_textarea:dataformat'] = 'The format of Text area user profile field user data'; $string['privacy:metadata:profile_field_textarea:tableexplanation'] = 'Additional profile data'; profile/field/checkbox/field.class.php 0000644 00000005014 15151162244 0013756 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'profilefield_checkbox', language 'en', branch 'MOODLE_20_STABLE' * * @package profilefield_checkbox * @copyright 2008 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_field_checkbox * * @copyright 2008 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_field_checkbox extends profile_field_base { /** * Add elements for editing the profile field value. * @param moodleform $mform */ public function edit_field_add($mform) { // Create the form field. $checkbox = $mform->addElement('advcheckbox', $this->inputname, format_string($this->field->name)); if ($this->data == '1') { $checkbox->setChecked(true); } $mform->setType($this->inputname, PARAM_BOOL); if ($this->is_required() and !has_capability('moodle/user:update', context_system::instance())) { $mform->addRule($this->inputname, get_string('required'), 'nonzero', null, 'client'); } } /** * Display the data for this field * * @return string HTML. */ public function display_data() { $options = new stdClass(); $options->para = false; $checked = intval($this->data) === 1 ? 'checked="checked"' : ''; return '<input disabled="disabled" type="checkbox" name="'.$this->inputname.'" '.$checked.' />'; } /** * Return the field type and null properties. * This will be used for validating the data submitted by a user. * * @return array the param type and null property * @since Moodle 3.2 */ public function get_field_properties() { return array(PARAM_BOOL, NULL_NOT_ALLOWED); } } profile/field/checkbox/tests/privacy/provider_test.php 0000644 00000031120 15151162244 0017274 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Base class for unit tests for profilefield_checkbox. * * @package profilefield_checkbox * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_checkbox\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\tests\provider_testcase; use profilefield_checkbox\privacy\provider; use core_privacy\local\request\approved_userlist; /** * Unit tests for user\profile\field\checkbox\classes\privacy\provider.php * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { $this->resetAfterTest(true); } /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // Get the field that was created. $userfielddata = $DB->get_records('user_info_data', array('userid' => $user->id)); // Confirm we got the right number of user field data. $this->assertCount(1, $userfielddata); $context = \context_user::instance($user->id); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertEquals($context, $contextlist->current()); } /** * Test that data is exported correctly for this plugin. */ public function test_export_user_data() { // Create profile category. $categoryid = $this->add_profile_category(); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create datetime profile field. $datetimeprofilefieldid = $this->add_profile_field($categoryid, 'datetime'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Add datetime user info data. $this->add_user_info_data($user->id, $datetimeprofilefieldid, '1524067200'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($user->id, $context, 'profilefield_checkbox'); $data = $writer->get_data([get_string('pluginname', 'profilefield_checkbox')]); $this->assertCount(3, (array) $data); $this->assertEquals('Test field', $data->name); $this->assertEquals('This is a test.', $data->description); $this->assertEquals('test data', $data->data); } /** * Test that user data is deleted using the context. */ public function test_delete_data_for_all_users_in_context() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create datetime profile field. $datetimeprofilefieldid = $this->add_profile_field($categoryid, 'datetime'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Add datetime user info data. $this->add_user_info_data($user->id, $datetimeprofilefieldid, '1524067200'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); provider::delete_data_for_all_users_in_context($context); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('test data', reset($userinfodata)->data); } /** * Test that user data is deleted for this user. */ public function test_delete_data_for_user() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create datetime profile field. $datetimeprofilefieldid = $this->add_profile_field($categoryid, 'datetime'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Add datetime user info data. $this->add_user_info_data($user->id, $datetimeprofilefieldid, '1524067200'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'profilefield_checkbox', [$context->id]); provider::delete_data_for_user($approvedlist); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('test data', reset($userinfodata)->data); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context() { $this->resetAfterTest(); $component = 'profilefield_checkbox'; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (related data still haven't been created). $userlist = new \core_privacy\local\request\userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users() { $this->resetAfterTest(); $component = 'profilefield_checkbox'; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create user1. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); $this->add_user_info_data($user1->id, $profilefieldid, 'test data'); $this->add_user_info_data($user2->id, $profilefieldid, 'test data'); // The list of users for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); } /** * Add dummy user info data. * * @param int $userid The ID of the user * @param int $fieldid The ID of the field * @param string $data The data */ private function add_user_info_data($userid, $fieldid, $data) { global $DB; $userinfodata = array( 'userid' => $userid, 'fieldid' => $fieldid, 'data' => $data, 'dataformat' => 0 ); $DB->insert_record('user_info_data', $userinfodata); } /** * Add dummy profile category. * * @return int The ID of the profile category */ private function add_profile_category() { $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); return $cat->id; } /** * Add dummy profile field. * * @param int $categoryid The ID of the profile category * @param string $datatype The datatype of the profile field * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { $data = $this->getDataGenerator()->create_custom_profile_field([ 'datatype' => $datatype, 'shortname' => 'tstField', 'name' => 'Test field', 'description' => 'This is a test.', 'categoryid' => $categoryid, ]); return $data->id; } } profile/field/checkbox/define.class.php 0000644 00000003117 15151162244 0014127 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Checkbox profile field * * @package profilefield_checkbox * @copyright 2008 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_define_checkbox * @copyright 2008 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_define_checkbox extends profile_define_base { /** * Add elements for creating/editing a checkbox profile field. * * @param moodleform $form */ public function define_form_specific($form) { // Select whether or not this should be checked by default. $form->addElement('selectyesno', 'defaultdata', get_string('profiledefaultchecked', 'admin')); $form->setDefault('defaultdata', 0); // Defaults to 'no'. $form->setType('defaultdata', PARAM_BOOL); } } profile/field/checkbox/version.php 0000644 00000002324 15151162244 0013255 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version information for the checkbox profile field type. * * @package profilefield_checkbox * @copyright 2008 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'profilefield_checkbox'; // Full name of the plugin (used for diagnostics) profile/field/checkbox/classes/privacy/provider.php 0000644 00000017307 15151162244 0016543 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy class for requesting user data. * * @package profilefield_checkbox * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_checkbox\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Privacy class for requesting user data. * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { return $collection->add_database_table('user_info_data', [ 'userid' => 'privacy:metadata:profilefield_checkbox:userid', 'fieldid' => 'privacy:metadata:profilefield_checkbox:fieldid', 'data' => 'privacy:metadata:profilefield_checkbox:data', 'dataformat' => 'privacy:metadata:profilefield_checkbox:dataformat' ], 'privacy:metadata:profilefield_checkbox:tableexplanation'); } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : contextlist { $sql = "SELECT ctx.id FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id JOIN {context} ctx ON ctx.instanceid = uda.userid AND ctx.contextlevel = :contextlevel WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'contextlevel' => CONTEXT_USER, 'datatype' => 'checkbox' ]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT uda.userid FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $context->instanceid, 'datatype' => 'checkbox' ]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { $results = static::get_records($user->id); foreach ($results as $result) { $data = (object) [ 'name' => $result->name, 'description' => $result->description, 'data' => $result->data ]; \core_privacy\local\request\writer::with_context($context)->export_data([ get_string('pluginname', 'profilefield_checkbox')], $data); } } } } /** * Delete all user data which matches the specified context. * * @param context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { // Delete data only for user context. if ($context->contextlevel == CONTEXT_USER) { static::delete_data($context->instanceid); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_data($context->instanceid); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { static::delete_data($context->instanceid); } } } /** * Delete data related to a userid. * * @param int $userid The user ID */ protected static function delete_data($userid) { global $DB; $params = [ 'userid' => $userid, 'datatype' => 'checkbox' ]; $DB->delete_records_select('user_info_data', "fieldid IN ( SELECT id FROM {user_info_field} WHERE datatype = :datatype) AND userid = :userid", $params); } /** * Get records related to this plugin and user. * * @param int $userid The user ID * @return array An array of records. */ protected static function get_records($userid) { global $DB; $sql = "SELECT * FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'datatype' => 'checkbox' ]; return $DB->get_records_sql($sql, $params); } } profile/field/checkbox/lang/en/profilefield_checkbox.php 0000644 00000003017 15151162244 0017425 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'profilefield_checkbox', language 'en', branch 'MOODLE_20_STABLE' * * @package profilefield_checkbox * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Checkbox'; $string['privacy:metadata:profilefield_checkbox:userid'] = 'The ID of the user whose data is stored by the Checkbox user profile field'; $string['privacy:metadata:profilefield_checkbox:fieldid'] = 'The ID of the profile field'; $string['privacy:metadata:profilefield_checkbox:data'] = 'The checkbox user profile field user data'; $string['privacy:metadata:profilefield_checkbox:dataformat'] = 'The format of Checkbox user profile field user data'; $string['privacy:metadata:profilefield_checkbox:tableexplanation'] = 'Additional profile data'; profile/field/menu/field.class.php 0000644 00000013302 15151162244 0013133 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Menu profile field. * * @package profilefield_menu * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_field_menu * * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_field_menu extends profile_field_base { /** @var array $options */ public $options; /** @var int $datakey */ public $datakey; /** * Constructor method. * * Pulls out the options for the menu from the database and sets the the corresponding key for the data if it exists. * * @param int $fieldid * @param int $userid * @param object $fielddata */ public function __construct($fieldid = 0, $userid = 0, $fielddata = null) { // First call parent constructor. parent::__construct($fieldid, $userid, $fielddata); // Param 1 for menu type is the options. if (isset($this->field->param1)) { $options = explode("\n", $this->field->param1); } else { $options = array(); } $this->options = array(); if (!empty($this->field->required)) { $this->options[''] = get_string('choose').'...'; } foreach ($options as $key => $option) { // Multilang formatting with filters. $this->options[$option] = format_string($option, true, ['context' => context_system::instance()]); } // Set the data key. if ($this->data !== null) { $key = $this->data; if (isset($this->options[$key]) || ($key = array_search($key, $this->options)) !== false) { $this->data = $key; $this->datakey = $key; } } } /** * Create the code snippet for this field instance * Overwrites the base class method * @param moodleform $mform Moodle form instance */ public function edit_field_add($mform) { $mform->addElement('select', $this->inputname, format_string($this->field->name), $this->options); } /** * Set the default value for this field instance * Overwrites the base class method. * @param moodleform $mform Moodle form instance */ public function edit_field_set_default($mform) { $key = $this->field->defaultdata; if (isset($this->options[$key]) || ($key = array_search($key, $this->options)) !== false){ $defaultkey = $key; } else { $defaultkey = ''; } $mform->setDefault($this->inputname, $defaultkey); } /** * The data from the form returns the key. * * This should be converted to the respective option string to be saved in database * Overwrites base class accessor method. * * @param mixed $data The key returned from the select input in the form * @param stdClass $datarecord The object that will be used to save the record * @return mixed Data or null */ public function edit_save_data_preprocess($data, $datarecord) { return isset($this->options[$data]) ? $data : null; } /** * When passing the user object to the form class for the edit profile page * we should load the key for the saved data * * Overwrites the base class method. * * @param stdClass $user User object. */ public function edit_load_user_data($user) { $user->{$this->inputname} = $this->datakey; } /** * HardFreeze the field if locked. * @param moodleform $mform instance of the moodleform class */ public function edit_field_set_locked($mform) { if (!$mform->elementExists($this->inputname)) { return; } if ($this->is_locked() and !has_capability('moodle/user:update', context_system::instance())) { $mform->hardFreeze($this->inputname); $mform->setConstant($this->inputname, format_string($this->datakey)); } } /** * Convert external data (csv file) from value to key for processing later by edit_save_data_preprocess * * @param string $value one of the values in menu options. * @return int options key for the menu */ public function convert_external_data($value) { if (isset($this->options[$value])) { $retval = $value; } else { $retval = array_search($value, $this->options); } // If value is not found in options then return null, so that it can be handled // later by edit_save_data_preprocess. if ($retval === false) { $retval = null; } return $retval; } /** * Return the field type and null properties. * This will be used for validating the data submitted by a user. * * @return array the param type and null property * @since Moodle 3.2 */ public function get_field_properties() { return array(PARAM_TEXT, NULL_NOT_ALLOWED); } } profile/field/menu/tests/privacy/provider_test.php 0000644 00000030750 15151162244 0016462 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Base class for unit tests for profilefield_menu. * * @package profilefield_menu * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_menu\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\tests\provider_testcase; use profilefield_menu\privacy\provider; use core_privacy\local\request\approved_userlist; /** * Unit tests for user\profile\field\menu\classes\privacy\provider.php * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { $this->resetAfterTest(true); } /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'menu'); // Create a user. $user = $this->getDataGenerator()->create_user(); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // Get the field that was created. $userfielddata = $DB->get_records('user_info_data', array('userid' => $user->id)); // Confirm we got the right number of user field data. $this->assertCount(1, $userfielddata); $context = \context_user::instance($user->id); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertEquals($context, $contextlist->current()); } /** * Test that data is exported correctly for this plugin. */ public function test_export_user_data() { // Create profile category. $categoryid = $this->add_profile_category(); // Create menu profile field. $menuprofilefieldid = $this->add_profile_field($categoryid, 'menu'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add menu user info data. $this->add_user_info_data($user->id, $menuprofilefieldid, 'test menu'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($user->id, $context, 'profilefield_menu'); $data = $writer->get_data([get_string('pluginname', 'profilefield_menu')]); $this->assertCount(3, (array) $data); $this->assertEquals('Test field', $data->name); $this->assertEquals('This is a test.', $data->description); $this->assertEquals('test menu', $data->data); } /** * Test that user data is deleted using the context. */ public function test_delete_data_for_all_users_in_context() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create menu profile field. $menuprofilefieldid = $this->add_profile_field($categoryid, 'menu'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add menu user info data. $this->add_user_info_data($user->id, $menuprofilefieldid, 'test menu'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); provider::delete_data_for_all_users_in_context($context); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('test menu', reset($userinfodata)->data); } /** * Test that user data is deleted for this user. */ public function test_delete_data_for_user() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create menu profile field. $menuprofilefieldid = $this->add_profile_field($categoryid, 'menu'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add menu user info data. $this->add_user_info_data($user->id, $menuprofilefieldid, 'test menu'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'profilefield_menu', [$context->id]); provider::delete_data_for_user($approvedlist); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('test menu', reset($userinfodata)->data); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context() { $this->resetAfterTest(); $component = 'profilefield_menu'; // Create profile category. $categoryid = $this->add_profile_category(); // Create menu profile field. $profilefieldid = $this->add_profile_field($categoryid, 'menu'); // Create a user. $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (related data still haven't been created). $userlist = new \core_privacy\local\request\userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users() { $this->resetAfterTest(); $component = 'profilefield_menu'; // Create profile category. $categoryid = $this->add_profile_category(); // Create menu profile field. $profilefieldid = $this->add_profile_field($categoryid, 'menu'); // Create user1. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); $this->add_user_info_data($user1->id, $profilefieldid, 'test data'); $this->add_user_info_data($user2->id, $profilefieldid, 'test data'); // The list of users for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); } /** * Add dummy user info data. * * @param int $userid The ID of the user * @param int $fieldid The ID of the field * @param string $data The data */ private function add_user_info_data($userid, $fieldid, $data) { global $DB; $userinfodata = array( 'userid' => $userid, 'fieldid' => $fieldid, 'data' => $data, 'dataformat' => 0 ); $DB->insert_record('user_info_data', $userinfodata); } /** * Add dummy profile category. * * @return int The ID of the profile category */ private function add_profile_category() { $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); return $cat->id; } /** * Add dummy profile field. * * @param int $categoryid The ID of the profile category * @param string $datatype The datatype of the profile field * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { $data = $this->getDataGenerator()->create_custom_profile_field([ 'datatype' => $datatype, 'shortname' => 'tstField', 'name' => 'Test field', 'description' => 'This is a test.', 'categoryid' => $categoryid, ]); return $data->id; } } profile/field/menu/define.class.php 0000644 00000005521 15151162244 0013306 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Menu profile field definition. * * @package profilefield_menu * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_define_menu * * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_define_menu extends profile_define_base { /** * Adds elements to the form for creating/editing this type of profile field. * @param moodleform $form */ public function define_form_specific($form) { // Param 1 for menu type contains the options. $form->addElement('textarea', 'param1', get_string('profilemenuoptions', 'admin'), array('rows' => 6, 'cols' => 40)); $form->setType('param1', PARAM_TEXT); // Default data. $form->addElement('text', 'defaultdata', get_string('profiledefaultdata', 'admin'), 'size="50"'); $form->setType('defaultdata', PARAM_TEXT); } /** * Validates data for the profile field. * * @param array $data * @param array $files * @return array */ public function define_validate_specific($data, $files) { $err = array(); $data->param1 = str_replace("\r", '', $data->param1); // Check that we have at least 2 options. if (($options = explode("\n", $data->param1)) === false) { $err['param1'] = get_string('profilemenunooptions', 'admin'); } else if (count($options) < 2) { $err['param1'] = get_string('profilemenutoofewoptions', 'admin'); } else if (!empty($data->defaultdata) and !in_array($data->defaultdata, $options)) { // Check the default data exists in the options. $err['defaultdata'] = get_string('profilemenudefaultnotinoptions', 'admin'); } return $err; } /** * Processes data before it is saved. * @param array|stdClass $data * @return array|stdClass */ public function define_save_preprocess($data) { $data->param1 = str_replace("\r", '', $data->param1); return $data; } } profile/field/menu/version.php 0000644 00000002273 15151162244 0012436 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Menu profile field version information. * * @package profilefield_menu * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'profilefield_menu'; // Full name of the plugin (used for diagnostics) profile/field/menu/classes/privacy/provider.php 0000644 00000017213 15151162244 0015715 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy class for requesting user data. * * @package profilefield_menu * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_menu\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Privacy class for requesting user data. * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { return $collection->add_database_table('user_info_data', [ 'userid' => 'privacy:metadata:profilefield_menu:userid', 'fieldid' => 'privacy:metadata:profilefield_menu:fieldid', 'data' => 'privacy:metadata:profilefield_menu:data', 'dataformat' => 'privacy:metadata:profilefield_menu:dataformat' ], 'privacy:metadata:profilefield_menu:tableexplanation'); } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : contextlist { $sql = "SELECT ctx.id FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id JOIN {context} ctx ON ctx.instanceid = uda.userid AND ctx.contextlevel = :contextlevel WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'contextlevel' => CONTEXT_USER, 'datatype' => 'menu' ]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT uda.userid FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $context->instanceid, 'datatype' => 'menu' ]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { $results = static::get_records($user->id); foreach ($results as $result) { $data = (object) [ 'name' => $result->name, 'description' => $result->description, 'data' => $result->data ]; \core_privacy\local\request\writer::with_context($context)->export_data([ get_string('pluginname', 'profilefield_menu')], $data); } } } } /** * Delete all user data which matches the specified context. * * @param context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { // Delete data only for user context. if ($context->contextlevel == CONTEXT_USER) { static::delete_data($context->instanceid); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_data($context->instanceid); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { static::delete_data($context->instanceid); } } } /** * Delete data related to a userid. * * @param int $userid The user ID */ protected static function delete_data($userid) { global $DB; $params = [ 'userid' => $userid, 'datatype' => 'menu' ]; $DB->delete_records_select('user_info_data', "fieldid IN ( SELECT id FROM {user_info_field} WHERE datatype = :datatype) AND userid = :userid", $params); } /** * Get records related to this plugin and user. * * @param int $userid The user ID * @return array An array of records. */ protected static function get_records($userid) { global $DB; $sql = "SELECT * FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'datatype' => 'menu' ]; return $DB->get_records_sql($sql, $params); } } profile/field/menu/lang/en/profilefield_menu.php 0000644 00000003013 15151162244 0015755 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'profilefield_menu', language 'en', branch 'MOODLE_20_STABLE' * * @package profilefield_menu * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Drop-down menu'; $string['privacy:metadata:profilefield_menu:userid'] = 'The ID of the user whose data is stored by the drop-down menu user profile field'; $string['privacy:metadata:profilefield_menu:fieldid'] = 'The ID of the profile field'; $string['privacy:metadata:profilefield_menu:data'] = 'Drop-down menu user profile field user data'; $string['privacy:metadata:profilefield_menu:dataformat'] = 'The format of the drop-down menu user profile field user data'; $string['privacy:metadata:profilefield_menu:tableexplanation'] = 'Additional profile data'; profile/field/datetime/field.class.php 0000644 00000011614 15151162244 0013767 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the datetime profile field class. * * @package profilefield_datetime * @copyright 2010 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ /** * Handles displaying and editing the datetime field. * * @copyright 2010 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ class profile_field_datetime extends profile_field_base { /** * Handles editing datetime fields. * * @param moodleform $mform */ public function edit_field_add($mform) { // Get the current calendar in use - see MDL-18375. $calendartype = \core_calendar\type_factory::get_calendar_instance(); // Check if the field is required. if ($this->field->required) { $optional = false; } else { $optional = true; } // Convert the year stored in the DB as gregorian to that used by the calendar type. $startdate = $calendartype->convert_from_gregorian($this->field->param1, 1, 1); $stopdate = $calendartype->convert_from_gregorian($this->field->param2, 1, 1); $attributes = array( 'startyear' => $startdate['year'], 'stopyear' => $stopdate['year'], 'optional' => $optional ); // Check if they wanted to include time as well. if (!empty($this->field->param3)) { $mform->addElement('date_time_selector', $this->inputname, format_string($this->field->name), $attributes); } else { $mform->addElement('date_selector', $this->inputname, format_string($this->field->name), $attributes); } $mform->setType($this->inputname, PARAM_INT); $mform->setDefault($this->inputname, time()); } /** * If timestamp is in YYYY-MM-DD or YYYY-MM-DD-HH-MM-SS format, then convert it to timestamp. * * @param string|int $datetime datetime to be converted. * @param stdClass $datarecord The object that will be used to save the record * @return int timestamp * @since Moodle 2.5 */ public function edit_save_data_preprocess($datetime, $datarecord) { if (!$datetime) { return 0; } if (is_numeric($datetime)) { $gregoriancalendar = \core_calendar\type_factory::get_calendar_instance('gregorian'); $datetime = $gregoriancalendar->timestamp_to_date_string($datetime, '%Y-%m-%d-%H-%M-%S', 99, true, true); } $datetime = explode('-', $datetime); // Bound year with start and end year. $datetime[0] = min(max($datetime[0], $this->field->param1), $this->field->param2); if (!empty($this->field->param3) && count($datetime) == 6) { return make_timestamp($datetime[0], $datetime[1], $datetime[2], $datetime[3], $datetime[4], $datetime[5]); } else { return make_timestamp($datetime[0], $datetime[1], $datetime[2]); } } /** * Display the data for this field. */ public function display_data() { // Check if time was specified. if (!empty($this->field->param3)) { $format = get_string('strftimedaydatetime', 'langconfig'); } else { $format = get_string('strftimedate', 'langconfig'); } // Check if a date has been specified. if (empty($this->data)) { return get_string('notset', 'profilefield_datetime'); } else { return userdate($this->data, $format); } } /** * Check if the field data is considered empty * * @return boolean */ public function is_empty() { return empty($this->data); } /** * Return the field type and null properties. * This will be used for validating the data submitted by a user. * * @return array the param type and null property * @since Moodle 3.2 */ public function get_field_properties() { return array(PARAM_INT, NULL_NOT_ALLOWED); } /** * Check if the field should convert the raw data into user-friendly data when exporting * * @return bool */ public function is_transform_supported(): bool { return true; } } profile/field/datetime/tests/privacy/provider_test.php 0000644 00000031132 15151162244 0017305 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Base class for unit tests for profilefield_datetime. * * @package profilefield_datetime * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_datetime\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\tests\provider_testcase; use profilefield_datetime\privacy\provider; use core_privacy\local\request\approved_userlist; /** * Unit tests for user\profile\field\datetime\classes\privacy\provider.php * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { $this->resetAfterTest(true); } /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'datetime'); // Create a user. $user = $this->getDataGenerator()->create_user(); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // Get the field that was created. $userfielddata = $DB->get_records('user_info_data', array('userid' => $user->id)); // Confirm we got the right number of user field data. $this->assertCount(1, $userfielddata); $context = \context_user::instance($user->id); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertEquals($context, $contextlist->current()); } /** * Test that data is exported correctly for this plugin. */ public function test_export_user_data() { // Create profile category. $categoryid = $this->add_profile_category(); // Create datetime profile field. $datetimeprofilefieldid = $this->add_profile_field($categoryid, 'datetime'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add datetime user info data. $this->add_user_info_data($user->id, $datetimeprofilefieldid, '1524067200'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($user->id, $context, 'profilefield_datetime'); $data = $writer->get_data([get_string('pluginname', 'profilefield_datetime')]); $this->assertCount(3, (array) $data); $this->assertEquals('Test field', $data->name); $this->assertEquals('This is a test.', $data->description); $this->assertEquals('19 April 2018', $data->data); } /** * Test that user data is deleted using the context. */ public function test_delete_data_for_all_users_in_context() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create datetime profile field. $datetimeprofilefieldid = $this->add_profile_field($categoryid, 'datetime'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add datetime user info data. $this->add_user_info_data($user->id, $datetimeprofilefieldid, '1524067200'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); provider::delete_data_for_all_users_in_context($context); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('1524067200', reset($userinfodata)->data); } /** * Test that user data is deleted for this user. */ public function test_delete_data_for_user() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create datetime profile field. $datetimeprofilefieldid = $this->add_profile_field($categoryid, 'datetime'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add datetime user info data. $this->add_user_info_data($user->id, $datetimeprofilefieldid, '1524067200'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'profilefield_datetime', [$context->id]); provider::delete_data_for_user($approvedlist); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('1524067200', reset($userinfodata)->data); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context() { $this->resetAfterTest(); $component = 'profilefield_datetime'; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'datetime'); // Create a user. $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (related data still haven't been created). $userlist = new \core_privacy\local\request\userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users() { $this->resetAfterTest(); $component = 'profilefield_datetime'; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'datetime'); // Create user1. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); $this->add_user_info_data($user1->id, $profilefieldid, 'test data'); $this->add_user_info_data($user2->id, $profilefieldid, 'test data'); // The list of users for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); } /** * Add dummy user info data. * * @param int $userid The ID of the user * @param int $fieldid The ID of the field * @param string $data The data */ private function add_user_info_data($userid, $fieldid, $data) { global $DB; $userinfodata = array( 'userid' => $userid, 'fieldid' => $fieldid, 'data' => $data, 'dataformat' => 0 ); $DB->insert_record('user_info_data', $userinfodata); } /** * Add dummy profile category. * * @return int The ID of the profile category */ private function add_profile_category() { $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); return $cat->id; } /** * Add dummy profile field. * * @param int $categoryid The ID of the profile category * @param string $datatype The datatype of the profile field * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { $data = $this->getDataGenerator()->create_custom_profile_field([ 'datatype' => $datatype, 'shortname' => 'tstField', 'name' => 'Test field', 'description' => 'This is a test.', 'categoryid' => $categoryid, ]); return $data->id; } } profile/field/datetime/define.class.php 0000644 00000015404 15151162244 0014137 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the datetime profile field definition class. * * @package profilefield_datetime * @copyright 2010 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ /** * Define datetime fields. * * @copyright 2010 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ class profile_define_datetime extends profile_define_base { /** * Define the setting for a datetime custom field. * * @param moodleform $form the user form */ public function define_form_specific($form) { // Get the current calendar in use - see MDL-18375. $calendartype = \core_calendar\type_factory::get_calendar_instance(); // Create variables to store start and end. list($year, $month, $day) = explode('_', date('Y_m_d')); $currentdate = $calendartype->convert_from_gregorian($year, $month, $day); $currentyear = $currentdate['year']; $arryears = $calendartype->get_years(); // Add elements. $form->addElement('select', 'param1', get_string('startyear', 'profilefield_datetime'), $arryears); $form->setType('param1', PARAM_INT); $form->setDefault('param1', $currentyear); $form->addElement('select', 'param2', get_string('endyear', 'profilefield_datetime'), $arryears); $form->setType('param2', PARAM_INT); $form->setDefault('param2', $currentyear); $form->addElement('checkbox', 'param3', get_string('wanttime', 'profilefield_datetime')); $form->setType('param3', PARAM_INT); $form->addElement('hidden', 'startday', '1'); $form->setType('startday', PARAM_INT); $form->addElement('hidden', 'startmonth', '1'); $form->setType('startmonth', PARAM_INT); $form->addElement('hidden', 'startyear', '1'); $form->setType('startyear', PARAM_INT); $form->addElement('hidden', 'endday', '1'); $form->setType('endday', PARAM_INT); $form->addElement('hidden', 'endmonth', '1'); $form->setType('endmonth', PARAM_INT); $form->addElement('hidden', 'endyear', '1'); $form->setType('endyear', PARAM_INT); $form->addElement('hidden', 'defaultdata', '0'); $form->setType('defaultdata', PARAM_INT); } /** * Validate the data from the profile field form. * * @param stdClass $data from the add/edit profile field form * @param array $files * @return array associative array of error messages */ public function define_validate_specific($data, $files) { $errors = array(); // Make sure the start year is not greater than the end year. if ($data->param1 > $data->param2) { $errors['param1'] = get_string('startyearafterend', 'profilefield_datetime'); } return $errors; } /** * Alter form based on submitted or existing data. * * @param moodleform $mform */ public function define_after_data(&$mform) { global $DB; // If we are adding a new profile field then the dates have already been set // by setDefault to the correct dates in the used calendar system. We only want // to execute the rest of the code when we have the years in the DB saved in // Gregorian that need converting to the date for this user. $id = optional_param('id', 0, PARAM_INT); if ($id === 0) { return; } // Get the field data from the DB. $field = $DB->get_record('user_info_field', array('id' => $id), 'param1, param2', MUST_EXIST); // Get the current calendar in use - see MDL-18375. $calendartype = \core_calendar\type_factory::get_calendar_instance(); // An array to store form values. $values = array(); // The start and end year will be set as a Gregorian year in the DB. We want // convert these to the equivalent year in the current calendar type being used. $startdate = $calendartype->convert_from_gregorian($field->param1, 1, 1); $values['startday'] = $startdate['day']; $values['startmonth'] = $startdate['month']; $values['startyear'] = $startdate['year']; $values['param1'] = $startdate['year']; $stopdate = $calendartype->convert_from_gregorian($field->param2, 1, 1); $values['endday'] = $stopdate['day']; $values['endmonth'] = $stopdate['month']; $values['endyear'] = $stopdate['year']; $values['param2'] = $stopdate['year']; // Set the values. foreach ($values as $key => $value) { $param = $mform->getElement($key); $param->setValue($value); } } /** * Preprocess data from the profile field form before * it is saved. * * @param stdClass $data from the add/edit profile field form * @return stdClass processed data object */ public function define_save_preprocess($data) { // Get the current calendar in use - see MDL-18375. $calendartype = \core_calendar\type_factory::get_calendar_instance(); // Check if the start year was changed, if it was then convert from the start of that year. if ($data->param1 != $data->startyear) { $startdate = $calendartype->convert_to_gregorian($data->param1, 1, 1); } else { $startdate = $calendartype->convert_to_gregorian($data->param1, $data->startmonth, $data->startday); } // Check if the end year was changed, if it was then convert from the start of that year. if ($data->param2 != $data->endyear) { $stopdate = $calendartype->convert_to_gregorian($data->param2, 1, 1); } else { $stopdate = $calendartype->convert_to_gregorian($data->param2, $data->endmonth, $data->endday); } $data->param1 = $startdate['year']; $data->param2 = $stopdate['year']; if (empty($data->param3)) { $data->param3 = null; } // No valid value in the default data column needed. $data->defaultdata = '0'; return $data; } } profile/field/datetime/version.php 0000644 00000002256 15151162244 0013267 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version information for the datetime field. * * @package profilefield_datetime * @copyright 2010 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'profilefield_datetime'; // Full name of the plugin (used for diagnostics) profile/field/datetime/classes/privacy/provider.php 0000644 00000017403 15151162244 0016546 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy class for requesting user data. * * @package profilefield_datetime * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_datetime\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\transform; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Privacy class for requesting user data. * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { return $collection->add_database_table('user_info_data', [ 'userid' => 'privacy:metadata:profilefield_datetime:userid', 'fieldid' => 'privacy:metadata:profilefield_datetime:fieldid', 'data' => 'privacy:metadata:profilefield_datetime:data', 'dataformat' => 'privacy:metadata:profilefield_datetime:dataformat' ], 'privacy:metadata:profilefield_datetime:tableexplanation'); } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : contextlist { $sql = "SELECT ctx.id FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id JOIN {context} ctx ON ctx.instanceid = uda.userid AND ctx.contextlevel = :contextlevel WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'contextlevel' => CONTEXT_USER, 'datatype' => 'datetime' ]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT uda.userid FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $context->instanceid, 'datatype' => 'datetime' ]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { $results = static::get_records($user->id); foreach ($results as $result) { $data = (object) [ 'name' => $result->name, 'description' => $result->description, 'data' => transform::date($result->data) ]; \core_privacy\local\request\writer::with_context($context)->export_data([ get_string('pluginname', 'profilefield_datetime')], $data); } } } } /** * Delete all user data which matches the specified context. * * @param context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { // Delete data only for user context. if ($context->contextlevel == CONTEXT_USER) { static::delete_data($context->instanceid); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_data($context->instanceid); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { static::delete_data($context->instanceid); } } } /** * Delete data related to a userid. * * @param int $userid The user ID */ protected static function delete_data($userid) { global $DB; $params = [ 'userid' => $userid, 'datatype' => 'datetime' ]; $DB->delete_records_select('user_info_data', "fieldid IN ( SELECT id FROM {user_info_field} WHERE datatype = :datatype) AND userid = :userid", $params); } /** * Get records related to this plugin and user. * * @param int $userid The user ID * @return array An array of records. */ protected static function get_records($userid) { global $DB; $sql = "SELECT * FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'datatype' => 'datetime' ]; return $DB->get_records_sql($sql, $params); } } profile/field/datetime/lang/en/profilefield_datetime.php 0000644 00000003533 15151162244 0017444 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The english language pack used in this profile field type. * * @package profilefield_datetime * @copyright 2010 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ $string['currentdatedefault'] = 'Check to use current date as default'; $string['defaultdate'] = 'Default date'; $string['endyear'] = 'End year'; $string['notset'] = 'Not set'; $string['pluginname'] = 'Date/Time'; $string['privacy:metadata:profilefield_datetime:userid'] = 'The ID of the user whose data is stored by the Date/time user profile field'; $string['privacy:metadata:profilefield_datetime:fieldid'] = 'The ID of the profile field'; $string['privacy:metadata:profilefield_datetime:data'] = 'Date/time user profile field user data'; $string['privacy:metadata:profilefield_datetime:dataformat'] = 'The format of Date/time user profile field user data'; $string['privacy:metadata:profilefield_datetime:tableexplanation'] = 'Additional profile data'; $string['specifydatedefault'] = 'or specify a date'; $string['startyearafterend'] = 'The start year can\'t occur after the end year'; $string['startyear'] = 'Start year'; $string['wanttime'] = 'Include time?'; profile/field/upgrade.txt 0000644 00000000417 15151162244 0011462 0 ustar 00 This files describes API changes in /user/profile/field/* - user profile fields, information provided here is intended especially for developers. === 3.4 === * profile_field_base::__construct() now takes three arguments instead of two. Update your plugins if required. profile/field/text/field.class.php 0000644 00000005316 15151162244 0013161 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Text profile field. * * @package profilefield_text * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_field_text * * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_field_text extends profile_field_base { /** * Overwrite the base class to display the data for this field */ public function display_data() { // Default formatting. $data = format_string($this->data); // Are we creating a link? if (!empty($this->field->param4) && !empty($data)) { // Define the target. if (! empty($this->field->param5)) { $target = 'target="'.$this->field->param5.'"'; } else { $target = ''; } // Create the link. $data = '<a href="'.str_replace('$$', urlencode($data), $this->field->param4).'" '.$target.'>'.htmlspecialchars($data, ENT_COMPAT).'</a>'; } return $data; } /** * Add fields for editing a text profile field. * @param moodleform $mform */ public function edit_field_add($mform) { $size = $this->field->param1; $maxlength = $this->field->param2; $fieldtype = ($this->field->param3 == 1 ? 'password' : 'text'); // Create the form field. $mform->addElement($fieldtype, $this->inputname, format_string($this->field->name), 'maxlength="'.$maxlength.'" size="'.$size.'" '); $mform->setType($this->inputname, PARAM_TEXT); } /** * Return the field type and null properties. * This will be used for validating the data submitted by a user. * * @return array the param type and null property * @since Moodle 3.2 */ public function get_field_properties() { return array(PARAM_TEXT, NULL_NOT_ALLOWED); } } profile/field/text/tests/privacy/provider_test.php 0000644 00000030702 15151162244 0016477 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Base class for unit tests for profilefield_text. * * @package profilefield_text * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_text\privacy; use core_privacy\tests\provider_testcase; use profilefield_text\privacy\provider; use core_privacy\local\request\approved_userlist; /** * Unit tests for user\profile\field\text\classes\privacy\provider.php * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { $this->resetAfterTest(true); } /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create profile field. $profilefieldid = $this->add_profile_field($categoryid, 'text'); // Create a user. $user = $this->getDataGenerator()->create_user(); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // Get the field that was created. $userfielddata = $DB->get_records('user_info_data', array('userid' => $user->id)); // Confirm we got the right number of user field data. $this->assertCount(1, $userfielddata); $context = \context_user::instance($user->id); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertEquals($context, $contextlist->current()); } /** * Test that data is exported correctly for this plugin. */ public function test_export_user_data() { // Create profile category. $categoryid = $this->add_profile_category(); // Create text profile field. $textprofilefieldid = $this->add_profile_field($categoryid, 'text'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add text user info data. $this->add_user_info_data($user->id, $textprofilefieldid, 'test text'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($user->id, $context, 'profilefield_text'); $data = $writer->get_data([get_string('pluginname', 'profilefield_text')]); $this->assertCount(3, (array) $data); $this->assertEquals('Test field', $data->name); $this->assertEquals('This is a test.', $data->description); $this->assertEquals('test text', $data->data); } /** * Test that user data is deleted using the context. */ public function test_delete_data_for_all_users_in_context() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create text profile field. $textprofilefieldid = $this->add_profile_field($categoryid, 'text'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add text user info data. $this->add_user_info_data($user->id, $textprofilefieldid, 'test text'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); provider::delete_data_for_all_users_in_context($context); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('test text', reset($userinfodata)->data); } /** * Test that user data is deleted for this user. */ public function test_delete_data_for_user() { global $DB; // Create profile category. $categoryid = $this->add_profile_category(); // Create text profile field. $textprofilefieldid = $this->add_profile_field($categoryid, 'text'); // Create checkbox profile field. $checkboxprofilefieldid = $this->add_profile_field($categoryid, 'checkbox'); // Create a user. $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Add text user info data. $this->add_user_info_data($user->id, $textprofilefieldid, 'test text'); // Add checkbox user info data. $this->add_user_info_data($user->id, $checkboxprofilefieldid, 'test data'); // Check that we have two entries. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(2, $userinfodata); $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'profilefield_text', [$context->id]); provider::delete_data_for_user($approvedlist); // Check that the correct profile field has been deleted. $userinfodata = $DB->get_records('user_info_data', ['userid' => $user->id]); $this->assertCount(1, $userinfodata); $this->assertNotEquals('test text', reset($userinfodata)->data); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context() { $this->resetAfterTest(); $component = 'profilefield_text'; // Create profile category. $categoryid = $this->add_profile_category(); // Create text profile field. $profilefieldid = $this->add_profile_field($categoryid, 'text'); // Create a user. $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (related data still haven't been created). $userlist = new \core_privacy\local\request\userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); $this->add_user_info_data($user->id, $profilefieldid, 'test data'); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users() { $this->resetAfterTest(); $component = 'profilefield_text'; // Create profile category. $categoryid = $this->add_profile_category(); // Create text profile field. $profilefieldid = $this->add_profile_field($categoryid, 'text'); // Create user1. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); $this->add_user_info_data($user1->id, $profilefieldid, 'test data'); $this->add_user_info_data($user2->id, $profilefieldid, 'test data'); // The list of users for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); } /** * Add dummy user info data. * * @param int $userid The ID of the user * @param int $fieldid The ID of the field * @param string $data The data */ private function add_user_info_data($userid, $fieldid, $data) { global $DB; $userinfodata = array( 'userid' => $userid, 'fieldid' => $fieldid, 'data' => $data, 'dataformat' => 0 ); $DB->insert_record('user_info_data', $userinfodata); } /** * Add dummy profile category. * * @return int The ID of the profile category */ private function add_profile_category() { $cat = $this->getDataGenerator()->create_custom_profile_field_category(['name' => 'Test category']); return $cat->id; } /** * Add dummy profile field. * * @param int $categoryid The ID of the profile category * @param string $datatype The datatype of the profile field * @return int The ID of the profile field */ private function add_profile_field($categoryid, $datatype) { $data = $this->getDataGenerator()->create_custom_profile_field([ 'datatype' => $datatype, 'shortname' => 'tstField', 'name' => 'Test field', 'description' => 'This is a test.', 'categoryid' => $categoryid, ]); return $data->id; } } profile/field/text/tests/field_class_test.php 0000644 00000005035 15151162244 0015441 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace profilefield_text; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/profile/field/text/field.class.php'); use profile_field_text; /** * Unit tests for the profilefield_text. * * @package profilefield_text * @copyright 2022 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \profilefield_text\profile_field_text */ class field_class_test extends \advanced_testcase { /** * Test that the profile text data is formatted and required filters applied * * @covers \profile_field_text::display_data * @dataProvider filter_profile_field_text_provider * @param string $input * @param string $expected */ public function test_filter_display_data(string $input, string $expected): void { $this->resetAfterTest(); $field = new profile_field_text(); $field->data = $input; filter_set_global_state('multilang', TEXTFILTER_ON); filter_set_global_state('emoticon', TEXTFILTER_ON); filter_set_applies_to_strings('multilang', true); $actual = $field->display_data(); $this->assertEquals($expected, $actual); } /** * Data provider for {@see test_filter_display_data} * * @return string[] */ public function filter_profile_field_text_provider(): array { return [ 'simple_string' => ['Simple string', 'Simple string'], 'format_string' => ['HTML & is escaped', 'HTML & is escaped'], 'multilang_filter' => ['<span class="multilang" lang="en">English</span><span class="multilang" lang="fr">French</span>', 'English'], 'emoticons_filter' => ['No emoticons filter :-(', 'No emoticons filter :-('] ]; } } profile/field/text/define.class.php 0000644 00000006175 15151162244 0013334 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Text profile field definition. * * @package profilefield_text * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class profile_define_text * * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_define_text extends profile_define_base { /** * Add elements for creating/editing a text profile field. * * @param MoodleQuickForm $form */ public function define_form_specific($form) { // Default data. $form->addElement('text', 'defaultdata', get_string('profiledefaultdata', 'admin'), 'size="50"'); $form->setType('defaultdata', PARAM_TEXT); // Param 1 for text type is the size of the field. $form->addElement('text', 'param1', get_string('profilefieldsize', 'admin'), 'size="6"'); $form->setDefault('param1', 30); $form->setType('param1', PARAM_INT); // Param 2 for text type is the maxlength of the field. $form->addElement('text', 'param2', get_string('profilefieldmaxlength', 'admin'), 'size="6"'); $form->setDefault('param2', 2048); $form->setType('param2', PARAM_INT); $form->addHelpButton('param2', 'profilefieldmaxlength', 'admin'); // Param 3 for text type detemines if this is a password field or not. $form->addElement('selectyesno', 'param3', get_string('profilefieldispassword', 'admin')); $form->setDefault('param3', 0); // Defaults to 'no'. $form->setType('param3', PARAM_INT); // Param 4 for text type contains a link. $form->addElement('text', 'param4', get_string('profilefieldlink', 'admin')); $form->setType('param4', PARAM_URL); $form->addHelpButton('param4', 'profilefieldlink', 'admin'); // Param 5 for text type contains link target. $targetoptions = array( '' => get_string('linktargetnone', 'editor'), '_blank' => get_string('linktargetblank', 'editor'), '_self' => get_string('linktargetself', 'editor'), '_top' => get_string('linktargettop', 'editor') ); $form->addElement('select', 'param5', get_string('profilefieldlinktarget', 'admin'), $targetoptions); $form->setType('param5', PARAM_RAW); } } profile/field/text/version.php 0000644 00000002274 15151162244 0012457 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Text profile field version information. * * @package profilefield_text * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'profilefield_text'; // Full name of the plugin (used for diagnostics). profile/field/text/classes/privacy/provider.php 0000644 00000017145 15151162244 0015741 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy class for requesting user data. * * @package profilefield_text * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace profilefield_text\privacy; use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Privacy class for requesting user data. * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { return $collection->add_database_table('user_info_data', [ 'userid' => 'privacy:metadata:profilefield_text:userid', 'fieldid' => 'privacy:metadata:profilefield_text:fieldid', 'data' => 'privacy:metadata:profilefield_text:data', 'dataformat' => 'privacy:metadata:profilefield_text:dataformat' ], 'privacy:metadata:profilefield_text:tableexplanation'); } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : contextlist { $sql = "SELECT ctx.id FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id JOIN {context} ctx ON ctx.instanceid = uda.userid AND ctx.contextlevel = :contextlevel WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'contextlevel' => CONTEXT_USER, 'datatype' => 'text' ]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT uda.userid FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $context->instanceid, 'datatype' => 'text' ]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { $results = static::get_records($user->id); foreach ($results as $result) { $data = (object) [ 'name' => $result->name, 'description' => $result->description, 'data' => $result->data ]; \core_privacy\local\request\writer::with_context($context)->export_data([ get_string('pluginname', 'profilefield_text')], $data); } } } } /** * Delete all user data which matches the specified context. * * @param context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { // Delete data only for user context. if ($context->contextlevel == CONTEXT_USER) { static::delete_data($context->instanceid); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_data($context->instanceid); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { $user = $contextlist->get_user(); foreach ($contextlist->get_contexts() as $context) { // Check if the context is a user context. if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $user->id) { static::delete_data($context->instanceid); } } } /** * Delete data related to a userid. * * @param int $userid The user ID */ protected static function delete_data($userid) { global $DB; $params = [ 'userid' => $userid, 'datatype' => 'text' ]; $DB->delete_records_select('user_info_data', "fieldid IN ( SELECT id FROM {user_info_field} WHERE datatype = :datatype) AND userid = :userid", $params); } /** * Get records related to this plugin and user. * * @param int $userid The user ID * @return array An array of records. */ protected static function get_records($userid) { global $DB; $sql = "SELECT * FROM {user_info_data} uda JOIN {user_info_field} uif ON uda.fieldid = uif.id WHERE uda.userid = :userid AND uif.datatype = :datatype"; $params = [ 'userid' => $userid, 'datatype' => 'text' ]; return $DB->get_records_sql($sql, $params); } } profile/field/text/lang/en/profilefield_text.php 0000644 00000002767 15151162244 0016034 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'profilefield_text', language 'en', branch 'MOODLE_20_STABLE' * * @package profilefield_text * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Text input'; $string['privacy:metadata:profilefield_text:userid'] = 'The ID of the user whose data is stored by the Text input user profile field'; $string['privacy:metadata:profilefield_text:fieldid'] = 'The ID of the profile field'; $string['privacy:metadata:profilefield_text:data'] = 'Text input user profile field user data'; $string['privacy:metadata:profilefield_text:dataformat'] = 'The format of Text input user profile field user data'; $string['privacy:metadata:profilefield_text:tableexplanation'] = 'Additional profile data'; profile/index.php 0000644 00000014230 15151162244 0010025 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Manage user profile fields. * @package core_user * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/profile/definelib.php'); admin_externalpage_setup('profilefields'); $action = optional_param('action', '', PARAM_ALPHA); $redirect = $CFG->wwwroot.'/user/profile/index.php'; $strdefaultcategory = get_string('profiledefaultcategory', 'admin'); $strcreatefield = get_string('profilecreatefield', 'admin'); // Do we have any actions to perform before printing the header. switch ($action) { case 'movecategory': $id = required_param('id', PARAM_INT); $dir = required_param('dir', PARAM_ALPHA); if (confirm_sesskey()) { profile_move_category($id, $dir); } redirect($redirect); break; case 'movefield': $id = required_param('id', PARAM_INT); $dir = required_param('dir', PARAM_ALPHA); if (confirm_sesskey()) { profile_move_field($id, $dir); } redirect($redirect); break; case 'deletecategory': $id = required_param('id', PARAM_INT); if (confirm_sesskey()) { profile_delete_category($id); } redirect($redirect, get_string('deleted')); break; case 'deletefield': $id = required_param('id', PARAM_INT); $confirm = optional_param('confirm', 0, PARAM_BOOL); // If no userdata for profile than don't show confirmation. $datacount = $DB->count_records('user_info_data', array('fieldid' => $id)); if (((data_submitted() and $confirm) or ($datacount === 0)) and confirm_sesskey()) { profile_delete_field($id); redirect($redirect, get_string('deleted')); } // Ask for confirmation, as there is user data available for field. $fieldname = $DB->get_field('user_info_field', 'name', array('id' => $id)); $optionsyes = array ('id' => $id, 'confirm' => 1, 'action' => 'deletefield', 'sesskey' => sesskey()); $strheading = get_string('profiledeletefield', 'admin', format_string($fieldname)); $PAGE->navbar->add($strheading); echo $OUTPUT->header(); echo $OUTPUT->heading($strheading); $formcontinue = new single_button(new moodle_url($redirect, $optionsyes), get_string('yes'), 'post'); $formcancel = new single_button(new moodle_url($redirect), get_string('no'), 'get'); echo $OUTPUT->confirm(get_string('profileconfirmfielddeletion', 'admin', $datacount), $formcontinue, $formcancel); echo $OUTPUT->footer(); die; break; default: // Normal form. } // Show all categories. $categories = $DB->get_records('user_info_category', null, 'sortorder ASC'); // Check that we have at least one category defined. if (empty($categories)) { $defaultcategory = new stdClass(); $defaultcategory->name = $strdefaultcategory; $defaultcategory->sortorder = 1; $DB->insert_record('user_info_category', $defaultcategory); redirect($redirect); } // Print the header. echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('profilefields', 'admin')); $outputcategories = []; $options = profile_list_datatypes(); foreach ($categories as $category) { // Category fields. $outputfields = []; if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) { foreach ($fields as $field) { $fieldname = format_string($field->name); $component = 'profilefield_' . $field->datatype; $classname = "\\$component\\helper"; if (class_exists($classname) && method_exists($classname, 'get_fieldname')) { $fieldname = $classname::get_fieldname($field->name); } $outputfields[] = [ 'id' => $field->id, 'shortname' => $field->shortname, 'datatype' => $field->datatype, 'name' => $fieldname, 'isfirst' => !count($outputfields), 'islast' => count($outputfields) == count($fields) - 1, ]; } } // Add new field menu. $menu = new \action_menu(); $menu->set_menu_trigger($strcreatefield); foreach ($options as $type => $fieldname) { $action = new \action_menu_link_secondary(new \moodle_url('#'), null, $fieldname, ['data-action' => 'createfield', 'data-categoryid' => $category->id, 'data-datatype' => $type, 'data-datatypename' => $fieldname]); $menu->add($action); } $menu->attributes['class'] .= ' float-left mr-1'; // Add category information to the template. $outputcategories[] = [ 'id' => $category->id, 'name' => format_string($category->name), 'fields' => $outputfields, 'hasfields' => count($outputfields), 'isfirst' => !count($outputcategories), 'islast' => count($outputcategories) == count($categories) - 1, 'candelete' => count($categories) > 1, 'addfieldmenu' => $menu->export_for_template($OUTPUT), ]; } echo $OUTPUT->render_from_template('core_user/edit_profile_fields', [ 'categories' => $outputcategories, 'sesskey' => sesskey(), 'baseurl' => (new moodle_url('/user/profile/index.php'))->out(false) ]); echo $OUTPUT->footer(); profile/lib.php 0000644 00000103745 15151162244 0007476 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Profile field API library file. * * @package core_user * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Visible to anyone who has the moodle/site:viewuseridentity permission. * Editable by the profile owner if they have the moodle/user:editownprofile capability * or any user with the moodle/user:update capability. */ define('PROFILE_VISIBLE_TEACHERS', '3'); /** * Visible to anyone who can view the user. * Editable by the profile owner if they have the moodle/user:editownprofile capability * or any user with the moodle/user:update capability. */ define('PROFILE_VISIBLE_ALL', '2'); /** * Visible to the profile owner or anyone with the moodle/user:viewalldetails capability. * Editable by the profile owner if they have the moodle/user:editownprofile capability * or any user with moodle/user:viewalldetails and moodle/user:update capabilities. */ define('PROFILE_VISIBLE_PRIVATE', '1'); /** * Only visible to users with the moodle/user:viewalldetails capability. * Only editable by users with the moodle/user:viewalldetails and moodle/user:update capabilities. */ define('PROFILE_VISIBLE_NONE', '0'); /** * Base class for the customisable profile fields. * * @package core_user * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_field_base { // These 2 variables are really what we're interested in. // Everything else can be extracted from them. /** @var int */ public $fieldid; /** @var int */ public $userid; /** @var stdClass */ public $field; /** @var string */ public $inputname; /** @var mixed */ public $data; /** @var string */ public $dataformat; /** @var string name of the user profile category */ protected $categoryname; /** * Constructor method. * @param int $fieldid id of the profile from the user_info_field table * @param int $userid id of the user for whom we are displaying data * @param stdClass $fielddata optional data for the field object plus additional fields 'hasuserdata', 'data' and 'dataformat' * with user data. (If $fielddata->hasuserdata is empty, user data is not available and we should use default data). * If this parameter is passed, constructor will not call load_data() at all. */ public function __construct($fieldid=0, $userid=0, $fielddata=null) { global $CFG; if ($CFG->debugdeveloper) { // In Moodle 3.4 the new argument $fielddata was added to the constructor. Make sure that // plugin constructor properly passes this argument. $backtrace = debug_backtrace(); if (isset($backtrace[1]['class']) && $backtrace[1]['function'] === '__construct' && in_array(self::class, class_parents($backtrace[1]['class']))) { // If this constructor is called from the constructor of the plugin make sure that the third argument was passed through. if (count($backtrace[1]['args']) >= 3 && count($backtrace[0]['args']) < 3) { debugging($backtrace[1]['class'].'::__construct() must support $fielddata as the third argument ' . 'and pass it to the parent constructor', DEBUG_DEVELOPER); } } } $this->set_fieldid($fieldid); $this->set_userid($userid); if ($fielddata) { $this->set_field($fielddata); if ($userid > 0 && !empty($fielddata->hasuserdata)) { $this->set_user_data($fielddata->data, $fielddata->dataformat); } } else { $this->load_data(); } } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function profile_field_base($fieldid=0, $userid=0) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($fieldid, $userid); } /** * Abstract method: Adds the profile field to the moodle form class * @abstract The following methods must be overwritten by child classes * @param MoodleQuickForm $mform instance of the moodleform class */ public function edit_field_add($mform) { throw new \moodle_exception('mustbeoveride', 'debug', '', 'edit_field_add'); } /** * Display the data for this field * @return string */ public function display_data() { $options = new stdClass(); $options->para = false; return format_text($this->data, FORMAT_MOODLE, $options); } /** * Print out the form field in the edit profile page * @param MoodleQuickForm $mform instance of the moodleform class * @return bool */ public function edit_field($mform) { if (!$this->is_editable()) { return false; } $this->edit_field_add($mform); $this->edit_field_set_default($mform); $this->edit_field_set_required($mform); return true; } /** * Tweaks the edit form * @param MoodleQuickForm $mform instance of the moodleform class * @return bool */ public function edit_after_data($mform) { if (!$this->is_editable()) { return false; } $this->edit_field_set_locked($mform); return true; } /** * Saves the data coming from form * @param stdClass $usernew data coming from the form */ public function edit_save_data($usernew) { global $DB; if (!isset($usernew->{$this->inputname})) { // Field not present in form, probably locked and invisible - skip it. return; } $data = new stdClass(); $usernew->{$this->inputname} = $this->edit_save_data_preprocess($usernew->{$this->inputname}, $data); if (!isset($usernew->{$this->inputname})) { // Field cannot be set to null, set the default value. $usernew->{$this->inputname} = $this->field->defaultdata; } $data->userid = $usernew->id; $data->fieldid = $this->field->id; $data->data = $usernew->{$this->inputname}; if ($dataid = $DB->get_field('user_info_data', 'id', array('userid' => $data->userid, 'fieldid' => $data->fieldid))) { $data->id = $dataid; $DB->update_record('user_info_data', $data); } else { $DB->insert_record('user_info_data', $data); } } /** * Validate the form field from profile page * * @param stdClass $usernew * @return array error messages for the form validation */ public function edit_validate_field($usernew) { global $DB; $errors = array(); // Get input value. if (isset($usernew->{$this->inputname})) { if (is_array($usernew->{$this->inputname}) && isset($usernew->{$this->inputname}['text'])) { $value = $usernew->{$this->inputname}['text']; } else { $value = $usernew->{$this->inputname}; } } else { $value = ''; } // Check for uniqueness of data if required. if ($this->is_unique() && (($value !== '') || $this->is_required())) { $data = $DB->get_records_sql(' SELECT id, userid FROM {user_info_data} WHERE fieldid = ? AND ' . $DB->sql_compare_text('data', 255) . ' = ' . $DB->sql_compare_text('?', 255), array($this->field->id, $value)); if ($data) { $existing = false; foreach ($data as $v) { if ($v->userid == $usernew->id) { $existing = true; break; } } if (!$existing) { $errors[$this->inputname] = get_string('valuealreadyused'); } } } return $errors; } /** * Sets the default data for the field in the form object * @param MoodleQuickForm $mform instance of the moodleform class */ public function edit_field_set_default($mform) { if (!empty($this->field->defaultdata)) { $mform->setDefault($this->inputname, $this->field->defaultdata); } } /** * Sets the required flag for the field in the form object * * @param MoodleQuickForm $mform instance of the moodleform class */ public function edit_field_set_required($mform) { global $USER; if ($this->is_required() && ($this->userid == $USER->id || isguestuser())) { $mform->addRule($this->inputname, get_string('required'), 'required', null, 'client'); } } /** * HardFreeze the field if locked. * @param MoodleQuickForm $mform instance of the moodleform class */ public function edit_field_set_locked($mform) { if (!$mform->elementExists($this->inputname)) { return; } if ($this->is_locked() and !has_capability('moodle/user:update', context_system::instance())) { $mform->hardFreeze($this->inputname); $mform->setConstant($this->inputname, $this->data); } } /** * Hook for child classess to process the data before it gets saved in database * @param stdClass $data * @param stdClass $datarecord The object that will be used to save the record * @return mixed */ public function edit_save_data_preprocess($data, $datarecord) { return $data; } /** * Loads a user object with data for this field ready for the edit profile * form * @param stdClass $user a user object */ public function edit_load_user_data($user) { if ($this->data !== null) { $user->{$this->inputname} = $this->data; } } /** * Check if the field data should be loaded into the user object * By default it is, but for field types where the data may be potentially * large, the child class should override this and return false * @return bool */ public function is_user_object_data() { return true; } /** * Accessor method: set the userid for this instance * @internal This method should not generally be overwritten by child classes. * @param integer $userid id from the user table */ public function set_userid($userid) { $this->userid = $userid; } /** * Accessor method: set the fieldid for this instance * @internal This method should not generally be overwritten by child classes. * @param integer $fieldid id from the user_info_field table */ public function set_fieldid($fieldid) { $this->fieldid = $fieldid; } /** * Sets the field object and default data and format into $this->data and $this->dataformat * * This method should be called before {@link self::set_user_data} * * @param stdClass $field * @throws coding_exception */ public function set_field($field) { global $CFG; if ($CFG->debugdeveloper) { $properties = ['id', 'shortname', 'name', 'datatype', 'description', 'descriptionformat', 'categoryid', 'sortorder', 'required', 'locked', 'visible', 'forceunique', 'signup', 'defaultdata', 'defaultdataformat', 'param1', 'param2', 'param3', 'param4', 'param5']; foreach ($properties as $property) { if (!property_exists($field, $property)) { debugging('The \'' . $property . '\' property must be set.', DEBUG_DEVELOPER); } } } if ($this->fieldid && $this->fieldid != $field->id) { throw new coding_exception('Can not set field object after a different field id was set'); } $this->fieldid = $field->id; $this->field = $field; $this->inputname = 'profile_field_' . $this->field->shortname; $this->data = $this->field->defaultdata; $this->dataformat = FORMAT_HTML; } /** * Sets user id and user data for the field * * @param mixed $data * @param int $dataformat */ public function set_user_data($data, $dataformat) { $this->data = $data; $this->dataformat = $dataformat; } /** * Set the name for the profile category where this field is * * @param string $categoryname */ public function set_category_name($categoryname) { $this->categoryname = $categoryname; } /** * Return field short name * * @return string */ public function get_shortname(): string { return $this->field->shortname; } /** * Returns the name of the profile category where this field is * * @return string */ public function get_category_name() { global $DB; if ($this->categoryname === null) { $this->categoryname = $DB->get_field('user_info_category', 'name', ['id' => $this->field->categoryid]); } return $this->categoryname; } /** * Accessor method: Load the field record and user data associated with the * object's fieldid and userid * * @internal This method should not generally be overwritten by child classes. */ public function load_data() { global $DB; // Load the field object. if (($this->fieldid == 0) or (!($field = $DB->get_record('user_info_field', array('id' => $this->fieldid))))) { $this->field = null; $this->inputname = ''; } else { $this->set_field($field); } if (!empty($this->field) && $this->userid > 0) { $params = array('userid' => $this->userid, 'fieldid' => $this->fieldid); if ($data = $DB->get_record('user_info_data', $params, 'data, dataformat')) { $this->set_user_data($data->data, $data->dataformat); } } else { $this->data = null; } } /** * Check if the field data is visible to the current user * @internal This method should not generally be overwritten by child classes. * * @param context|null $context * @return bool */ public function is_visible(?context $context = null): bool { global $USER, $COURSE; if ($context === null) { $context = ($this->userid > 0) ? context_user::instance($this->userid) : context_system::instance(); } switch ($this->field->visible) { case PROFILE_VISIBLE_TEACHERS: if ($this->is_signup_field() && (empty($this->userid) || isguestuser($this->userid))) { return true; } else if ($this->userid == $USER->id) { return true; } else if ($this->userid > 0) { return has_capability('moodle/user:viewalldetails', $context); } else { $coursecontext = context_course::instance($COURSE->id); return has_capability('moodle/site:viewuseridentity', $coursecontext); } case PROFILE_VISIBLE_ALL: return true; case PROFILE_VISIBLE_PRIVATE: if ($this->is_signup_field() && (empty($this->userid) || isguestuser($this->userid))) { return true; } else if ($this->userid == $USER->id) { return true; } else { return has_capability('moodle/user:viewalldetails', $context); } default: // PROFILE_VISIBLE_NONE, so let's check capabilities at system level. if ($this->userid > 0) { $context = context_system::instance(); } return has_capability('moodle/user:viewalldetails', $context); } } /** * Check if the field data is editable for the current user * This method should not generally be overwritten by child classes. * @return bool */ public function is_editable() { global $USER; if (!$this->is_visible()) { return false; } if ($this->is_signup_field() && (empty($this->userid) || isguestuser($this->userid))) { // Allow editing the field on the signup page. return true; } $systemcontext = context_system::instance(); if ($this->userid == $USER->id && has_capability('moodle/user:editownprofile', $systemcontext)) { return true; } if (has_capability('moodle/user:update', $systemcontext)) { return true; } // Checking for mentors have capability to edit user's profile. if ($this->userid > 0) { $usercontext = context_user::instance($this->userid); if ($this->userid != $USER->id && has_capability('moodle/user:editprofile', $usercontext, $USER->id)) { return true; } } return false; } /** * Check if the field data is considered empty * @internal This method should not generally be overwritten by child classes. * @return boolean */ public function is_empty() { return ( ($this->data != '0') and empty($this->data)); } /** * Check if the field is required on the edit profile page * @internal This method should not generally be overwritten by child classes. * @return bool */ public function is_required() { return (boolean)$this->field->required; } /** * Check if the field is locked on the edit profile page * @internal This method should not generally be overwritten by child classes. * @return bool */ public function is_locked() { return (boolean)$this->field->locked; } /** * Check if the field data should be unique * @internal This method should not generally be overwritten by child classes. * @return bool */ public function is_unique() { return (boolean)$this->field->forceunique; } /** * Check if the field should appear on the signup page * @internal This method should not generally be overwritten by child classes. * @return bool */ public function is_signup_field() { return (boolean)$this->field->signup; } /** * Return the field settings suitable to be exported via an external function. * By default it return all the field settings. * * @return array all the settings * @since Moodle 3.2 */ public function get_field_config_for_external() { return (array) $this->field; } /** * Return the field type and null properties. * This will be used for validating the data submitted by a user. * * @return array the param type and null property * @since Moodle 3.2 */ public function get_field_properties() { return array(PARAM_RAW, NULL_NOT_ALLOWED); } /** * Check if the field should convert the raw data into user-friendly data when exporting * * @return bool */ public function is_transform_supported(): bool { return false; } } /** * Return profile field instance for given type * * @param string $type * @param int $fieldid * @param int $userid * @param stdClass|null $fielddata * @return profile_field_base */ function profile_get_user_field(string $type, int $fieldid = 0, int $userid = 0, ?stdClass $fielddata = null): profile_field_base { global $CFG; require_once("{$CFG->dirroot}/user/profile/field/{$type}/field.class.php"); // Return instance of profile field type. $profilefieldtype = "profile_field_{$type}"; return new $profilefieldtype($fieldid, $userid, $fielddata); } /** * Returns an array of all custom field records with any defined data (or empty data), for the specified user id. * @param int $userid * @return profile_field_base[] */ function profile_get_user_fields_with_data(int $userid): array { global $DB; // Join any user info data present with each user info field for the user object. $sql = 'SELECT uif.*, uic.name AS categoryname '; if ($userid > 0) { $sql .= ', uind.id AS hasuserdata, uind.data, uind.dataformat '; } $sql .= 'FROM {user_info_field} uif '; $sql .= 'LEFT JOIN {user_info_category} uic ON uif.categoryid = uic.id '; if ($userid > 0) { $sql .= 'LEFT JOIN {user_info_data} uind ON uif.id = uind.fieldid AND uind.userid = :userid '; } $sql .= 'ORDER BY uic.sortorder ASC, uif.sortorder ASC '; $fields = $DB->get_records_sql($sql, ['userid' => $userid]); $data = []; foreach ($fields as $field) { $field->hasuserdata = !empty($field->hasuserdata); $fieldobject = profile_get_user_field($field->datatype, $field->id, $userid, $field); $fieldobject->set_category_name($field->categoryname); unset($field->categoryname); $data[] = $fieldobject; } return $data; } /** * Returns an array of all custom field records with any defined data (or empty data), for the specified user id, by category. * @param int $userid * @return profile_field_base[][] */ function profile_get_user_fields_with_data_by_category(int $userid): array { $fields = profile_get_user_fields_with_data($userid); $data = []; foreach ($fields as $field) { $data[$field->field->categoryid][] = $field; } return $data; } /** * Loads user profile field data into the user object. * @param stdClass $user */ function profile_load_data(stdClass $user): void { $fields = profile_get_user_fields_with_data($user->id); foreach ($fields as $formfield) { $formfield->edit_load_user_data($user); } } /** * Print out the customisable categories and fields for a users profile * * @param MoodleQuickForm $mform instance of the moodleform class * @param int $userid id of user whose profile is being edited or 0 for the new user */ function profile_definition(MoodleQuickForm $mform, int $userid = 0): void { $categories = profile_get_user_fields_with_data_by_category($userid); foreach ($categories as $categoryid => $fields) { // Check first if *any* fields will be displayed. $fieldstodisplay = []; foreach ($fields as $formfield) { if ($formfield->is_editable()) { $fieldstodisplay[] = $formfield; } } if (empty($fieldstodisplay)) { continue; } // Display the header and the fields. $mform->addElement('header', 'category_'.$categoryid, format_string($fields[0]->get_category_name())); foreach ($fieldstodisplay as $formfield) { $formfield->edit_field($mform); } } } /** * Adds profile fields to user edit forms. * @param MoodleQuickForm $mform * @param int $userid */ function profile_definition_after_data(MoodleQuickForm $mform, int $userid): void { $userid = ($userid < 0) ? 0 : (int)$userid; $fields = profile_get_user_fields_with_data($userid); foreach ($fields as $formfield) { $formfield->edit_after_data($mform); } } /** * Validates profile data. * @param stdClass $usernew * @param array $files * @return array array of errors, same as in {@see moodleform::validation()} */ function profile_validation(stdClass $usernew, array $files): array { $err = array(); $fields = profile_get_user_fields_with_data($usernew->id); foreach ($fields as $formfield) { $err += $formfield->edit_validate_field($usernew, $files); } return $err; } /** * Saves profile data for a user. * @param stdClass $usernew */ function profile_save_data(stdClass $usernew): void { global $CFG; $fields = profile_get_user_fields_with_data($usernew->id); foreach ($fields as $formfield) { $formfield->edit_save_data($usernew); } } /** * Display profile fields. * * @deprecated since Moodle 3.11 MDL-71051 - please do not use this function any more. * @todo MDL-71413 This will be deleted in Moodle 4.3. * * @param int $userid */ function profile_display_fields($userid) { debugging('Function profile_display_fields() is deprecated because it is no longer used and will be '. 'removed in future versions of Moodle', DEBUG_DEVELOPER); $categories = profile_get_user_fields_with_data_by_category($userid); foreach ($categories as $categoryid => $fields) { foreach ($fields as $formfield) { if ($formfield->is_visible() and !$formfield->is_empty()) { echo html_writer::tag('dt', format_string($formfield->field->name)); echo html_writer::tag('dd', $formfield->display_data()); } } } } /** * Retrieves a list of profile fields that must be displayed in the sign-up form. * * @return array list of profile fields info * @since Moodle 3.2 */ function profile_get_signup_fields(): array { $profilefields = array(); $fieldobjects = profile_get_user_fields_with_data(0); foreach ($fieldobjects as $fieldobject) { $field = (object)$fieldobject->get_field_config_for_external(); if ($fieldobject->get_category_name() !== null && $fieldobject->is_signup_field() && $field->visible <> 0) { $profilefields[] = (object) array( 'categoryid' => $field->categoryid, 'categoryname' => $fieldobject->get_category_name(), 'fieldid' => $field->id, 'datatype' => $field->datatype, 'object' => $fieldobject ); } } return $profilefields; } /** * Adds code snippet to a moodle form object for custom profile fields that * should appear on the signup page * @param MoodleQuickForm $mform moodle form object */ function profile_signup_fields(MoodleQuickForm $mform): void { if ($fields = profile_get_signup_fields()) { foreach ($fields as $field) { // Check if we change the categories. if (!isset($currentcat) || $currentcat != $field->categoryid) { $currentcat = $field->categoryid; $mform->addElement('header', 'category_'.$field->categoryid, format_string($field->categoryname)); }; $field->object->edit_field($mform); } } } /** * Returns an object with the custom profile fields set for the given user * @param int $userid * @param bool $onlyinuserobject True if you only want the ones in $USER. * @return stdClass object where properties names are shortnames of custom profile fields */ function profile_user_record(int $userid, bool $onlyinuserobject = true): stdClass { $usercustomfields = new stdClass(); $fields = profile_get_user_fields_with_data($userid); foreach ($fields as $formfield) { if (!$onlyinuserobject || $formfield->is_user_object_data()) { $usercustomfields->{$formfield->field->shortname} = $formfield->data; } } return $usercustomfields; } /** * Obtains a list of all available custom profile fields, indexed by id. * * Some profile fields are not included in the user object data (see * profile_user_record function above). Optionally, you can obtain only those * fields that are included in the user object. * * To be clear, this function returns the available fields, and does not * return the field values for a particular user. * * @param bool $onlyinuserobject True if you only want the ones in $USER * @return array Array of field objects from database (indexed by id) * @since Moodle 2.7.1 */ function profile_get_custom_fields(bool $onlyinuserobject = false): array { $fieldobjects = profile_get_user_fields_with_data(0); $fields = []; foreach ($fieldobjects as $fieldobject) { if (!$onlyinuserobject || $fieldobject->is_user_object_data()) { $fields[$fieldobject->fieldid] = (object)$fieldobject->get_field_config_for_external(); } } ksort($fields); return $fields; } /** * Load custom profile fields into user object * * @param stdClass $user user object */ function profile_load_custom_fields($user) { $user->profile = (array)profile_user_record($user->id); } /** * Save custom profile fields for a user. * * @param int $userid The user id * @param array $profilefields The fields to save */ function profile_save_custom_fields($userid, $profilefields) { global $DB; $fields = profile_get_user_fields_with_data(0); if ($fields) { foreach ($fields as $fieldobject) { $field = (object)$fieldobject->get_field_config_for_external(); if (isset($profilefields[$field->shortname])) { $conditions = array('fieldid' => $field->id, 'userid' => $userid); $id = $DB->get_field('user_info_data', 'id', $conditions); $data = $profilefields[$field->shortname]; if ($id) { $DB->set_field('user_info_data', 'data', $data, array('id' => $id)); } else { $record = array('fieldid' => $field->id, 'userid' => $userid, 'data' => $data); $DB->insert_record('user_info_data', $record); } } } } } /** * Gets basic data about custom profile fields. This is minimal data that is cached within the * current request for all fields so that it can be used quickly. * * @param string $shortname Shortname of custom profile field * @param bool $casesensitive Whether to perform case-sensitive matching of shortname. Note current limitations of custom profile * fields allow the same shortname to exist differing only by it's case * @return stdClass|null Object with properties id, shortname, name, visible, datatype, categoryid, etc */ function profile_get_custom_field_data_by_shortname(string $shortname, bool $casesensitive = true): ?stdClass { $cache = \cache::make_from_params(cache_store::MODE_REQUEST, 'core_profile', 'customfields', [], ['simplekeys' => true, 'simpledata' => true]); $data = $cache->get($shortname); if ($data === false) { // If we don't have data, we get and cache it for all fields to avoid multiple DB requests. $fields = profile_get_custom_fields(); $data = null; foreach ($fields as $field) { $cache->set($field->shortname, $field); // Perform comparison according to case sensitivity parameter. $shortnamematch = $casesensitive ? strcmp($field->shortname, $shortname) === 0 : strcasecmp($field->shortname, $shortname) === 0; if ($shortnamematch) { $data = $field; } } } return $data; } /** * Trigger a user profile viewed event. * * @param stdClass $user user object * @param stdClass $context context object (course or user) * @param stdClass $course course object * @since Moodle 2.9 */ function profile_view($user, $context, $course = null) { $eventdata = array( 'objectid' => $user->id, 'relateduserid' => $user->id, 'context' => $context ); if (!empty($course)) { $eventdata['courseid'] = $course->id; $eventdata['other'] = array( 'courseid' => $course->id, 'courseshortname' => $course->shortname, 'coursefullname' => $course->fullname ); } $event = \core\event\user_profile_viewed::create($eventdata); $event->add_record_snapshot('user', $user); $event->trigger(); } /** * Does the user have all required custom fields set? * * Internal, to be exclusively used by {@link user_not_fully_set_up()} only. * * Note that if users have no way to fill a required field via editing their * profiles (e.g. the field is not visible or it is locked), we still return true. * So this is actually checking if we should redirect the user to edit their * profile, rather than whether there is a value in the database. * * @param int $userid * @return bool */ function profile_has_required_custom_fields_set($userid) { $profilefields = profile_get_user_fields_with_data($userid); foreach ($profilefields as $profilefield) { if ($profilefield->is_required() && !$profilefield->is_locked() && $profilefield->is_empty() && $profilefield->get_field_config_for_external()['visible']) { return false; } } return true; } /** * Return the list of valid custom profile user fields. * * @return array array of profile field names */ function get_profile_field_names(): array { $profilefields = profile_get_user_fields_with_data(0); $profilefieldnames = []; foreach ($profilefields as $field) { $profilefieldnames[] = $field->inputname; } return $profilefieldnames; } /** * Return the list of profile fields * in a format they can be used for choices in a group select menu. * * @return array array of category name with its profile fields */ function get_profile_field_list(): array { $customfields = profile_get_user_fields_with_data_by_category(0); $data = []; foreach ($customfields as $category) { foreach ($category as $field) { $categoryname = $field->get_category_name(); if (!isset($data[$categoryname])) { $data[$categoryname] = []; } $data[$categoryname][$field->inputname] = $field->field->name; } } return $data; } editadvanced_form.php 0000644 00000031423 15151162244 0010717 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form for editing a users profile * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. } require_once($CFG->dirroot.'/lib/formslib.php'); require_once($CFG->dirroot.'/user/lib.php'); /** * Class user_editadvanced_form. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_editadvanced_form extends moodleform { /** * Define the form. */ public function definition() { global $USER, $CFG, $COURSE; $mform = $this->_form; $editoroptions = null; $filemanageroptions = null; if (!is_array($this->_customdata)) { throw new coding_exception('invalid custom data for user_edit_form'); } $editoroptions = $this->_customdata['editoroptions']; $filemanageroptions = $this->_customdata['filemanageroptions']; $user = $this->_customdata['user']; $userid = $user->id; // Accessibility: "Required" is bad legend text. $strgeneral = get_string('general'); $strrequired = get_string('required'); // Add some extra hidden fields. $mform->addElement('hidden', 'id'); $mform->setType('id', core_user::get_property_type('id')); $mform->addElement('hidden', 'course', $COURSE->id); $mform->setType('course', PARAM_INT); // Print the required moodle fields first. $mform->addElement('header', 'moodle', $strgeneral); $auths = core_component::get_plugin_list('auth'); $enabled = get_string('pluginenabled', 'core_plugin'); $disabled = get_string('plugindisabled', 'core_plugin'); $authoptions = array($enabled => array(), $disabled => array()); $cannotchangepass = array(); $cannotchangeusername = array(); foreach ($auths as $auth => $unused) { $authinst = get_auth_plugin($auth); if (!$authinst->is_internal()) { $cannotchangeusername[] = $auth; } $passwordurl = $authinst->change_password_url(); if (!($authinst->can_change_password() && empty($passwordurl))) { if ($userid < 1 and $authinst->is_internal()) { // This is unlikely but we can not create account without password // when plugin uses passwords, we need to set it initially at least. } else { $cannotchangepass[] = $auth; } } if (is_enabled_auth($auth)) { $authoptions[$enabled][$auth] = get_string('pluginname', "auth_{$auth}"); } else { $authoptions[$disabled][$auth] = get_string('pluginname', "auth_{$auth}"); } } $purpose = user_edit_map_field_purpose($userid, 'username'); $mform->addElement('text', 'username', get_string('username'), 'size="20"' . $purpose); $mform->addHelpButton('username', 'username', 'auth'); $mform->setType('username', PARAM_RAW); if ($userid !== -1) { $mform->disabledIf('username', 'auth', 'in', $cannotchangeusername); } $mform->addElement('selectgroups', 'auth', get_string('chooseauthmethod', 'auth'), $authoptions); $mform->addHelpButton('auth', 'chooseauthmethod', 'auth'); $mform->addElement('advcheckbox', 'suspended', get_string('suspended', 'auth')); $mform->addHelpButton('suspended', 'suspended', 'auth'); $mform->addElement('checkbox', 'createpassword', get_string('createpassword', 'auth')); $mform->disabledIf('createpassword', 'auth', 'in', $cannotchangepass); if (!empty($CFG->passwordpolicy)) { $mform->addElement('static', 'passwordpolicyinfo', '', print_password_policy()); } $purpose = user_edit_map_field_purpose($userid, 'password'); $mform->addElement('passwordunmask', 'newpassword', get_string('newpassword'), 'size="20"' . $purpose); $mform->addHelpButton('newpassword', 'newpassword'); $mform->setType('newpassword', core_user::get_property_type('password')); $mform->disabledIf('newpassword', 'createpassword', 'checked'); $mform->disabledIf('newpassword', 'auth', 'in', $cannotchangepass); // Check if the user has active external tokens. if ($userid and empty($CFG->passwordchangetokendeletion)) { if ($tokens = webservice::get_active_tokens($userid)) { $services = ''; foreach ($tokens as $token) { $services .= format_string($token->servicename) . ','; } $services = get_string('userservices', 'webservice', rtrim($services, ',')); $mform->addElement('advcheckbox', 'signoutofotherservices', get_string('signoutofotherservices'), $services); $mform->addHelpButton('signoutofotherservices', 'signoutofotherservices'); $mform->disabledIf('signoutofotherservices', 'newpassword', 'eq', ''); $mform->setDefault('signoutofotherservices', 1); } } $mform->addElement('advcheckbox', 'preference_auth_forcepasswordchange', get_string('forcepasswordchange')); $mform->addHelpButton('preference_auth_forcepasswordchange', 'forcepasswordchange'); $mform->disabledIf('preference_auth_forcepasswordchange', 'createpassword', 'checked'); // Shared fields. useredit_shared_definition($mform, $editoroptions, $filemanageroptions, $user); // Next the customisable profile fields. profile_definition($mform, $userid); if ($userid == -1) { $btnstring = get_string('createuser'); } else { $btnstring = get_string('updatemyprofile'); } $this->add_action_buttons(true, $btnstring); $this->set_data($user); } /** * Extend the form definition after data has been parsed. */ public function definition_after_data() { global $USER, $CFG, $DB, $OUTPUT; $mform = $this->_form; // Trim required name fields. foreach (useredit_get_required_name_fields() as $field) { $mform->applyFilter($field, 'trim'); } if ($userid = $mform->getElementValue('id')) { $user = $DB->get_record('user', array('id' => $userid)); } else { $user = false; } // User can not change own auth method. if ($userid == $USER->id) { $mform->hardFreeze('auth'); $mform->hardFreeze('preference_auth_forcepasswordchange'); } // Admin must choose some password and supply correct email. if (!empty($USER->newadminuser)) { $mform->addRule('newpassword', get_string('required'), 'required', null, 'client'); if ($mform->elementExists('suspended')) { $mform->removeElement('suspended'); } } // Require password for new users. if ($userid > 0) { if ($mform->elementExists('createpassword')) { $mform->removeElement('createpassword'); } } if ($user and is_mnet_remote_user($user)) { // Only local accounts can be suspended. if ($mform->elementExists('suspended')) { $mform->removeElement('suspended'); } } if ($user and ($user->id == $USER->id or is_siteadmin($user))) { // Prevent self and admin mess ups. if ($mform->elementExists('suspended')) { $mform->hardFreeze('suspended'); } } // Print picture. if (empty($USER->newadminuser)) { if ($user) { $context = context_user::instance($user->id, MUST_EXIST); $fs = get_file_storage(); $hasuploadedpicture = ($fs->file_exists($context->id, 'user', 'icon', 0, '/', 'f2.png') || $fs->file_exists($context->id, 'user', 'icon', 0, '/', 'f2.jpg')); if (!empty($user->picture) && $hasuploadedpicture) { $imagevalue = $OUTPUT->user_picture($user, array('courseid' => SITEID, 'size' => 64)); } else { $imagevalue = get_string('none'); } } else { $imagevalue = get_string('none'); } $imageelement = $mform->getElement('currentpicture'); $imageelement->setValue($imagevalue); if ($user && $mform->elementExists('deletepicture') && !$hasuploadedpicture) { $mform->removeElement('deletepicture'); } } // Next the customisable profile fields. profile_definition_after_data($mform, $userid); } /** * Validate the form data. * @param array $usernew * @param array $files * @return array|bool */ public function validation($usernew, $files) { global $CFG, $DB; $usernew = (object)$usernew; $usernew->username = trim($usernew->username); $user = $DB->get_record('user', array('id' => $usernew->id)); $err = array(); if (!$user and !empty($usernew->createpassword)) { if ($usernew->suspended) { // Show some error because we can not mail suspended users. $err['suspended'] = get_string('error'); } } else { if (!empty($usernew->newpassword)) { $errmsg = ''; // Prevent eclipse warning. if (!check_password_policy($usernew->newpassword, $errmsg, $usernew)) { $err['newpassword'] = $errmsg; } } else if (!$user) { $auth = get_auth_plugin($usernew->auth); if ($auth->is_internal()) { // Internal accounts require password! $err['newpassword'] = get_string('required'); } } } if (empty($usernew->username)) { // Might be only whitespace. $err['username'] = get_string('required'); } else if (!$user or $user->username !== $usernew->username) { // Check new username does not exist. if ($DB->record_exists('user', array('username' => $usernew->username, 'mnethostid' => $CFG->mnet_localhost_id))) { $err['username'] = get_string('usernameexists'); } // Check allowed characters. if ($usernew->username !== core_text::strtolower($usernew->username)) { $err['username'] = get_string('usernamelowercase'); } else { if ($usernew->username !== core_user::clean_field($usernew->username, 'username')) { $err['username'] = get_string('invalidusername'); } } } if (!$user or (isset($usernew->email) && $user->email !== $usernew->email)) { if (!validate_email($usernew->email)) { $err['email'] = get_string('invalidemail'); } else if (empty($CFG->allowaccountssameemail)) { // Make a case-insensitive query for the given email address. $select = $DB->sql_equal('email', ':email', false) . ' AND mnethostid = :mnethostid AND id <> :userid'; $params = array( 'email' => $usernew->email, 'mnethostid' => $CFG->mnet_localhost_id, 'userid' => $usernew->id ); // If there are other user(s) that already have the same email, show an error. if ($DB->record_exists_select('user', $select, $params)) { $err['email'] = get_string('emailexists'); } } } // Next the customisable profile fields. $err += profile_validation($usernew, $files); if (count($err) == 0) { return true; } else { return $err; } } } renderer.php 0000644 00000023267 15151162244 0007076 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Provides user rendering functionality such as printing private files tree and displaying a search utility * * @package core_user * @copyright 2010 Dongsheng Cai <dongsheng@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Provides user rendering functionality such as printing private files tree and displaying a search utility * @copyright 2010 Dongsheng Cai <dongsheng@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_user_renderer extends plugin_renderer_base { /** * Prints user search utility that can search user by first initial of firstname and/or first initial of lastname * Prints a header with a title and the number of users found within that subset * @param string $url the url to return to, complete with any parameters needed for the return * @param string $firstinitial the first initial of the firstname * @param string $lastinitial the first initial of the lastname * @param int $usercount the amount of users meeting the search criteria * @param int $totalcount the amount of users of the set/subset being searched * @param string $heading heading of the subset being searched, default is All Participants * @return string html output */ public function user_search($url, $firstinitial, $lastinitial, $usercount, $totalcount, $heading = null) { if ($firstinitial !== 'all') { set_user_preference('ifirst', $firstinitial); } if ($lastinitial !== 'all') { set_user_preference('ilast', $lastinitial); } if (!isset($heading)) { $heading = get_string('allparticipants'); } $content = html_writer::start_tag('form', array('action' => new moodle_url($url))); $content .= html_writer::start_tag('div'); // Search utility heading. $content .= $this->output->heading($heading.get_string('labelsep', 'langconfig').$usercount.'/'.$totalcount, 3); // Initials bar. $prefixfirst = 'sifirst'; $prefixlast = 'silast'; $content .= $this->output->initials_bar($firstinitial, 'firstinitial', get_string('firstname'), $prefixfirst, $url); $content .= $this->output->initials_bar($lastinitial, 'lastinitial', get_string('lastname'), $prefixlast, $url); $content .= html_writer::end_tag('div'); $content .= html_writer::tag('div', ' '); $content .= html_writer::end_tag('form'); return $content; } /** * Displays the list of tagged users * * @param array $userlist * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @return string */ public function user_list($userlist, $exclusivemode) { $tagfeed = new core_tag\output\tagfeed(); foreach ($userlist as $user) { $userpicture = $this->output->user_picture($user, array('size' => $exclusivemode ? 100 : 35)); $fullname = fullname($user); if (user_can_view_profile($user)) { $profilelink = new moodle_url('/user/view.php', array('id' => $user->id)); $fullname = html_writer::link($profilelink, $fullname); } $tagfeed->add($userpicture, $fullname); } $items = $tagfeed->export_for_template($this->output); if ($exclusivemode) { $output = '<div><ul class="inline-list">'; foreach ($items['items'] as $item) { $output .= '<li><div class="user-box">'. $item['img'] . $item['heading'] ."</div></li>\n"; } $output .= "</ul></div>\n"; return $output; } return $this->output->render_from_template('core_tag/tagfeed', $items); } /** * Renders the unified filter element for the course participants page. * @deprecated since 3.9 * @throws coding_exception */ public function unified_filter() { throw new coding_exception('unified_filter cannot be used any more, please use participants_filter instead'); } /** * Render the data required for the participants filter on the course participants page. * * @param context $context The context of the course being displayed * @param string $tableregionid Container of the table to be updated by this filter, is used to retrieve the table * @return string */ public function participants_filter(context $context, string $tableregionid): string { $renderable = new \core_user\output\participants_filter($context, $tableregionid); $templatecontext = $renderable->export_for_template($this->output); return $this->output->render_from_template('core_user/participantsfilter', $templatecontext); } /** * Returns a formatted filter option. * * @param int $filtertype The filter type (e.g. status, role, group, enrolment, last access). * @param string $criteria The string label of the filter type. * @param int $value The value for the filter option. * @param string $label The string representation of the filter option's value. * @return array The formatted option with the ['filtertype:value' => 'criteria: label'] format. */ protected function format_filter_option($filtertype, $criteria, $value, $label) { $optionlabel = get_string('filteroption', 'moodle', (object)['criteria' => $criteria, 'value' => $label]); $optionvalue = "$filtertype:$value"; return [$optionvalue => $optionlabel]; } /** * Handles cases when after reloading the applied filters are missing in the filter options. * * @param array $filtersapplied The applied filters. * @param array $filteroptions The filter options. * @return array The formatted options with the ['filtertype:value' => 'criteria: label'] format. */ private function handle_missing_applied_filters($filtersapplied, $filteroptions) { global $DB; foreach ($filtersapplied as $filter) { if (!array_key_exists($filter, $filteroptions)) { $filtervalue = explode(':', $filter); if (count($filtervalue) !== 2) { continue; } $key = $filtervalue[0]; $value = $filtervalue[1]; switch($key) { case USER_FILTER_LAST_ACCESS: $now = usergetmidnight(time()); $criteria = get_string('usersnoaccesssince'); // Days. for ($i = 1; $i < 7; $i++) { $timestamp = strtotime('-' . $i . ' days', $now); if ($timestamp < $value) { break; } $val = get_string('numdays', 'moodle', $i); $filteroptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $val); } // Weeks. for ($i = 1; $i < 10; $i++) { $timestamp = strtotime('-'.$i.' weeks', $now); if ($timestamp < $value) { break; } $val = get_string('numweeks', 'moodle', $i); $filteroptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $val); } // Months. for ($i = 2; $i < 12; $i++) { $timestamp = strtotime('-'.$i.' months', $now); if ($timestamp < $value) { break; } $val = get_string('nummonths', 'moodle', $i); $filteroptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $val); } // Try a year. $timestamp = strtotime('-1 year', $now); if ($timestamp >= $value) { $val = get_string('numyear', 'moodle', 1); $filteroptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $val); } break; case USER_FILTER_ROLE: $criteria = get_string('role'); if ($role = $DB->get_record('role', array('id' => $value))) { $role = role_get_name($role); $filteroptions += $this->format_filter_option(USER_FILTER_ROLE, $criteria, $value, $role); } break; } } } return $filteroptions; } } repository.php 0000644 00000005453 15151162244 0007504 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file is part of the User section Moodle * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once(__DIR__ . '/../config.php'); require_once($CFG->dirroot . '/repository/lib.php'); $config = optional_param('config', 0, PARAM_INT); $course = optional_param('course', SITEID, PARAM_INT); $url = new moodle_url('/user/repository.php', array('course' => $course)); if ($config !== 0) { $url->param('config', $config); } $PAGE->set_url($url); $course = $DB->get_record("course", array("id" => $course), '*', MUST_EXIST); $user = $USER; $baseurl = $CFG->wwwroot . '/user/repository.php'; $namestr = get_string('name'); $fullname = fullname($user); $strrepos = get_string('repositories', 'repository'); $configstr = get_string('manageuserrepository', 'repository'); $pluginstr = get_string('plugin', 'repository'); require_login($course, false); $coursecontext = context_course::instance($course->id, MUST_EXIST); $link = new moodle_url('/user/view.php', array('id' => $user->id)); $PAGE->navbar->add($fullname, $link); $PAGE->navbar->add($strrepos); $PAGE->set_title("$course->fullname: $fullname: $strrepos"); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); $currenttab = 'repositories'; require('tabs.php'); echo $OUTPUT->heading($configstr); echo $OUTPUT->box_start(); $params = array(); $params['context'] = $coursecontext; $params['currentcontext'] = $PAGE->context; $params['userid'] = $USER->id; if (!$instances = repository::get_instances($params)) { throw new \moodle_exception('noinstances', 'repository', $CFG->wwwroot . '/user/view.php'); } $table = new html_table(); $table->head = array($namestr, $pluginstr, ''); $table->data = array(); foreach ($instances as $i) { $path = '/repository/'.$i->type.'/settings.php'; $settings = file_exists($CFG->dirroot.$path); $table->data[] = array($i->name, $i->type, $settings ? '<a href="'.$CFG->wwwroot.$path.'">' .get_string('settings', 'repository').'</a>' : ''); } echo html_writer::table($table); echo $OUTPUT->footer(); amd/build/repository.min.js.map 0000644 00000004654 15151162244 0012531 0 ustar 00 {"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module to handle AJAX interactions.\n *\n * @module core_user/repository\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {call as fetchMany} from 'core/ajax';\n\n/**\n * Unenrol the user with the specified user enrolmentid ID.\n *\n * @param {Number} userEnrolmentId\n * @return {Promise}\n */\nexport const unenrolUser = userEnrolmentId => {\n return fetchMany([{\n methodname: 'core_enrol_unenrol_user_enrolment',\n args: {\n ueid: userEnrolmentId,\n },\n }])[0];\n};\n\n/**\n * Submit the user enrolment form with the specified form data.\n *\n * @param {String} formdata\n * @return {Promise}\n */\nexport const submitUserEnrolmentForm = formdata => {\n return fetchMany([{\n methodname: 'core_enrol_submit_user_enrolment_form',\n args: {\n formdata,\n },\n }])[0];\n};\n\nexport const createNotesForUsers = notes => {\n return fetchMany([{\n methodname: 'core_notes_create_notes',\n args: {\n notes\n }\n }])[0];\n};\n\nexport const sendMessagesToUsers = messages => {\n return fetchMany([{\n methodname: 'core_message_send_instant_messages',\n args: {messages}\n }])[0];\n};\n"],"names":["userEnrolmentId","methodname","args","ueid","formdata","notes","messages"],"mappings":"oRA8B2BA,kBAChB,cAAU,CAAC,CACdC,WAAY,oCACZC,KAAM,CACFC,KAAMH,oBAEV,oCAS+BI,WAC5B,cAAU,CAAC,CACdH,WAAY,wCACZC,KAAM,CACFE,SAAAA,aAEJ,gCAG2BC,QACxB,cAAU,CAAC,CACdJ,WAAY,0BACZC,KAAM,CACFG,MAAAA,UAEJ,gCAG2BC,WACxB,cAAU,CAAC,CACdL,WAAY,qCACZC,KAAM,CAACI,SAAAA,aACP"} amd/build/private_files.min.js.map 0000644 00000006464 15151162244 0013147 0 ustar 00 {"version":3,"file":"private_files.min.js","sources":["../src/private_files.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module to handle AJAX interactions with user private files\n *\n * @module core_user/private_files\n * @copyright 2020 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport DynamicForm from 'core_form/dynamicform';\nimport ModalForm from 'core_form/modalform';\nimport {get_string as getString} from 'core/str';\nimport {add as addToast} from 'core/toast';\n\n/**\n * Initialize private files form as AJAX form\n *\n * @param {String} containerSelector\n * @param {String} formClass\n */\nexport const initDynamicForm = (containerSelector, formClass) => {\n const form = new DynamicForm(document.querySelector(containerSelector), formClass);\n\n // When form is saved, refresh it to remove validation errors, if any:\n form.addEventListener(form.events.FORM_SUBMITTED, () => {\n form.load();\n getString('changessaved')\n .then(addToast)\n .catch(null);\n });\n\n // Reload the page on cancel.\n form.addEventListener(form.events.CANCEL_BUTTON_PRESSED, () => window.location.reload());\n};\n\n/**\n * Initialize private files form as Modal form\n *\n * @param {String} elementSelector\n * @param {String} formClass\n */\nexport const initModal = (elementSelector, formClass) => {\n document.querySelector(elementSelector).addEventListener('click', function(e) {\n e.preventDefault();\n const form = new ModalForm({\n formClass,\n args: {nosubmit: true},\n modalConfig: {title: getString('privatefilesmanage')},\n returnFocus: e.target,\n });\n form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());\n form.show();\n });\n};\n"],"names":["containerSelector","formClass","form","DynamicForm","document","querySelector","addEventListener","events","FORM_SUBMITTED","load","then","addToast","catch","CANCEL_BUTTON_PRESSED","window","location","reload","elementSelector","e","preventDefault","ModalForm","args","nosubmit","modalConfig","title","returnFocus","target","show"],"mappings":";;;;;;;yOAiC+B,CAACA,kBAAmBC,mBACzCC,KAAO,IAAIC,qBAAYC,SAASC,cAAcL,mBAAoBC,WAGxEC,KAAKI,iBAAiBJ,KAAKK,OAAOC,gBAAgB,KAC9CN,KAAKO,2BACK,gBACTC,KAAKC,YACLC,MAAM,SAIXV,KAAKI,iBAAiBJ,KAAKK,OAAOM,uBAAuB,IAAMC,OAAOC,SAASC,+BAS1D,CAACC,gBAAiBhB,aACvCG,SAASC,cAAcY,iBAAiBX,iBAAiB,SAAS,SAASY,GACvEA,EAAEC,uBACIjB,KAAO,IAAIkB,mBAAU,CACvBnB,UAAAA,UACAoB,KAAM,CAACC,UAAU,GACjBC,YAAa,CAACC,OAAO,mBAAU,uBAC/BC,YAAaP,EAAEQ,SAEnBxB,KAAKI,iBAAiBJ,KAAKK,OAAOC,gBAAgB,IAAMM,OAAOC,SAASC,WACxEd,KAAKyB"} amd/build/participants.min.js.map 0000644 00000025743 15151162244 0013015 0 ustar 00 {"version":3,"file":"participants.min.js","sources":["../src/participants.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Some UI stuff for participants page.\n * This is also used by the report/participants/index.php because it has the same functionality.\n *\n * @module core_user/participants\n * @copyright 2017 Damyon Wiese\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as DynamicTable from 'core_table/dynamic';\nimport * as Str from 'core/str';\nimport CheckboxToggleAll from 'core/checkbox-toggleall';\nimport CustomEvents from 'core/custom_interaction_events';\nimport DynamicTableSelectors from 'core_table/local/dynamic/selectors';\nimport ModalEvents from 'core/modal_events';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport jQuery from 'jquery';\nimport {showAddNote, showSendMessage} from 'core_user/local/participants/bulkactions';\nimport 'core/inplace_editable';\n\nconst Selectors = {\n bulkActionSelect: \"#formactionid\",\n bulkUserSelectedCheckBoxes: \"input[data-togglegroup='participants-table'][data-toggle='slave']:checked\",\n checkCountButton: \"#checkall\",\n showCountText: '[data-region=\"participant-count\"]',\n showCountToggle: '[data-action=\"showcount\"]',\n stateHelpIcon: '[data-region=\"state-help-icon\"]',\n tableForm: uniqueId => `form[data-table-unique-id=\"${uniqueId}\"]`,\n};\n\nexport const init = ({\n uniqueid,\n noteStateNames = {},\n}) => {\n const root = document.querySelector(Selectors.tableForm(uniqueid));\n const getTableFromUniqueId = uniqueId => root.querySelector(DynamicTableSelectors.main.fromRegionId(uniqueId));\n\n /**\n * Private method.\n *\n * @method registerEventListeners\n * @private\n */\n const registerEventListeners = () => {\n CustomEvents.define(Selectors.bulkActionSelect, [CustomEvents.events.accessibleChange]);\n jQuery(Selectors.bulkActionSelect).on(CustomEvents.events.accessibleChange, e => {\n const bulkActionSelect = e.target.closest('select');\n const action = bulkActionSelect.value;\n const tableRoot = getTableFromUniqueId(uniqueid);\n const checkboxes = tableRoot.querySelectorAll(Selectors.bulkUserSelectedCheckBoxes);\n const pendingPromise = new Pending('core_user/participants:bulkActionSelect');\n\n if (action.indexOf('#') !== -1) {\n e.preventDefault();\n\n const ids = [];\n checkboxes.forEach(checkbox => {\n ids.push(checkbox.getAttribute('name').replace('user', ''));\n });\n\n let bulkAction;\n if (action === '#messageselect') {\n bulkAction = showSendMessage(ids);\n } else if (action === '#addgroupnote') {\n bulkAction = showAddNote(\n root.dataset.courseId,\n ids,\n noteStateNames,\n root.querySelector(Selectors.stateHelpIcon)\n );\n }\n\n if (bulkAction) {\n const pendingBulkAction = new Pending('core_user/participants:bulkActionSelected');\n bulkAction\n .then(modal => {\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Focus on the action select when the dialog is closed.\n bulkActionSelect.focus();\n });\n\n pendingBulkAction.resolve();\n return modal;\n })\n .catch(Notification.exception);\n }\n } else if (action !== '' && checkboxes.length) {\n bulkActionSelect.form.submit();\n }\n\n resetBulkAction(bulkActionSelect);\n pendingPromise.resolve();\n });\n\n root.addEventListener('click', e => {\n // Handle clicking of the \"Select all\" actions.\n const checkCountButton = root.querySelector(Selectors.checkCountButton);\n const checkCountButtonClicked = checkCountButton && checkCountButton.contains(e.target);\n\n if (checkCountButtonClicked) {\n e.preventDefault();\n\n const tableRoot = getTableFromUniqueId(uniqueid);\n\n DynamicTable.setPageSize(tableRoot, checkCountButton.dataset.targetPageSize)\n .then(tableRoot => {\n // Update the toggle state.\n CheckboxToggleAll.setGroupState(root, 'participants-table', true);\n\n return tableRoot;\n })\n .catch(Notification.exception);\n }\n });\n\n // When the content is refreshed, update the row counts in various places.\n root.addEventListener(DynamicTable.Events.tableContentRefreshed, e => {\n const checkCountButton = root.querySelector(Selectors.checkCountButton);\n\n const tableRoot = e.target;\n\n const defaultPageSize = parseInt(tableRoot.dataset.tableDefaultPerPage, 10);\n const currentPageSize = parseInt(tableRoot.dataset.tablePageSize, 10);\n const totalRowCount = parseInt(tableRoot.dataset.tableTotalRows, 10);\n\n CheckboxToggleAll.updateSlavesFromMasterState(root, 'participants-table');\n\n const pageCountStrings = [\n {\n key: 'countparticipantsfound',\n component: 'core_user',\n param: totalRowCount,\n },\n ];\n\n if (totalRowCount <= defaultPageSize) {\n if (checkCountButton) {\n checkCountButton.classList.add('hidden');\n }\n } else if (totalRowCount <= currentPageSize) {\n // The are fewer than the current page size.\n pageCountStrings.push({\n key: 'selectalluserswithcount',\n component: 'core',\n param: defaultPageSize,\n });\n\n if (checkCountButton) {\n // The 'Check all [x]' button is only visible when there are values to set.\n checkCountButton.classList.add('hidden');\n }\n } else {\n pageCountStrings.push({\n key: 'selectalluserswithcount',\n component: 'core',\n param: totalRowCount,\n });\n\n if (checkCountButton) {\n checkCountButton.classList.remove('hidden');\n }\n }\n\n Str.get_strings(pageCountStrings)\n .then(([showingParticipantCountString, selectCountString]) => {\n const showingParticipantCount = root.querySelector(Selectors.showCountText);\n showingParticipantCount.innerHTML = showingParticipantCountString;\n\n if (selectCountString && checkCountButton) {\n checkCountButton.value = selectCountString;\n }\n\n return;\n })\n .catch(Notification.exception);\n });\n };\n\n const resetBulkAction = bulkActionSelect => {\n bulkActionSelect.value = '';\n };\n\n registerEventListeners();\n};\n"],"names":["Selectors","uniqueId","_ref","uniqueid","noteStateNames","root","document","querySelector","getTableFromUniqueId","DynamicTableSelectors","main","fromRegionId","resetBulkAction","bulkActionSelect","value","define","CustomEvents","events","accessibleChange","on","e","target","closest","action","checkboxes","querySelectorAll","pendingPromise","Pending","indexOf","preventDefault","ids","bulkAction","forEach","checkbox","push","getAttribute","replace","dataset","courseId","pendingBulkAction","then","modal","getRoot","ModalEvents","hidden","focus","resolve","catch","Notification","exception","length","form","submit","addEventListener","checkCountButton","contains","tableRoot","DynamicTable","setPageSize","targetPageSize","setGroupState","Events","tableContentRefreshed","defaultPageSize","parseInt","tableDefaultPerPage","currentPageSize","tablePageSize","totalRowCount","tableTotalRows","updateSlavesFromMasterState","pageCountStrings","key","component","param","classList","add","remove","Str","get_strings","_ref2","showingParticipantCountString","selectCountString","innerHTML"],"mappings":";;;;;;;;giBAoCMA,2BACgB,gBADhBA,qCAE0B,4EAF1BA,2BAGgB,YAHhBA,wBAIa,oCAJbA,wBAMa,kCANbA,oBAOSC,+CAA0CA,6BAGrCC,WAACC,SACjBA,SADiBC,eAEjBA,eAAiB,eAEXC,KAAOC,SAASC,cAAcP,oBAAoBG,WAClDK,qBAAuBP,UAAYI,KAAKE,cAAcE,mBAAsBC,KAAKC,aAAaV,WA+I9FW,gBAAkBC,mBACpBA,iBAAiBC,MAAQ,uCAvIZC,OAAOf,2BAA4B,CAACgB,mCAAaC,OAAOC,uCAC9DlB,4BAA4BmB,GAAGH,mCAAaC,OAAOC,kBAAkBE,UAClEP,iBAAmBO,EAAEC,OAAOC,QAAQ,UACpCC,OAASV,iBAAiBC,MAE1BU,WADYhB,qBAAqBL,UACVsB,iBAAiBzB,sCACxC0B,eAAiB,IAAIC,iBAAQ,+CAEN,IAAzBJ,OAAOK,QAAQ,KAAa,CAC5BR,EAAES,uBAEIC,IAAM,OAKRC,cAJJP,WAAWQ,SAAQC,WACfH,IAAII,KAAKD,SAASE,aAAa,QAAQC,QAAQ,OAAQ,QAI5C,mBAAXb,OACAQ,YAAa,gCAAgBD,KACX,kBAAXP,SACPQ,YAAa,4BACT1B,KAAKgC,QAAQC,SACbR,IACA1B,eACAC,KAAKE,cAAcP,2BAIvB+B,WAAY,OACNQ,kBAAoB,IAAIZ,iBAAQ,6CACtCI,WACCS,MAAKC,QACFA,MAAMC,UAAUvB,GAAGwB,sBAAYC,QAAQ,KAEnC/B,iBAAiBgC,WAGrBN,kBAAkBO,UACXL,SAEVM,MAAMC,sBAAaC,gBAEN,KAAX1B,QAAiBC,WAAW0B,QACnCrC,iBAAiBsC,KAAKC,SAG1BxC,gBAAgBC,kBAChBa,eAAeoB,aAGnBzC,KAAKgD,iBAAiB,SAASjC,UAErBkC,iBAAmBjD,KAAKE,cAAcP,+BACZsD,kBAAoBA,iBAAiBC,SAASnC,EAAEC,QAEnD,CACzBD,EAAES,uBAEI2B,UAAYhD,qBAAqBL,UAEvCsD,aAAaC,YAAYF,UAAWF,iBAAiBjB,QAAQsB,gBAC5DnB,MAAKgB,uCAEgBI,cAAcvD,KAAM,sBAAsB,GAErDmD,aAEVT,MAAMC,sBAAaC,eAK5B5C,KAAKgD,iBAAiBI,aAAaI,OAAOC,uBAAuB1C,UACvDkC,iBAAmBjD,KAAKE,cAAcP,4BAEtCwD,UAAYpC,EAAEC,OAEd0C,gBAAkBC,SAASR,UAAUnB,QAAQ4B,oBAAqB,IAClEC,gBAAkBF,SAASR,UAAUnB,QAAQ8B,cAAe,IAC5DC,cAAgBJ,SAASR,UAAUnB,QAAQgC,eAAgB,+BAE/CC,4BAA4BjE,KAAM,4BAE9CkE,iBAAmB,CACrB,CACIC,IAAK,yBACLC,UAAW,YACXC,MAAON,gBAIXA,eAAiBL,gBACbT,kBACAA,iBAAiBqB,UAAUC,IAAI,UAE5BR,eAAiBF,iBAExBK,iBAAiBrC,KAAK,CAClBsC,IAAK,0BACLC,UAAW,OACXC,MAAOX,kBAGPT,kBAEAA,iBAAiBqB,UAAUC,IAAI,YAGnCL,iBAAiBrC,KAAK,CAClBsC,IAAK,0BACLC,UAAW,OACXC,MAAON,gBAGPd,kBACAA,iBAAiBqB,UAAUE,OAAO,WAI1CC,IAAIC,YAAYR,kBACf/B,MAAKwC,YAAEC,8BAA+BC,yBACH7E,KAAKE,cAAcP,yBAC3BmF,UAAYF,8BAEhCC,mBAAqB5B,mBACrBA,iBAAiBxC,MAAQoE,sBAKhCnC,MAAMC,sBAAaC"} amd/build/status_field.min.js 0000644 00000015460 15151162244 0012221 0 ustar 00 define("core_user/status_field",["exports","core_table/dynamic","./repository","core/str","core_table/local/dynamic/selectors","core/fragment","core/modal_events","core/modal_factory","core/notification","core/templates","core/toast"],(function(_exports,DynamicTable,Repository,Str,_selectors,_fragment,_modal_events,_modal_factory,_notification,_templates,_toast){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * AMD module for the user enrolment status field in the course participants page. * * @module core_user/status_field * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,DynamicTable=_interopRequireWildcard(DynamicTable),Repository=_interopRequireWildcard(Repository),Str=_interopRequireWildcard(Str),_selectors=_interopRequireDefault(_selectors),_fragment=_interopRequireDefault(_fragment),_modal_events=_interopRequireDefault(_modal_events),_modal_factory=_interopRequireDefault(_modal_factory),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates);const Selectors_editEnrolment='[data-action="editenrolment"]',Selectors_showDetails='[data-action="showdetails"]',Selectors_unenrol='[data-action="unenrol"]',Selectors_statusElement="[data-status]",getDynamicTableFromLink=link=>link.closest(_selectors.default.main.region),getStatusContainer=link=>link.closest(Selectors_statusElement),getUserEnrolmentIdFromLink=link=>link.getAttribute("rel"),showEditDialogue=(link,getBody)=>{const container=getStatusContainer(link),userEnrolmentId=getUserEnrolmentIdFromLink(link);_modal_factory.default.create({large:!0,title:Str.get_string("edituserenrolment","enrol",container.dataset.fullname),type:_modal_factory.default.types.SAVE_CANCEL,body:getBody(userEnrolmentId)}).then((modal=>(modal.getRoot().on(_modal_events.default.save,(e=>{e.preventDefault(),submitEditFormAjax(link,getBody,modal,userEnrolmentId,container.dataset)})),modal.getRoot().on(_modal_events.default.hidden,(()=>{modal.destroy()})),modal.show(),modal))).catch(_notification.default.exception)},showUnenrolConfirmation=link=>{const container=getStatusContainer(link),userEnrolmentId=getUserEnrolmentIdFromLink(link);_modal_factory.default.create({type:_modal_factory.default.types.SAVE_CANCEL}).then((modal=>{modal.getRoot().on(_modal_events.default.save,(e=>{e.preventDefault(),submitUnenrolFormAjax(link,modal,{ueid:userEnrolmentId},container.dataset)})),modal.getRoot().on(_modal_events.default.hidden,(()=>{modal.destroy()})),modal.show();const stringData=[{key:"unenrol",component:"enrol"},{key:"unenrolconfirm",component:"enrol",param:{user:container.dataset.fullname,course:container.dataset.coursename,enrolinstancename:container.dataset.enrolinstancename}}];return Promise.all([Str.get_strings(stringData),modal])})).then((_ref=>{let[strings,modal]=_ref;return modal.setTitle(strings[0]),modal.setSaveButtonText(strings[0]),modal.setBody(strings[1]),modal})).catch(_notification.default.exception)},showStatusDetails=link=>{const container=getStatusContainer(link),context={editenrollink:"",statusclass:container.querySelector("span.badge").getAttribute("class"),...container.dataset},editEnrolLink=container.querySelector(Selectors_editEnrolment);editEnrolLink&&(context.editenrollink=editEnrolLink.outerHTML),_modal_factory.default.create({large:!0,type:_modal_factory.default.types.CANCEL,title:Str.get_string("enroldetails","enrol"),body:_templates.default.render("core_user/status_details",context)}).then((modal=>(editEnrolLink&&modal.getRoot().on("click",Selectors_editEnrolment,(e=>{e.preventDefault(),modal.hide(),editEnrolLink.click()})),modal.show(),modal.getRoot().on(_modal_events.default.hidden,(()=>modal.destroy())),modal))).catch(_notification.default.exception)},submitEditFormAjax=(clickedLink,getBody,modal,userEnrolmentId,userData)=>{const form=modal.getRoot().find("form");Repository.submitUserEnrolmentForm(form.serialize()).then((data=>{if(!data.result)throw data.result;return modal.hide(),modal.destroy(),data})).then((()=>(DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink)).catch(_notification.default.exception),Str.get_string("enrolmentupdatedforuser","core_enrol",userData)))).then((notificationString=>{(0,_toast.add)(notificationString)})).catch((()=>(modal.setBody(getBody(userEnrolmentId,JSON.stringify(form.serialize()))),modal)))},submitUnenrolFormAjax=(clickedLink,modal,args,userData)=>{Repository.unenrolUser(args.ueid).then((data=>data.result?(modal.hide(),modal.destroy(),data):(_notification.default.alert(data.errors[0].key,data.errors[0].message),data))).then((()=>(DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink)).catch(_notification.default.exception),Str.get_string("unenrolleduser","core_enrol",userData)))).then((notificationString=>{(0,_toast.add)(notificationString)})).catch(_notification.default.exception)},getBody=function(contextId,ueid){let formdata=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;return _fragment.default.loadFragment("enrol","user_enrolment_form",contextId,{ueid:ueid,formdata:formdata})};_exports.init=_ref2=>{let{contextid:contextid,uniqueid:uniqueid}=_ref2;((contextId,uniqueId)=>{const getBodyFunction=(userEnrolmentId,formData)=>getBody(contextId,userEnrolmentId,formData);document.addEventListener("click",(e=>{if(!e.target.closest(_selectors.default.main.fromRegionId(uniqueId)))return;const editLink=e.target.closest(Selectors_editEnrolment);editLink&&(e.preventDefault(),showEditDialogue(editLink,getBodyFunction));const unenrolLink=e.target.closest(Selectors_unenrol);unenrolLink&&(e.preventDefault(),showUnenrolConfirmation(unenrolLink));const showDetailsLink=e.target.closest(Selectors_showDetails);showDetailsLink&&(e.preventDefault(),showStatusDetails(showDetailsLink))}))})(contextid,uniqueid)}})); //# sourceMappingURL=status_field.min.js.map amd/build/private_files.min.js 0000644 00000002744 15151162244 0012370 0 ustar 00 define("core_user/private_files",["exports","core_form/dynamicform","core_form/modalform","core/str","core/toast"],(function(_exports,_dynamicform,_modalform,_str,_toast){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Module to handle AJAX interactions with user private files * * @module core_user/private_files * @copyright 2020 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initModal=_exports.initDynamicForm=void 0,_dynamicform=_interopRequireDefault(_dynamicform),_modalform=_interopRequireDefault(_modalform);_exports.initDynamicForm=(containerSelector,formClass)=>{const form=new _dynamicform.default(document.querySelector(containerSelector),formClass);form.addEventListener(form.events.FORM_SUBMITTED,(()=>{form.load(),(0,_str.get_string)("changessaved").then(_toast.add).catch(null)})),form.addEventListener(form.events.CANCEL_BUTTON_PRESSED,(()=>window.location.reload()))};_exports.initModal=(elementSelector,formClass)=>{document.querySelector(elementSelector).addEventListener("click",(function(e){e.preventDefault();const form=new _modalform.default({formClass:formClass,args:{nosubmit:!0},modalConfig:{title:(0,_str.get_string)("privatefilesmanage")},returnFocus:e.target});form.addEventListener(form.events.FORM_SUBMITTED,(()=>window.location.reload())),form.show()}))}})); //# sourceMappingURL=private_files.min.js.map amd/build/edit_profile_fields.min.js 0000644 00000004254 15151162244 0013525 0 ustar 00 define("core_user/edit_profile_fields",["exports","core_form/modalform","core/str"],(function(_exports,_modalform,_str){var obj; /** * User profile fields editor * * @module core_user/edit_profile_fields * @copyright 2021 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=(obj=_modalform)&&obj.__esModule?obj:{default:obj};const Selectors_actions={editCategory:'[data-action="editcategory"]',editField:'[data-action="editfield"]',createField:'[data-action="createfield"]'};_exports.init=()=>{document.addEventListener("click",(function(e){let element=e.target.closest(Selectors_actions.editCategory);if(element){e.preventDefault();const title=element.getAttribute("data-id")?(0,_str.get_string)("profileeditcategory","admin",element.getAttribute("data-name")):(0,_str.get_string)("profilecreatenewcategory","admin"),form=new _modalform.default({formClass:"core_user\\form\\profile_category_form",args:{id:element.getAttribute("data-id")},modalConfig:{title:title},returnFocus:element});form.addEventListener(form.events.FORM_SUBMITTED,(()=>window.location.reload())),form.show()}if(element=e.target.closest(Selectors_actions.editField),element){e.preventDefault();const form=new _modalform.default({formClass:"core_user\\form\\profile_field_form",args:{id:element.getAttribute("data-id")},modalConfig:{title:(0,_str.get_string)("profileeditfield","admin",element.getAttribute("data-name"))},returnFocus:element});form.addEventListener(form.events.FORM_SUBMITTED,(()=>window.location.reload())),form.show()}if(element=e.target.closest(Selectors_actions.createField),element){e.preventDefault();const form=new _modalform.default({formClass:"core_user\\form\\profile_field_form",args:{datatype:element.getAttribute("data-datatype"),categoryid:element.getAttribute("data-categoryid")},modalConfig:{title:(0,_str.get_string)("profilecreatenewfield","admin",element.getAttribute("data-datatypename"))},returnFocus:element});form.addEventListener(form.events.FORM_SUBMITTED,(()=>window.location.reload())),form.show()}}))}})); //# sourceMappingURL=edit_profile_fields.min.js.map amd/build/participants_filter.min.js 0000644 00000006415 15151162244 0013601 0 ustar 00 define("core_user/participants_filter",["exports","core/datafilter","core_table/dynamic","core/datafilter/selectors","core/notification","core/pending"],(function(_exports,_datafilter,DynamicTable,_selectors,_notification,_pending){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Participants filter management. * * @module core_user/participants_filter * @copyright 2021 Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_datafilter=_interopRequireDefault(_datafilter),DynamicTable=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(DynamicTable),_selectors=_interopRequireDefault(_selectors),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);_exports.init=filterRegionId=>{const filterSet=document.getElementById(filterRegionId),coreFilter=new _datafilter.default(filterSet,(function(filters,pendingPromise){DynamicTable.setFilters(DynamicTable.getTableFromId(filterSet.dataset.tableRegion),{jointype:parseInt(filterSet.querySelector(_selectors.default.filterset.fields.join).value,10),filters:filters}).then((result=>(pendingPromise.resolve(),result))).catch(_notification.default.exception)}));coreFilter.init();const tableRoot=DynamicTable.getTableFromId(filterSet.dataset.tableRegion),initialFilters=DynamicTable.getFilters(tableRoot);if(initialFilters){const initialFilterPromise=new _pending.default("core/filter:setFilterFromConfig");(config=>{const filterConfig=Object.entries(config.filters);if(!filterConfig.length)return Promise.resolve();filterSet.querySelector(_selectors.default.filterset.fields.join).value=config.jointype;const filterPromises=filterConfig.map((_ref=>{let[filterType,filterData]=_ref;if("courseid"===filterType)return!1;const filterValues=filterData.values;return!!filterValues.length&&coreFilter.addFilterRow().then((_ref2=>{let[filterRow]=_ref2;coreFilter.addFilter(filterRow,filterType,filterValues)}))})).filter((promise=>promise));return filterPromises.length?Promise.all(filterPromises).then((()=>coreFilter.removeEmptyFilters())).then((()=>{coreFilter.updateFiltersOptions()})).then((()=>{coreFilter.updateTableFromFilter()})):Promise.resolve()})(initialFilters).then((()=>initialFilterPromise.resolve())).catch()}}})); //# sourceMappingURL=participants_filter.min.js.map amd/build/repository.min.js 0000644 00000001476 15151162244 0011754 0 ustar 00 define("core_user/repository",["exports","core/ajax"],(function(_exports,_ajax){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unenrolUser=_exports.submitUserEnrolmentForm=_exports.sendMessagesToUsers=_exports.createNotesForUsers=void 0;_exports.unenrolUser=userEnrolmentId=>(0,_ajax.call)([{methodname:"core_enrol_unenrol_user_enrolment",args:{ueid:userEnrolmentId}}])[0];_exports.submitUserEnrolmentForm=formdata=>(0,_ajax.call)([{methodname:"core_enrol_submit_user_enrolment_form",args:{formdata:formdata}}])[0];_exports.createNotesForUsers=notes=>(0,_ajax.call)([{methodname:"core_notes_create_notes",args:{notes:notes}}])[0];_exports.sendMessagesToUsers=messages=>(0,_ajax.call)([{methodname:"core_message_send_instant_messages",args:{messages:messages}}])[0]})); //# sourceMappingURL=repository.min.js.map amd/build/edit_profile_fields.min.js.map 0000644 00000011265 15151162244 0014301 0 ustar 00 {"version":3,"file":"edit_profile_fields.min.js","sources":["../src/edit_profile_fields.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\nimport ModalForm from 'core_form/modalform';\nimport {get_string as getString} from 'core/str';\n\n/**\n * User profile fields editor\n *\n * @module core_user/edit_profile_fields\n * @copyright 2021 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst Selectors = {\n actions: {\n editCategory: '[data-action=\"editcategory\"]',\n editField: '[data-action=\"editfield\"]',\n createField: '[data-action=\"createfield\"]',\n },\n};\n\nexport const init = () => {\n document.addEventListener('click', function(e) {\n let element = e.target.closest(Selectors.actions.editCategory);\n if (element) {\n e.preventDefault();\n const title = element.getAttribute('data-id') ?\n getString('profileeditcategory', 'admin', element.getAttribute('data-name')) :\n getString('profilecreatenewcategory', 'admin');\n const form = new ModalForm({\n formClass: 'core_user\\\\form\\\\profile_category_form',\n args: {id: element.getAttribute('data-id')},\n modalConfig: {title},\n returnFocus: element,\n });\n form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());\n form.show();\n }\n\n element = e.target.closest(Selectors.actions.editField);\n if (element) {\n e.preventDefault();\n const form = new ModalForm({\n formClass: 'core_user\\\\form\\\\profile_field_form',\n args: {id: element.getAttribute('data-id')},\n modalConfig: {title: getString('profileeditfield', 'admin', element.getAttribute('data-name'))},\n returnFocus: element,\n });\n form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());\n form.show();\n }\n\n element = e.target.closest(Selectors.actions.createField);\n if (element) {\n e.preventDefault();\n const form = new ModalForm({\n formClass: 'core_user\\\\form\\\\profile_field_form',\n args: {datatype: element.getAttribute('data-datatype'), categoryid: element.getAttribute('data-categoryid')},\n modalConfig: {title: getString('profilecreatenewfield', 'admin', element.getAttribute('data-datatypename'))},\n returnFocus: element,\n });\n form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());\n form.show();\n }\n });\n};\n"],"names":["Selectors","editCategory","editField","createField","document","addEventListener","e","element","target","closest","preventDefault","title","getAttribute","form","ModalForm","formClass","args","id","modalConfig","returnFocus","events","FORM_SUBMITTED","window","location","reload","show","datatype","categoryid"],"mappings":";;;;;;;sJA0BMA,kBACO,CACLC,aAAc,+BACdC,UAAW,4BACXC,YAAa,6CAID,KAChBC,SAASC,iBAAiB,SAAS,SAASC,OACpCC,QAAUD,EAAEE,OAAOC,QAAQT,kBAAkBC,iBAC7CM,QAAS,CACTD,EAAEI,uBACIC,MAAQJ,QAAQK,aAAa,YAC/B,mBAAU,sBAAuB,QAASL,QAAQK,aAAa,eAC/D,mBAAU,2BAA4B,SACpCC,KAAO,IAAIC,mBAAU,CACvBC,UAAW,yCACXC,KAAM,CAACC,GAAIV,QAAQK,aAAa,YAChCM,YAAa,CAACP,MAAAA,OACdQ,YAAaZ,UAEjBM,KAAKR,iBAAiBQ,KAAKO,OAAOC,gBAAgB,IAAMC,OAAOC,SAASC,WACxEX,KAAKY,UAGTlB,QAAUD,EAAEE,OAAOC,QAAQT,kBAAkBE,WACzCK,QAAS,CACTD,EAAEI,uBACIG,KAAO,IAAIC,mBAAU,CACvBC,UAAW,sCACXC,KAAM,CAACC,GAAIV,QAAQK,aAAa,YAChCM,YAAa,CAACP,OAAO,mBAAU,mBAAoB,QAASJ,QAAQK,aAAa,eACjFO,YAAaZ,UAEjBM,KAAKR,iBAAiBQ,KAAKO,OAAOC,gBAAgB,IAAMC,OAAOC,SAASC,WACxEX,KAAKY,UAGTlB,QAAUD,EAAEE,OAAOC,QAAQT,kBAAkBG,aACzCI,QAAS,CACTD,EAAEI,uBACIG,KAAO,IAAIC,mBAAU,CACvBC,UAAW,sCACXC,KAAM,CAACU,SAAUnB,QAAQK,aAAa,iBAAkBe,WAAYpB,QAAQK,aAAa,oBACzFM,YAAa,CAACP,OAAO,mBAAU,wBAAyB,QAASJ,QAAQK,aAAa,uBACtFO,YAAaZ,UAEjBM,KAAKR,iBAAiBQ,KAAKO,OAAOC,gBAAgB,IAAMC,OAAOC,SAASC,WACxEX,KAAKY"} amd/build/participants_filter.min.js.map 0000644 00000013664 15151162244 0014361 0 ustar 00 {"version":3,"file":"participants_filter.min.js","sources":["../src/participants_filter.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Participants filter management.\n *\n * @module core_user/participants_filter\n * @copyright 2021 Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CoreFilter from 'core/datafilter';\nimport * as DynamicTable from 'core_table/dynamic';\nimport Selectors from 'core/datafilter/selectors';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\n/**\n * Initialise the participants filter on the element with the given id.\n *\n * @param {String} filterRegionId The id for the filter element.\n */\nexport const init = filterRegionId => {\n\n const filterSet = document.getElementById(filterRegionId);\n\n // Create and initialize filter.\n const coreFilter = new CoreFilter(filterSet, function(filters, pendingPromise) {\n DynamicTable.setFilters(\n DynamicTable.getTableFromId(filterSet.dataset.tableRegion),\n {\n jointype: parseInt(filterSet.querySelector(Selectors.filterset.fields.join).value, 10),\n filters,\n }\n )\n .then(result => {\n pendingPromise.resolve();\n\n return result;\n })\n .catch(Notification.exception);\n });\n coreFilter.init();\n\n /**\n * Set the current filter options based on a provided configuration.\n *\n * @param {Object} config\n * @param {Number} config.jointype\n * @param {Object} config.filters\n * @returns {Promise}\n */\n const setFilterFromConfig = config => {\n const filterConfig = Object.entries(config.filters);\n\n if (!filterConfig.length) {\n // There are no filters to set from.\n return Promise.resolve();\n }\n\n // Set the main join type.\n filterSet.querySelector(Selectors.filterset.fields.join).value = config.jointype;\n\n const filterPromises = filterConfig.map(([filterType, filterData]) => {\n if (filterType === 'courseid') {\n // The courseid is a special case.\n return false;\n }\n\n const filterValues = filterData.values;\n\n if (!filterValues.length) {\n // There are no values for this filter.\n // Skip it.\n return false;\n }\n return coreFilter.addFilterRow()\n .then(([filterRow]) => {\n coreFilter.addFilter(filterRow, filterType, filterValues);\n return;\n });\n }).filter(promise => promise);\n\n if (!filterPromises.length) {\n return Promise.resolve();\n }\n\n return Promise.all(filterPromises)\n .then(() => {\n return coreFilter.removeEmptyFilters();\n })\n .then(() => {\n coreFilter.updateFiltersOptions();\n return;\n })\n .then(() => {\n coreFilter.updateTableFromFilter();\n return;\n });\n };\n\n // Initialize DynamicTable for showing result.\n const tableRoot = DynamicTable.getTableFromId(filterSet.dataset.tableRegion);\n const initialFilters = DynamicTable.getFilters(tableRoot);\n if (initialFilters) {\n const initialFilterPromise = new Pending('core/filter:setFilterFromConfig');\n // Apply the initial filter configuration.\n setFilterFromConfig(initialFilters)\n .then(() => initialFilterPromise.resolve())\n .catch();\n }\n};\n\n"],"names":["filterRegionId","filterSet","document","getElementById","coreFilter","CoreFilter","filters","pendingPromise","DynamicTable","setFilters","getTableFromId","dataset","tableRegion","jointype","parseInt","querySelector","Selectors","filterset","fields","join","value","then","result","resolve","catch","Notification","exception","init","tableRoot","initialFilters","getFilters","initialFilterPromise","Pending","config","filterConfig","Object","entries","length","Promise","filterPromises","map","_ref","filterType","filterData","filterValues","values","addFilterRow","_ref2","filterRow","addFilter","filter","promise","all","removeEmptyFilters","updateFiltersOptions","updateTableFromFilter","setFilterFromConfig"],"mappings":";;;;;;;o8BAkCoBA,uBAEVC,UAAYC,SAASC,eAAeH,gBAGpCI,WAAa,IAAIC,oBAAWJ,WAAW,SAASK,QAASC,gBAC3DC,aAAaC,WACTD,aAAaE,eAAeT,UAAUU,QAAQC,aAC9C,CACIC,SAAUC,SAASb,UAAUc,cAAcC,mBAAUC,UAAUC,OAAOC,MAAMC,MAAO,IACnFd,QAAAA,UAGHe,MAAKC,SACFf,eAAegB,UAERD,UAEVE,MAAMC,sBAAaC,cAE5BtB,WAAWuB,aA4DLC,UAAYpB,aAAaE,eAAeT,UAAUU,QAAQC,aAC1DiB,eAAiBrB,aAAasB,WAAWF,cAC3CC,eAAgB,OACVE,qBAAuB,IAAIC,iBAAQ,mCArDjBC,CAAAA,eAClBC,aAAeC,OAAOC,QAAQH,OAAO3B,aAEtC4B,aAAaG,cAEPC,QAAQf,UAInBtB,UAAUc,cAAcC,mBAAUC,UAAUC,OAAOC,MAAMC,MAAQa,OAAOpB,eAElE0B,eAAiBL,aAAaM,KAAIC,WAAEC,WAAYC,oBAC/B,aAAfD,kBAEO,QAGLE,aAAeD,WAAWE,eAE3BD,aAAaP,QAKXjC,WAAW0C,eACbzB,MAAK0B,YAAEC,iBACJ5C,WAAW6C,UAAUD,UAAWN,WAAYE,oBAGrDM,QAAOC,SAAWA,iBAEhBZ,eAAeF,OAIbC,QAAQc,IAAIb,gBACdlB,MAAK,IACKjB,WAAWiD,uBAErBhC,MAAK,KACFjB,WAAWkD,0BAGdjC,MAAK,KACFjB,WAAWmD,2BAZRjB,QAAQf,WAuBnBiC,CAAoB3B,gBACfR,MAAK,IAAMU,qBAAqBR,YAChCC"} amd/build/local/participants/bulkactions.min.js.map 0000644 00000020255 15151162244 0016416 0 ustar 00 {"version":3,"file":"bulkactions.min.js","sources":["../../../src/local/participants/bulkactions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Bulk actions for lists of participants.\n *\n * @module core_user/local/participants/bulkactions\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Repository from 'core_user/repository';\nimport * as Str from 'core/str';\nimport ModalEvents from 'core/modal_events';\nimport ModalFactory from 'core/modal_factory';\nimport Notification from 'core/notification';\nimport Templates from 'core/templates';\nimport {add as notifyUser} from 'core/toast';\n\n/**\n * Show the add note popup\n *\n * @param {Number} courseid\n * @param {Number[]} users\n * @param {String[]} noteStateNames\n * @param {HTMLElement} stateHelpIcon\n * @return {Promise}\n */\nexport const showAddNote = (courseid, users, noteStateNames, stateHelpIcon) => {\n if (!users.length) {\n // No users were selected.\n return Promise.resolve();\n }\n\n const states = [];\n for (let key in noteStateNames) {\n switch (key) {\n case 'draft':\n states.push({value: 'personal', label: noteStateNames[key]});\n break;\n case 'public':\n states.push({value: 'course', label: noteStateNames[key], selected: 1});\n break;\n case 'site':\n states.push({value: key, label: noteStateNames[key]});\n break;\n }\n }\n\n const context = {\n stateNames: states,\n stateHelpIcon: stateHelpIcon.innerHTML,\n };\n\n let titlePromise = null;\n if (users.length === 1) {\n titlePromise = Str.get_string('addbulknotesingle', 'core_notes');\n } else {\n titlePromise = Str.get_string('addbulknote', 'core_notes', users.length);\n }\n\n return ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n body: Templates.render('core_user/add_bulk_note', context),\n title: titlePromise,\n buttons: {\n save: titlePromise,\n },\n removeOnClose: true,\n })\n .then(modal => {\n modal.getRoot().on(ModalEvents.save, () => submitAddNote(courseid, users, modal));\n\n modal.show();\n\n return modal;\n });\n};\n\n/**\n * Add a note to this list of users.\n *\n * @param {Number} courseid\n * @param {Number[]} users\n * @param {Modal} modal\n * @return {Promise}\n */\nconst submitAddNote = (courseid, users, modal) => {\n const text = modal.getRoot().find('form textarea').val();\n const publishstate = modal.getRoot().find('form select').val();\n\n const notes = users.map(userid => {\n return {\n userid,\n text,\n courseid,\n publishstate,\n };\n });\n\n return Repository.createNotesForUsers(notes)\n .then(noteIds => {\n if (noteIds.length === 1) {\n return Str.get_string('addbulknotedonesingle', 'core_notes');\n } else {\n return Str.get_string('addbulknotedone', 'core_notes', noteIds.length);\n }\n })\n .then(msg => notifyUser(msg))\n .catch(Notification.exception);\n};\n\n/**\n * Show the send message popup.\n *\n * @param {Number[]} users\n * @return {Promise}\n */\nexport const showSendMessage = users => {\n if (!users.length) {\n // Nothing to do.\n return Promise.resolve();\n }\n\n let titlePromise;\n if (users.length === 1) {\n titlePromise = Str.get_string('sendbulkmessagesingle', 'core_message');\n } else {\n titlePromise = Str.get_string('sendbulkmessage', 'core_message', users.length);\n }\n\n return ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n body: Templates.render('core_user/send_bulk_message', {}),\n title: titlePromise,\n buttons: {\n save: titlePromise,\n },\n removeOnClose: true,\n })\n .then(modal => {\n modal.getRoot().on(ModalEvents.save, (e) => {\n const text = modal.getRoot().find('form textarea').val();\n if (text.trim() === '') {\n modal.getRoot().find('[data-role=\"messagetextrequired\"]').removeAttr('hidden');\n e.preventDefault();\n return;\n }\n\n submitSendMessage(modal, users, text);\n });\n\n modal.show();\n\n return modal;\n });\n};\n\n/**\n * Send a message to these users.\n *\n * @param {Modal} modal\n * @param {Number[]} users\n * @param {String} text\n * @return {Promise}\n */\nconst submitSendMessage = (modal, users, text) => {\n const messages = users.map(touserid => {\n return {\n touserid,\n text,\n };\n });\n\n return Repository.sendMessagesToUsers(messages)\n .then(messageIds => {\n if (messageIds.length == 1) {\n return Str.get_string('sendbulkmessagesentsingle', 'core_message');\n } else {\n return Str.get_string('sendbulkmessagesent', 'core_message', messageIds.length);\n }\n })\n .then(msg => notifyUser(msg))\n .catch(Notification.exception);\n};\n"],"names":["courseid","users","noteStateNames","stateHelpIcon","length","Promise","resolve","states","key","push","value","label","selected","context","stateNames","innerHTML","titlePromise","Str","get_string","ModalFactory","create","type","types","SAVE_CANCEL","body","Templates","render","title","buttons","save","removeOnClose","then","modal","getRoot","on","ModalEvents","submitAddNote","show","text","find","val","publishstate","notes","map","userid","Repository","createNotesForUsers","noteIds","msg","catch","Notification","exception","e","trim","removeAttr","preventDefault","submitSendMessage","messages","touserid","sendMessagesToUsers","messageIds"],"mappings":";;;;;;;maAwC2B,CAACA,SAAUC,MAAOC,eAAgBC,qBACpDF,MAAMG,cAEAC,QAAQC,gBAGbC,OAAS,OACV,IAAIC,OAAON,sBACJM,SACC,QACDD,OAAOE,KAAK,CAACC,MAAO,WAAYC,MAAOT,eAAeM,iBAErD,SACDD,OAAOE,KAAK,CAACC,MAAO,SAAUC,MAAOT,eAAeM,KAAMI,SAAU,cAEnE,OACDL,OAAOE,KAAK,CAACC,MAAOF,IAAKG,MAAOT,eAAeM,aAKrDK,QAAU,CACZC,WAAYP,OACZJ,cAAeA,cAAcY,eAG7BC,aAAe,YAEfA,aADiB,IAAjBf,MAAMG,OACSa,IAAIC,WAAW,oBAAqB,cAEpCD,IAAIC,WAAW,cAAe,aAAcjB,MAAMG,QAG9De,uBAAaC,OAAO,CACvBC,KAAMF,uBAAaG,MAAMC,YACzBC,KAAMC,mBAAUC,OAAO,0BAA2Bb,SAClDc,MAAOX,aACPY,QAAS,CACLC,KAAMb,cAEVc,eAAe,IAElBC,MAAKC,QACFA,MAAMC,UAAUC,GAAGC,sBAAYN,MAAM,IAAMO,cAAcpC,SAAUC,MAAO+B,SAE1EA,MAAMK,OAECL,gBAYTI,cAAgB,CAACpC,SAAUC,MAAO+B,eAC9BM,KAAON,MAAMC,UAAUM,KAAK,iBAAiBC,MAC7CC,aAAeT,MAAMC,UAAUM,KAAK,eAAeC,MAEnDE,MAAQzC,MAAM0C,KAAIC,SACb,CACHA,OAAAA,OACAN,KAAAA,KACAtC,SAAAA,SACAyC,aAAAA,wBAIDI,WAAWC,oBAAoBJ,OACrCX,MAAKgB,SACqB,IAAnBA,QAAQ3C,OACDa,IAAIC,WAAW,wBAAyB,cAExCD,IAAIC,WAAW,kBAAmB,aAAc6B,QAAQ3C,UAGtE2B,MAAKiB,MAAO,cAAWA,OACvBC,MAAMC,sBAAaC,qCASOlD,YACtBA,MAAMG,cAEAC,QAAQC,cAGfU,oBAEAA,aADiB,IAAjBf,MAAMG,OACSa,IAAIC,WAAW,wBAAyB,gBAExCD,IAAIC,WAAW,kBAAmB,eAAgBjB,MAAMG,QAGpEe,uBAAaC,OAAO,CACvBC,KAAMF,uBAAaG,MAAMC,YACzBC,KAAMC,mBAAUC,OAAO,8BAA+B,IACtDC,MAAOX,aACPY,QAAS,CACLC,KAAMb,cAEVc,eAAe,IAElBC,MAAKC,QACFA,MAAMC,UAAUC,GAAGC,sBAAYN,MAAOuB,UAC5Bd,KAAON,MAAMC,UAAUM,KAAK,iBAAiBC,SAC/B,KAAhBF,KAAKe,cACLrB,MAAMC,UAAUM,KAAK,qCAAqCe,WAAW,eACrEF,EAAEG,iBAINC,kBAAkBxB,MAAO/B,MAAOqC,SAGpCN,MAAMK,OAECL,gBAYTwB,kBAAoB,CAACxB,MAAO/B,MAAOqC,cAC/BmB,SAAWxD,MAAM0C,KAAIe,WAChB,CACHA,SAAAA,SACApB,KAAAA,gBAIDO,WAAWc,oBAAoBF,UACrC1B,MAAK6B,YACuB,GAArBA,WAAWxD,OACJa,IAAIC,WAAW,4BAA6B,gBAE5CD,IAAIC,WAAW,sBAAuB,eAAgB0C,WAAWxD,UAG/E2B,MAAKiB,MAAO,cAAWA,OACvBC,MAAMC,sBAAaC"} amd/build/local/participants/bulkactions.min.js 0000644 00000011073 15151162244 0015640 0 ustar 00 define("core_user/local/participants/bulkactions",["exports","core_user/repository","core/str","core/modal_events","core/modal_factory","core/notification","core/templates","core/toast"],(function(_exports,Repository,Str,_modal_events,_modal_factory,_notification,_templates,_toast){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * Bulk actions for lists of participants. * * @module core_user/local/participants/bulkactions * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showSendMessage=_exports.showAddNote=void 0,Repository=_interopRequireWildcard(Repository),Str=_interopRequireWildcard(Str),_modal_events=_interopRequireDefault(_modal_events),_modal_factory=_interopRequireDefault(_modal_factory),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates);_exports.showAddNote=(courseid,users,noteStateNames,stateHelpIcon)=>{if(!users.length)return Promise.resolve();const states=[];for(let key in noteStateNames)switch(key){case"draft":states.push({value:"personal",label:noteStateNames[key]});break;case"public":states.push({value:"course",label:noteStateNames[key],selected:1});break;case"site":states.push({value:key,label:noteStateNames[key]})}const context={stateNames:states,stateHelpIcon:stateHelpIcon.innerHTML};let titlePromise=null;return titlePromise=1===users.length?Str.get_string("addbulknotesingle","core_notes"):Str.get_string("addbulknote","core_notes",users.length),_modal_factory.default.create({type:_modal_factory.default.types.SAVE_CANCEL,body:_templates.default.render("core_user/add_bulk_note",context),title:titlePromise,buttons:{save:titlePromise},removeOnClose:!0}).then((modal=>(modal.getRoot().on(_modal_events.default.save,(()=>submitAddNote(courseid,users,modal))),modal.show(),modal)))};const submitAddNote=(courseid,users,modal)=>{const text=modal.getRoot().find("form textarea").val(),publishstate=modal.getRoot().find("form select").val(),notes=users.map((userid=>({userid:userid,text:text,courseid:courseid,publishstate:publishstate})));return Repository.createNotesForUsers(notes).then((noteIds=>1===noteIds.length?Str.get_string("addbulknotedonesingle","core_notes"):Str.get_string("addbulknotedone","core_notes",noteIds.length))).then((msg=>(0,_toast.add)(msg))).catch(_notification.default.exception)};_exports.showSendMessage=users=>{if(!users.length)return Promise.resolve();let titlePromise;return titlePromise=1===users.length?Str.get_string("sendbulkmessagesingle","core_message"):Str.get_string("sendbulkmessage","core_message",users.length),_modal_factory.default.create({type:_modal_factory.default.types.SAVE_CANCEL,body:_templates.default.render("core_user/send_bulk_message",{}),title:titlePromise,buttons:{save:titlePromise},removeOnClose:!0}).then((modal=>(modal.getRoot().on(_modal_events.default.save,(e=>{const text=modal.getRoot().find("form textarea").val();if(""===text.trim())return modal.getRoot().find('[data-role="messagetextrequired"]').removeAttr("hidden"),void e.preventDefault();submitSendMessage(modal,users,text)})),modal.show(),modal)))};const submitSendMessage=(modal,users,text)=>{const messages=users.map((touserid=>({touserid:touserid,text:text})));return Repository.sendMessagesToUsers(messages).then((messageIds=>1==messageIds.length?Str.get_string("sendbulkmessagesentsingle","core_message"):Str.get_string("sendbulkmessagesent","core_message",messageIds.length))).then((msg=>(0,_toast.add)(msg))).catch(_notification.default.exception)}})); //# sourceMappingURL=bulkactions.min.js.map amd/build/form_user_selector.min.js 0000644 00000002500 15151162244 0013423 0 ustar 00 define("core_user/form_user_selector",["exports","core/ajax","core/templates","core/str"],(function(_exports,_ajax,_templates,_str){var obj; /** * Provides the required functionality for an autocomplete element to select a user. * * @module core_user/form_user_selector * @copyright 2020 David Mudrák <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.processResults=function(selector,results){return Array.isArray(results)?results.map((result=>({value:result.id,label:result.label}))):results},_exports.transport=async function(selector,query,callback,failure){const request={methodname:"core_user_search_identity",args:{query:query}};try{const response=await _ajax.default.call([request])[0];if(response.overflow){const msg=await(0,_str.get_string)("toomanyuserstoshow","core",">"+response.maxusersperpage);callback(msg)}else{let labels=[];response.list.forEach((user=>{labels.push((0,_templates.render)("core_user/form_user_selector_suggestion",user))})),labels=await Promise.all(labels),response.list.forEach(((user,index)=>{user.label=labels[index]})),callback(response.list)}}catch(e){failure(e)}},_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj}})); //# sourceMappingURL=form_user_selector.min.js.map amd/build/status_field.min.js.map 0000644 00000035022 15151162244 0012771 0 ustar 00 {"version":3,"file":"status_field.min.js","sources":["../src/status_field.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * AMD module for the user enrolment status field in the course participants page.\n *\n * @module core_user/status_field\n * @copyright 2017 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as DynamicTable from 'core_table/dynamic';\nimport * as Repository from './repository';\nimport * as Str from 'core/str';\nimport DynamicTableSelectors from 'core_table/local/dynamic/selectors';\nimport Fragment from 'core/fragment';\nimport ModalEvents from 'core/modal_events';\nimport ModalFactory from 'core/modal_factory';\nimport Notification from 'core/notification';\nimport Templates from 'core/templates';\nimport {add as notifyUser} from 'core/toast';\n\nconst Selectors = {\n editEnrolment: '[data-action=\"editenrolment\"]',\n showDetails: '[data-action=\"showdetails\"]',\n unenrol: '[data-action=\"unenrol\"]',\n statusElement: '[data-status]',\n};\n\n/**\n * Get the dynamic table from the specified link.\n *\n * @param {HTMLElement} link\n * @returns {HTMLElement}\n */\nconst getDynamicTableFromLink = link => link.closest(DynamicTableSelectors.main.region);\n\n/**\n * Get the status container from the specified link.\n *\n * @param {HTMLElement} link\n * @returns {HTMLElement}\n */\nconst getStatusContainer = link => link.closest(Selectors.statusElement);\n\n/**\n * Get user enrolment id from the specified link\n *\n * @param {HTMLElement} link\n * @returns {Number}\n */\nconst getUserEnrolmentIdFromLink = link => link.getAttribute('rel');\n\n/**\n * Register all event listeners for the status fields.\n *\n * @param {Number} contextId\n * @param {Number} uniqueId\n */\nconst registerEventListeners = (contextId, uniqueId) => {\n const getBodyFunction = (userEnrolmentId, formData) => getBody(contextId, userEnrolmentId, formData);\n\n document.addEventListener('click', e => {\n const tableRoot = e.target.closest(DynamicTableSelectors.main.fromRegionId(uniqueId));\n if (!tableRoot) {\n return;\n }\n\n const editLink = e.target.closest(Selectors.editEnrolment);\n if (editLink) {\n e.preventDefault();\n\n showEditDialogue(editLink, getBodyFunction);\n }\n\n const unenrolLink = e.target.closest(Selectors.unenrol);\n if (unenrolLink) {\n e.preventDefault();\n\n showUnenrolConfirmation(unenrolLink);\n }\n\n const showDetailsLink = e.target.closest(Selectors.showDetails);\n if (showDetailsLink) {\n e.preventDefault();\n\n showStatusDetails(showDetailsLink);\n }\n });\n};\n\n/**\n * Show the edit dialogue.\n *\n * @param {HTMLElement} link\n * @param {Function} getBody Function to get the body for the specified user enrolment\n */\nconst showEditDialogue = (link, getBody) => {\n const container = getStatusContainer(link);\n const userEnrolmentId = getUserEnrolmentIdFromLink(link);\n\n ModalFactory.create({\n large: true,\n title: Str.get_string('edituserenrolment', 'enrol', container.dataset.fullname),\n type: ModalFactory.types.SAVE_CANCEL,\n body: getBody(userEnrolmentId)\n })\n .then(modal => {\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, e => {\n // Don't close the modal yet.\n e.preventDefault();\n\n // Submit form data.\n submitEditFormAjax(link, getBody, modal, userEnrolmentId, container.dataset);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Destroy when hidden.\n modal.destroy();\n });\n\n // Show the modal.\n modal.show();\n\n return modal;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Show and handle the unenrolment confirmation dialogue.\n *\n * @param {HTMLElement} link\n */\nconst showUnenrolConfirmation = link => {\n const container = getStatusContainer(link);\n const userEnrolmentId = getUserEnrolmentIdFromLink(link);\n\n ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n })\n .then(modal => {\n // Handle confirm event.\n modal.getRoot().on(ModalEvents.save, e => {\n // Don't close the modal yet.\n e.preventDefault();\n\n // Submit data.\n submitUnenrolFormAjax(\n link,\n modal,\n {\n ueid: userEnrolmentId,\n },\n container.dataset\n );\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Destroy when hidden.\n modal.destroy();\n });\n\n // Display the delete confirmation modal.\n modal.show();\n\n const stringData = [\n {\n key: 'unenrol',\n component: 'enrol',\n },\n {\n key: 'unenrolconfirm',\n component: 'enrol',\n param: {\n user: container.dataset.fullname,\n course: container.dataset.coursename,\n enrolinstancename: container.dataset.enrolinstancename,\n }\n }\n ];\n\n return Promise.all([Str.get_strings(stringData), modal]);\n })\n .then(([strings, modal]) => {\n modal.setTitle(strings[0]);\n modal.setSaveButtonText(strings[0]);\n modal.setBody(strings[1]);\n\n return modal;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Show the user details dialogue.\n *\n * @param {HTMLElement} link\n */\nconst showStatusDetails = link => {\n const container = getStatusContainer(link);\n\n const context = {\n editenrollink: '',\n statusclass: container.querySelector('span.badge').getAttribute('class'),\n ...container.dataset,\n };\n\n // Find the edit enrolment link.\n const editEnrolLink = container.querySelector(Selectors.editEnrolment);\n if (editEnrolLink) {\n // If there's an edit enrolment link for this user, clone it into the context for the modal.\n context.editenrollink = editEnrolLink.outerHTML;\n }\n\n ModalFactory.create({\n large: true,\n type: ModalFactory.types.CANCEL,\n title: Str.get_string('enroldetails', 'enrol'),\n body: Templates.render('core_user/status_details', context),\n })\n .then(modal => {\n if (editEnrolLink) {\n modal.getRoot().on('click', Selectors.editEnrolment, e => {\n e.preventDefault();\n modal.hide();\n\n // Trigger click event for the edit enrolment link to show the edit enrolment modal.\n editEnrolLink.click();\n });\n }\n\n modal.show();\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, () => modal.destroy());\n\n return modal;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Submit the edit dialogue.\n *\n * @param {HTMLElement} clickedLink\n * @param {Function} getBody\n * @param {Object} modal\n * @param {Number} userEnrolmentId\n * @param {Object} userData\n */\nconst submitEditFormAjax = (clickedLink, getBody, modal, userEnrolmentId, userData) => {\n const form = modal.getRoot().find('form');\n\n Repository.submitUserEnrolmentForm(form.serialize())\n .then(data => {\n if (!data.result) {\n throw data.result;\n }\n\n // Dismiss the modal.\n modal.hide();\n modal.destroy();\n\n return data;\n })\n .then(() => {\n DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink))\n .catch(Notification.exception);\n\n return Str.get_string('enrolmentupdatedforuser', 'core_enrol', userData);\n })\n .then(notificationString => {\n notifyUser(notificationString);\n\n return;\n })\n .catch(() => {\n modal.setBody(getBody(userEnrolmentId, JSON.stringify(form.serialize())));\n\n return modal;\n });\n};\n\n/**\n * Submit the unenrolment form.\n *\n * @param {HTMLElement} clickedLink\n * @param {Object} modal\n * @param {Object} args\n * @param {Object} userData\n */\nconst submitUnenrolFormAjax = (clickedLink, modal, args, userData) => {\n Repository.unenrolUser(args.ueid)\n .then(data => {\n if (!data.result) {\n // Display an alert containing the error message\n Notification.alert(data.errors[0].key, data.errors[0].message);\n\n return data;\n }\n\n // Dismiss the modal.\n modal.hide();\n modal.destroy();\n\n return data;\n })\n .then(() => {\n DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink))\n .catch(Notification.exception);\n\n return Str.get_string('unenrolleduser', 'core_enrol', userData);\n })\n .then(notificationString => {\n notifyUser(notificationString);\n\n return;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Get the body fragment.\n *\n * @param {Number} contextId\n * @param {Number} ueid The user enrolment id\n * @param {Object} formdata\n * @returns {Promise}\n */\nconst getBody = (contextId, ueid, formdata = null) => Fragment.loadFragment(\n 'enrol',\n 'user_enrolment_form',\n contextId,\n {\n ueid,\n formdata,\n }\n);\n\n/**\n * Initialise the statu field handler.\n *\n * @param {object} param\n * @param {Number} param.contextid\n * @param {Number} param.uniqueid\n */\nexport const init = ({contextid, uniqueid}) => {\n registerEventListeners(contextid, uniqueid);\n};\n"],"names":["Selectors","getDynamicTableFromLink","link","closest","DynamicTableSelectors","main","region","getStatusContainer","getUserEnrolmentIdFromLink","getAttribute","showEditDialogue","getBody","container","userEnrolmentId","create","large","title","Str","get_string","dataset","fullname","type","ModalFactory","types","SAVE_CANCEL","body","then","modal","getRoot","on","ModalEvents","save","e","preventDefault","submitEditFormAjax","hidden","destroy","show","catch","Notification","exception","showUnenrolConfirmation","submitUnenrolFormAjax","ueid","stringData","key","component","param","user","course","coursename","enrolinstancename","Promise","all","get_strings","_ref","strings","setTitle","setSaveButtonText","setBody","showStatusDetails","context","editenrollink","statusclass","querySelector","editEnrolLink","outerHTML","CANCEL","Templates","render","hide","click","clickedLink","userData","form","find","Repository","submitUserEnrolmentForm","serialize","data","result","DynamicTable","refreshTableContent","notificationString","JSON","stringify","args","unenrolUser","alert","errors","message","contextId","formdata","Fragment","loadFragment","_ref2","contextid","uniqueid","uniqueId","getBodyFunction","formData","document","addEventListener","target","fromRegionId","editLink","unenrolLink","showDetailsLink","registerEventListeners"],"mappings":";;;;;;;igBAkCMA,wBACa,gCADbA,sBAEW,8BAFXA,kBAGO,0BAHPA,wBAIa,gBASbC,wBAA0BC,MAAQA,KAAKC,QAAQC,mBAAsBC,KAAKC,QAQ1EC,mBAAqBL,MAAQA,KAAKC,QAAQH,yBAQ1CQ,2BAA6BN,MAAQA,KAAKO,aAAa,OA8CvDC,iBAAmB,CAACR,KAAMS,iBACtBC,UAAYL,mBAAmBL,MAC/BW,gBAAkBL,2BAA2BN,6BAEtCY,OAAO,CAChBC,OAAO,EACPC,MAAOC,IAAIC,WAAW,oBAAqB,QAASN,UAAUO,QAAQC,UACtEC,KAAMC,uBAAaC,MAAMC,YACzBC,KAAMd,QAAQE,mBAEjBa,MAAKC,QAEFA,MAAMC,UAAUC,GAAGC,sBAAYC,MAAMC,IAEjCA,EAAEC,iBAGFC,mBAAmBhC,KAAMS,QAASgB,MAAOd,gBAAiBD,UAAUO,YAIxEQ,MAAMC,UAAUC,GAAGC,sBAAYK,QAAQ,KAEnCR,MAAMS,aAIVT,MAAMU,OAECV,SAEVW,MAAMC,sBAAaC,YAQlBC,wBAA0BvC,aACtBU,UAAYL,mBAAmBL,MAC/BW,gBAAkBL,2BAA2BN,6BAEtCY,OAAO,CAChBO,KAAMC,uBAAaC,MAAMC,cAE5BE,MAAKC,QAEFA,MAAMC,UAAUC,GAAGC,sBAAYC,MAAMC,IAEjCA,EAAEC,iBAGFS,sBACIxC,KACAyB,MACA,CACIgB,KAAM9B,iBAEVD,UAAUO,YAKlBQ,MAAMC,UAAUC,GAAGC,sBAAYK,QAAQ,KAEnCR,MAAMS,aAIVT,MAAMU,aAEAO,WAAa,CACf,CACIC,IAAK,UACLC,UAAW,SAEf,CACID,IAAK,iBACLC,UAAW,QACXC,MAAO,CACHC,KAAMpC,UAAUO,QAAQC,SACxB6B,OAAQrC,UAAUO,QAAQ+B,WAC1BC,kBAAmBvC,UAAUO,QAAQgC,4BAK1CC,QAAQC,IAAI,CAACpC,IAAIqC,YAAYV,YAAajB,WAEpDD,MAAK6B,WAAEC,QAAS7B,mBACbA,MAAM8B,SAASD,QAAQ,IACvB7B,MAAM+B,kBAAkBF,QAAQ,IAChC7B,MAAMgC,QAAQH,QAAQ,IAEf7B,SAEVW,MAAMC,sBAAaC,YAQlBoB,kBAAoB1D,aAChBU,UAAYL,mBAAmBL,MAE/B2D,QAAU,CACZC,cAAe,GACfC,YAAanD,UAAUoD,cAAc,cAAcvD,aAAa,YAC7DG,UAAUO,SAIX8C,cAAgBrD,UAAUoD,cAAchE,yBAC1CiE,gBAEAJ,QAAQC,cAAgBG,cAAcC,kCAG7BpD,OAAO,CAChBC,OAAO,EACPM,KAAMC,uBAAaC,MAAM4C,OACzBnD,MAAOC,IAAIC,WAAW,eAAgB,SACtCO,KAAM2C,mBAAUC,OAAO,2BAA4BR,WAEtDnC,MAAKC,QACEsC,eACAtC,MAAMC,UAAUC,GAAG,QAAS7B,yBAAyBgC,IACjDA,EAAEC,iBACFN,MAAM2C,OAGNL,cAAcM,WAItB5C,MAAMU,OAGNV,MAAMC,UAAUC,GAAGC,sBAAYK,QAAQ,IAAMR,MAAMS,YAE5CT,SAEVW,MAAMC,sBAAaC,YAYlBN,mBAAqB,CAACsC,YAAa7D,QAASgB,MAAOd,gBAAiB4D,kBAChEC,KAAO/C,MAAMC,UAAU+C,KAAK,QAElCC,WAAWC,wBAAwBH,KAAKI,aACvCpD,MAAKqD,WACGA,KAAKC,aACAD,KAAKC,cAIfrD,MAAM2C,OACN3C,MAAMS,UAEC2C,QAEVrD,MAAK,KACFuD,aAAaC,oBAAoBjF,wBAAwBuE,cACxDlC,MAAMC,sBAAaC,WAEbvB,IAAIC,WAAW,0BAA2B,aAAcuD,aAElE/C,MAAKyD,oCACSA,uBAId7C,OAAM,KACHX,MAAMgC,QAAQhD,QAAQE,gBAAiBuE,KAAKC,UAAUX,KAAKI,eAEpDnD,UAYTe,sBAAwB,CAAC8B,YAAa7C,MAAO2D,KAAMb,YACrDG,WAAWW,YAAYD,KAAK3C,MAC3BjB,MAAKqD,MACGA,KAAKC,QAQVrD,MAAM2C,OACN3C,MAAMS,UAEC2C,6BATUS,MAAMT,KAAKU,OAAO,GAAG5C,IAAKkC,KAAKU,OAAO,GAAGC,SAE/CX,QASdrD,MAAK,KACFuD,aAAaC,oBAAoBjF,wBAAwBuE,cACxDlC,MAAMC,sBAAaC,WAEbvB,IAAIC,WAAW,iBAAkB,aAAcuD,aAEzD/C,MAAKyD,oCACSA,uBAId7C,MAAMC,sBAAaC,YAWlB7B,QAAU,SAACgF,UAAWhD,UAAMiD,gEAAW,YAASC,kBAASC,aAC3D,QACA,sBACAH,UACA,CACIhD,KAAAA,KACAiD,SAAAA,0BAWYG,YAACC,UAACA,UAADC,SAAYA,gBAnSF,EAACN,UAAWO,kBACjCC,gBAAkB,CAACtF,gBAAiBuF,WAAazF,QAAQgF,UAAW9E,gBAAiBuF,UAE3FC,SAASC,iBAAiB,SAAStE,QACbA,EAAEuE,OAAOpG,QAAQC,mBAAsBC,KAAKmG,aAAaN,wBAKrEO,SAAWzE,EAAEuE,OAAOpG,QAAQH,yBAC9ByG,WACAzE,EAAEC,iBAEFvB,iBAAiB+F,SAAUN,wBAGzBO,YAAc1E,EAAEuE,OAAOpG,QAAQH,mBACjC0G,cACA1E,EAAEC,iBAEFQ,wBAAwBiE,oBAGtBC,gBAAkB3E,EAAEuE,OAAOpG,QAAQH,uBACrC2G,kBACA3E,EAAEC,iBAEF2B,kBAAkB+C,sBAyQ1BC,CAAuBZ,UAAWC"} amd/build/form_user_selector.min.js.map 0000644 00000007362 15151162244 0014212 0 ustar 00 {"version":3,"file":"form_user_selector.min.js","sources":["../src/form_user_selector.js"],"sourcesContent":["// This file is part of Moodle - https://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Provides the required functionality for an autocomplete element to select a user.\n *\n * @module core_user/form_user_selector\n * @copyright 2020 David Mudrák <david@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport {render as renderTemplate} from 'core/templates';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Load the list of users matching the query and render the selector labels for them.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {String} query The query string.\n * @param {Function} callback A callback function receiving an array of results.\n * @param {Function} failure A function to call in case of failure, receiving the error message.\n */\nexport async function transport(selector, query, callback, failure) {\n\n const request = {\n methodname: 'core_user_search_identity',\n args: {\n query: query\n }\n };\n\n try {\n const response = await Ajax.call([request])[0];\n\n if (response.overflow) {\n const msg = await getString('toomanyuserstoshow', 'core', '>' + response.maxusersperpage);\n callback(msg);\n\n } else {\n let labels = [];\n response.list.forEach(user => {\n labels.push(renderTemplate('core_user/form_user_selector_suggestion', user));\n });\n labels = await Promise.all(labels);\n\n response.list.forEach((user, index) => {\n user.label = labels[index];\n });\n\n callback(response.list);\n }\n\n } catch (e) {\n failure(e);\n }\n}\n\n/**\n * Process the results for auto complete elements.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {Array} results An array or results returned by {@see transport()}.\n * @return {Array} New array of the selector options.\n */\nexport function processResults(selector, results) {\n\n if (!Array.isArray(results)) {\n return results;\n\n } else {\n return results.map(result => ({value: result.id, label: result.label}));\n }\n}\n"],"names":["selector","results","Array","isArray","map","result","value","id","label","query","callback","failure","request","methodname","args","response","Ajax","call","overflow","msg","maxusersperpage","labels","list","forEach","user","push","Promise","all","index","e"],"mappings":";;;;;;;8FA6E+BA,SAAUC,gBAEhCC,MAAMC,QAAQF,SAIRA,QAAQG,KAAIC,UAAYC,MAAOD,OAAOE,GAAIC,MAAOH,OAAOG,UAHxDP,2CA7CiBD,SAAUS,MAAOC,SAAUC,eAEjDC,QAAU,CACZC,WAAY,4BACZC,KAAM,CACFL,MAAOA,kBAKLM,eAAiBC,cAAKC,KAAK,CAACL,UAAU,MAExCG,SAASG,SAAU,OACbC,UAAY,mBAAU,qBAAsB,OAAQ,IAAMJ,SAASK,iBACzEV,SAASS,SAEN,KACCE,OAAS,GACbN,SAASO,KAAKC,SAAQC,OAClBH,OAAOI,MAAK,qBAAe,0CAA2CD,UAE1EH,aAAeK,QAAQC,IAAIN,QAE3BN,SAASO,KAAKC,SAAQ,CAACC,KAAMI,SACzBJ,KAAKhB,MAAQa,OAAOO,UAGxBlB,SAASK,SAASO,OAGxB,MAAOO,GACLlB,QAAQkB"} amd/build/participants.min.js 0000644 00000014000 15151162244 0012221 0 ustar 00 define("core_user/participants",["exports","core_table/dynamic","core/str","core/checkbox-toggleall","core/custom_interaction_events","core_table/local/dynamic/selectors","core/modal_events","core/notification","core/pending","jquery","core_user/local/participants/bulkactions","core/inplace_editable"],(function(_exports,DynamicTable,Str,_checkboxToggleall,_custom_interaction_events,_selectors,_modal_events,_notification,_pending,_jquery,_bulkactions,_inplace_editable){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * Some UI stuff for participants page. * This is also used by the report/participants/index.php because it has the same functionality. * * @module core_user/participants * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,DynamicTable=_interopRequireWildcard(DynamicTable),Str=_interopRequireWildcard(Str),_checkboxToggleall=_interopRequireDefault(_checkboxToggleall),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_selectors=_interopRequireDefault(_selectors),_modal_events=_interopRequireDefault(_modal_events),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_jquery=_interopRequireDefault(_jquery);const Selectors_bulkActionSelect="#formactionid",Selectors_bulkUserSelectedCheckBoxes="input[data-togglegroup='participants-table'][data-toggle='slave']:checked",Selectors_checkCountButton="#checkall",Selectors_showCountText='[data-region="participant-count"]',Selectors_stateHelpIcon='[data-region="state-help-icon"]',Selectors_tableForm=uniqueId=>'form[data-table-unique-id="'.concat(uniqueId,'"]');_exports.init=_ref=>{let{uniqueid:uniqueid,noteStateNames:noteStateNames={}}=_ref;const root=document.querySelector(Selectors_tableForm(uniqueid)),getTableFromUniqueId=uniqueId=>root.querySelector(_selectors.default.main.fromRegionId(uniqueId)),resetBulkAction=bulkActionSelect=>{bulkActionSelect.value=""};_custom_interaction_events.default.define(Selectors_bulkActionSelect,[_custom_interaction_events.default.events.accessibleChange]),(0,_jquery.default)(Selectors_bulkActionSelect).on(_custom_interaction_events.default.events.accessibleChange,(e=>{const bulkActionSelect=e.target.closest("select"),action=bulkActionSelect.value,checkboxes=getTableFromUniqueId(uniqueid).querySelectorAll(Selectors_bulkUserSelectedCheckBoxes),pendingPromise=new _pending.default("core_user/participants:bulkActionSelect");if(-1!==action.indexOf("#")){e.preventDefault();const ids=[];let bulkAction;if(checkboxes.forEach((checkbox=>{ids.push(checkbox.getAttribute("name").replace("user",""))})),"#messageselect"===action?bulkAction=(0,_bulkactions.showSendMessage)(ids):"#addgroupnote"===action&&(bulkAction=(0,_bulkactions.showAddNote)(root.dataset.courseId,ids,noteStateNames,root.querySelector(Selectors_stateHelpIcon))),bulkAction){const pendingBulkAction=new _pending.default("core_user/participants:bulkActionSelected");bulkAction.then((modal=>(modal.getRoot().on(_modal_events.default.hidden,(()=>{bulkActionSelect.focus()})),pendingBulkAction.resolve(),modal))).catch(_notification.default.exception)}}else""!==action&&checkboxes.length&&bulkActionSelect.form.submit();resetBulkAction(bulkActionSelect),pendingPromise.resolve()})),root.addEventListener("click",(e=>{const checkCountButton=root.querySelector(Selectors_checkCountButton);if(checkCountButton&&checkCountButton.contains(e.target)){e.preventDefault();const tableRoot=getTableFromUniqueId(uniqueid);DynamicTable.setPageSize(tableRoot,checkCountButton.dataset.targetPageSize).then((tableRoot=>(_checkboxToggleall.default.setGroupState(root,"participants-table",!0),tableRoot))).catch(_notification.default.exception)}})),root.addEventListener(DynamicTable.Events.tableContentRefreshed,(e=>{const checkCountButton=root.querySelector(Selectors_checkCountButton),tableRoot=e.target,defaultPageSize=parseInt(tableRoot.dataset.tableDefaultPerPage,10),currentPageSize=parseInt(tableRoot.dataset.tablePageSize,10),totalRowCount=parseInt(tableRoot.dataset.tableTotalRows,10);_checkboxToggleall.default.updateSlavesFromMasterState(root,"participants-table");const pageCountStrings=[{key:"countparticipantsfound",component:"core_user",param:totalRowCount}];totalRowCount<=defaultPageSize?checkCountButton&&checkCountButton.classList.add("hidden"):totalRowCount<=currentPageSize?(pageCountStrings.push({key:"selectalluserswithcount",component:"core",param:defaultPageSize}),checkCountButton&&checkCountButton.classList.add("hidden")):(pageCountStrings.push({key:"selectalluserswithcount",component:"core",param:totalRowCount}),checkCountButton&&checkCountButton.classList.remove("hidden")),Str.get_strings(pageCountStrings).then((_ref2=>{let[showingParticipantCountString,selectCountString]=_ref2;root.querySelector(Selectors_showCountText).innerHTML=showingParticipantCountString,selectCountString&&checkCountButton&&(checkCountButton.value=selectCountString)})).catch(_notification.default.exception)}))}})); //# sourceMappingURL=participants.min.js.mapamd/src/status_field.js 0000644 00000023615 15151162244 0011130 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * AMD module for the user enrolment status field in the course participants page. * * @module core_user/status_field * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import * as DynamicTable from 'core_table/dynamic'; import * as Repository from './repository'; import * as Str from 'core/str'; import DynamicTableSelectors from 'core_table/local/dynamic/selectors'; import Fragment from 'core/fragment'; import ModalEvents from 'core/modal_events'; import ModalFactory from 'core/modal_factory'; import Notification from 'core/notification'; import Templates from 'core/templates'; import {add as notifyUser} from 'core/toast'; const Selectors = { editEnrolment: '[data-action="editenrolment"]', showDetails: '[data-action="showdetails"]', unenrol: '[data-action="unenrol"]', statusElement: '[data-status]', }; /** * Get the dynamic table from the specified link. * * @param {HTMLElement} link * @returns {HTMLElement} */ const getDynamicTableFromLink = link => link.closest(DynamicTableSelectors.main.region); /** * Get the status container from the specified link. * * @param {HTMLElement} link * @returns {HTMLElement} */ const getStatusContainer = link => link.closest(Selectors.statusElement); /** * Get user enrolment id from the specified link * * @param {HTMLElement} link * @returns {Number} */ const getUserEnrolmentIdFromLink = link => link.getAttribute('rel'); /** * Register all event listeners for the status fields. * * @param {Number} contextId * @param {Number} uniqueId */ const registerEventListeners = (contextId, uniqueId) => { const getBodyFunction = (userEnrolmentId, formData) => getBody(contextId, userEnrolmentId, formData); document.addEventListener('click', e => { const tableRoot = e.target.closest(DynamicTableSelectors.main.fromRegionId(uniqueId)); if (!tableRoot) { return; } const editLink = e.target.closest(Selectors.editEnrolment); if (editLink) { e.preventDefault(); showEditDialogue(editLink, getBodyFunction); } const unenrolLink = e.target.closest(Selectors.unenrol); if (unenrolLink) { e.preventDefault(); showUnenrolConfirmation(unenrolLink); } const showDetailsLink = e.target.closest(Selectors.showDetails); if (showDetailsLink) { e.preventDefault(); showStatusDetails(showDetailsLink); } }); }; /** * Show the edit dialogue. * * @param {HTMLElement} link * @param {Function} getBody Function to get the body for the specified user enrolment */ const showEditDialogue = (link, getBody) => { const container = getStatusContainer(link); const userEnrolmentId = getUserEnrolmentIdFromLink(link); ModalFactory.create({ large: true, title: Str.get_string('edituserenrolment', 'enrol', container.dataset.fullname), type: ModalFactory.types.SAVE_CANCEL, body: getBody(userEnrolmentId) }) .then(modal => { // Handle save event. modal.getRoot().on(ModalEvents.save, e => { // Don't close the modal yet. e.preventDefault(); // Submit form data. submitEditFormAjax(link, getBody, modal, userEnrolmentId, container.dataset); }); // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, () => { // Destroy when hidden. modal.destroy(); }); // Show the modal. modal.show(); return modal; }) .catch(Notification.exception); }; /** * Show and handle the unenrolment confirmation dialogue. * * @param {HTMLElement} link */ const showUnenrolConfirmation = link => { const container = getStatusContainer(link); const userEnrolmentId = getUserEnrolmentIdFromLink(link); ModalFactory.create({ type: ModalFactory.types.SAVE_CANCEL, }) .then(modal => { // Handle confirm event. modal.getRoot().on(ModalEvents.save, e => { // Don't close the modal yet. e.preventDefault(); // Submit data. submitUnenrolFormAjax( link, modal, { ueid: userEnrolmentId, }, container.dataset ); }); // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, () => { // Destroy when hidden. modal.destroy(); }); // Display the delete confirmation modal. modal.show(); const stringData = [ { key: 'unenrol', component: 'enrol', }, { key: 'unenrolconfirm', component: 'enrol', param: { user: container.dataset.fullname, course: container.dataset.coursename, enrolinstancename: container.dataset.enrolinstancename, } } ]; return Promise.all([Str.get_strings(stringData), modal]); }) .then(([strings, modal]) => { modal.setTitle(strings[0]); modal.setSaveButtonText(strings[0]); modal.setBody(strings[1]); return modal; }) .catch(Notification.exception); }; /** * Show the user details dialogue. * * @param {HTMLElement} link */ const showStatusDetails = link => { const container = getStatusContainer(link); const context = { editenrollink: '', statusclass: container.querySelector('span.badge').getAttribute('class'), ...container.dataset, }; // Find the edit enrolment link. const editEnrolLink = container.querySelector(Selectors.editEnrolment); if (editEnrolLink) { // If there's an edit enrolment link for this user, clone it into the context for the modal. context.editenrollink = editEnrolLink.outerHTML; } ModalFactory.create({ large: true, type: ModalFactory.types.CANCEL, title: Str.get_string('enroldetails', 'enrol'), body: Templates.render('core_user/status_details', context), }) .then(modal => { if (editEnrolLink) { modal.getRoot().on('click', Selectors.editEnrolment, e => { e.preventDefault(); modal.hide(); // Trigger click event for the edit enrolment link to show the edit enrolment modal. editEnrolLink.click(); }); } modal.show(); // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, () => modal.destroy()); return modal; }) .catch(Notification.exception); }; /** * Submit the edit dialogue. * * @param {HTMLElement} clickedLink * @param {Function} getBody * @param {Object} modal * @param {Number} userEnrolmentId * @param {Object} userData */ const submitEditFormAjax = (clickedLink, getBody, modal, userEnrolmentId, userData) => { const form = modal.getRoot().find('form'); Repository.submitUserEnrolmentForm(form.serialize()) .then(data => { if (!data.result) { throw data.result; } // Dismiss the modal. modal.hide(); modal.destroy(); return data; }) .then(() => { DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink)) .catch(Notification.exception); return Str.get_string('enrolmentupdatedforuser', 'core_enrol', userData); }) .then(notificationString => { notifyUser(notificationString); return; }) .catch(() => { modal.setBody(getBody(userEnrolmentId, JSON.stringify(form.serialize()))); return modal; }); }; /** * Submit the unenrolment form. * * @param {HTMLElement} clickedLink * @param {Object} modal * @param {Object} args * @param {Object} userData */ const submitUnenrolFormAjax = (clickedLink, modal, args, userData) => { Repository.unenrolUser(args.ueid) .then(data => { if (!data.result) { // Display an alert containing the error message Notification.alert(data.errors[0].key, data.errors[0].message); return data; } // Dismiss the modal. modal.hide(); modal.destroy(); return data; }) .then(() => { DynamicTable.refreshTableContent(getDynamicTableFromLink(clickedLink)) .catch(Notification.exception); return Str.get_string('unenrolleduser', 'core_enrol', userData); }) .then(notificationString => { notifyUser(notificationString); return; }) .catch(Notification.exception); }; /** * Get the body fragment. * * @param {Number} contextId * @param {Number} ueid The user enrolment id * @param {Object} formdata * @returns {Promise} */ const getBody = (contextId, ueid, formdata = null) => Fragment.loadFragment( 'enrol', 'user_enrolment_form', contextId, { ueid, formdata, } ); /** * Initialise the statu field handler. * * @param {object} param * @param {Number} param.contextid * @param {Number} param.uniqueid */ export const init = ({contextid, uniqueid}) => { registerEventListeners(contextid, uniqueid); }; amd/src/participants.js 0000644 00000017377 15151162244 0011153 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Some UI stuff for participants page. * This is also used by the report/participants/index.php because it has the same functionality. * * @module core_user/participants * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import * as DynamicTable from 'core_table/dynamic'; import * as Str from 'core/str'; import CheckboxToggleAll from 'core/checkbox-toggleall'; import CustomEvents from 'core/custom_interaction_events'; import DynamicTableSelectors from 'core_table/local/dynamic/selectors'; import ModalEvents from 'core/modal_events'; import Notification from 'core/notification'; import Pending from 'core/pending'; import jQuery from 'jquery'; import {showAddNote, showSendMessage} from 'core_user/local/participants/bulkactions'; import 'core/inplace_editable'; const Selectors = { bulkActionSelect: "#formactionid", bulkUserSelectedCheckBoxes: "input[data-togglegroup='participants-table'][data-toggle='slave']:checked", checkCountButton: "#checkall", showCountText: '[data-region="participant-count"]', showCountToggle: '[data-action="showcount"]', stateHelpIcon: '[data-region="state-help-icon"]', tableForm: uniqueId => `form[data-table-unique-id="${uniqueId}"]`, }; export const init = ({ uniqueid, noteStateNames = {}, }) => { const root = document.querySelector(Selectors.tableForm(uniqueid)); const getTableFromUniqueId = uniqueId => root.querySelector(DynamicTableSelectors.main.fromRegionId(uniqueId)); /** * Private method. * * @method registerEventListeners * @private */ const registerEventListeners = () => { CustomEvents.define(Selectors.bulkActionSelect, [CustomEvents.events.accessibleChange]); jQuery(Selectors.bulkActionSelect).on(CustomEvents.events.accessibleChange, e => { const bulkActionSelect = e.target.closest('select'); const action = bulkActionSelect.value; const tableRoot = getTableFromUniqueId(uniqueid); const checkboxes = tableRoot.querySelectorAll(Selectors.bulkUserSelectedCheckBoxes); const pendingPromise = new Pending('core_user/participants:bulkActionSelect'); if (action.indexOf('#') !== -1) { e.preventDefault(); const ids = []; checkboxes.forEach(checkbox => { ids.push(checkbox.getAttribute('name').replace('user', '')); }); let bulkAction; if (action === '#messageselect') { bulkAction = showSendMessage(ids); } else if (action === '#addgroupnote') { bulkAction = showAddNote( root.dataset.courseId, ids, noteStateNames, root.querySelector(Selectors.stateHelpIcon) ); } if (bulkAction) { const pendingBulkAction = new Pending('core_user/participants:bulkActionSelected'); bulkAction .then(modal => { modal.getRoot().on(ModalEvents.hidden, () => { // Focus on the action select when the dialog is closed. bulkActionSelect.focus(); }); pendingBulkAction.resolve(); return modal; }) .catch(Notification.exception); } } else if (action !== '' && checkboxes.length) { bulkActionSelect.form.submit(); } resetBulkAction(bulkActionSelect); pendingPromise.resolve(); }); root.addEventListener('click', e => { // Handle clicking of the "Select all" actions. const checkCountButton = root.querySelector(Selectors.checkCountButton); const checkCountButtonClicked = checkCountButton && checkCountButton.contains(e.target); if (checkCountButtonClicked) { e.preventDefault(); const tableRoot = getTableFromUniqueId(uniqueid); DynamicTable.setPageSize(tableRoot, checkCountButton.dataset.targetPageSize) .then(tableRoot => { // Update the toggle state. CheckboxToggleAll.setGroupState(root, 'participants-table', true); return tableRoot; }) .catch(Notification.exception); } }); // When the content is refreshed, update the row counts in various places. root.addEventListener(DynamicTable.Events.tableContentRefreshed, e => { const checkCountButton = root.querySelector(Selectors.checkCountButton); const tableRoot = e.target; const defaultPageSize = parseInt(tableRoot.dataset.tableDefaultPerPage, 10); const currentPageSize = parseInt(tableRoot.dataset.tablePageSize, 10); const totalRowCount = parseInt(tableRoot.dataset.tableTotalRows, 10); CheckboxToggleAll.updateSlavesFromMasterState(root, 'participants-table'); const pageCountStrings = [ { key: 'countparticipantsfound', component: 'core_user', param: totalRowCount, }, ]; if (totalRowCount <= defaultPageSize) { if (checkCountButton) { checkCountButton.classList.add('hidden'); } } else if (totalRowCount <= currentPageSize) { // The are fewer than the current page size. pageCountStrings.push({ key: 'selectalluserswithcount', component: 'core', param: defaultPageSize, }); if (checkCountButton) { // The 'Check all [x]' button is only visible when there are values to set. checkCountButton.classList.add('hidden'); } } else { pageCountStrings.push({ key: 'selectalluserswithcount', component: 'core', param: totalRowCount, }); if (checkCountButton) { checkCountButton.classList.remove('hidden'); } } Str.get_strings(pageCountStrings) .then(([showingParticipantCountString, selectCountString]) => { const showingParticipantCount = root.querySelector(Selectors.showCountText); showingParticipantCount.innerHTML = showingParticipantCountString; if (selectCountString && checkCountButton) { checkCountButton.value = selectCountString; } return; }) .catch(Notification.exception); }); }; const resetBulkAction = bulkActionSelect => { bulkActionSelect.value = ''; }; registerEventListeners(); }; amd/src/repository.js 0000644 00000003641 15151162244 0010656 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Module to handle AJAX interactions. * * @module core_user/repository * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {call as fetchMany} from 'core/ajax'; /** * Unenrol the user with the specified user enrolmentid ID. * * @param {Number} userEnrolmentId * @return {Promise} */ export const unenrolUser = userEnrolmentId => { return fetchMany([{ methodname: 'core_enrol_unenrol_user_enrolment', args: { ueid: userEnrolmentId, }, }])[0]; }; /** * Submit the user enrolment form with the specified form data. * * @param {String} formdata * @return {Promise} */ export const submitUserEnrolmentForm = formdata => { return fetchMany([{ methodname: 'core_enrol_submit_user_enrolment_form', args: { formdata, }, }])[0]; }; export const createNotesForUsers = notes => { return fetchMany([{ methodname: 'core_notes_create_notes', args: { notes } }])[0]; }; export const sendMessagesToUsers = messages => { return fetchMany([{ methodname: 'core_message_send_instant_messages', args: {messages} }])[0]; }; amd/src/form_user_selector.js 0000644 00000005443 15151162244 0012342 0 ustar 00 // This file is part of Moodle - https://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Provides the required functionality for an autocomplete element to select a user. * * @module core_user/form_user_selector * @copyright 2020 David Mudrák <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Ajax from 'core/ajax'; import {render as renderTemplate} from 'core/templates'; import {get_string as getString} from 'core/str'; /** * Load the list of users matching the query and render the selector labels for them. * * @param {String} selector The selector of the auto complete element. * @param {String} query The query string. * @param {Function} callback A callback function receiving an array of results. * @param {Function} failure A function to call in case of failure, receiving the error message. */ export async function transport(selector, query, callback, failure) { const request = { methodname: 'core_user_search_identity', args: { query: query } }; try { const response = await Ajax.call([request])[0]; if (response.overflow) { const msg = await getString('toomanyuserstoshow', 'core', '>' + response.maxusersperpage); callback(msg); } else { let labels = []; response.list.forEach(user => { labels.push(renderTemplate('core_user/form_user_selector_suggestion', user)); }); labels = await Promise.all(labels); response.list.forEach((user, index) => { user.label = labels[index]; }); callback(response.list); } } catch (e) { failure(e); } } /** * Process the results for auto complete elements. * * @param {String} selector The selector of the auto complete element. * @param {Array} results An array or results returned by {@see transport()}. * @return {Array} New array of the selector options. */ export function processResults(selector, results) { if (!Array.isArray(results)) { return results; } else { return results.map(result => ({value: result.id, label: result.label})); } } amd/src/private_files.js 0000644 00000004507 15151162244 0011275 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Module to handle AJAX interactions with user private files * * @module core_user/private_files * @copyright 2020 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import DynamicForm from 'core_form/dynamicform'; import ModalForm from 'core_form/modalform'; import {get_string as getString} from 'core/str'; import {add as addToast} from 'core/toast'; /** * Initialize private files form as AJAX form * * @param {String} containerSelector * @param {String} formClass */ export const initDynamicForm = (containerSelector, formClass) => { const form = new DynamicForm(document.querySelector(containerSelector), formClass); // When form is saved, refresh it to remove validation errors, if any: form.addEventListener(form.events.FORM_SUBMITTED, () => { form.load(); getString('changessaved') .then(addToast) .catch(null); }); // Reload the page on cancel. form.addEventListener(form.events.CANCEL_BUTTON_PRESSED, () => window.location.reload()); }; /** * Initialize private files form as Modal form * * @param {String} elementSelector * @param {String} formClass */ export const initModal = (elementSelector, formClass) => { document.querySelector(elementSelector).addEventListener('click', function(e) { e.preventDefault(); const form = new ModalForm({ formClass, args: {nosubmit: true}, modalConfig: {title: getString('privatefilesmanage')}, returnFocus: e.target, }); form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload()); form.show(); }); }; amd/src/participants_filter.js 0000644 00000010206 15151162244 0012500 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Participants filter management. * * @module core_user/participants_filter * @copyright 2021 Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import CoreFilter from 'core/datafilter'; import * as DynamicTable from 'core_table/dynamic'; import Selectors from 'core/datafilter/selectors'; import Notification from 'core/notification'; import Pending from 'core/pending'; /** * Initialise the participants filter on the element with the given id. * * @param {String} filterRegionId The id for the filter element. */ export const init = filterRegionId => { const filterSet = document.getElementById(filterRegionId); // Create and initialize filter. const coreFilter = new CoreFilter(filterSet, function(filters, pendingPromise) { DynamicTable.setFilters( DynamicTable.getTableFromId(filterSet.dataset.tableRegion), { jointype: parseInt(filterSet.querySelector(Selectors.filterset.fields.join).value, 10), filters, } ) .then(result => { pendingPromise.resolve(); return result; }) .catch(Notification.exception); }); coreFilter.init(); /** * Set the current filter options based on a provided configuration. * * @param {Object} config * @param {Number} config.jointype * @param {Object} config.filters * @returns {Promise} */ const setFilterFromConfig = config => { const filterConfig = Object.entries(config.filters); if (!filterConfig.length) { // There are no filters to set from. return Promise.resolve(); } // Set the main join type. filterSet.querySelector(Selectors.filterset.fields.join).value = config.jointype; const filterPromises = filterConfig.map(([filterType, filterData]) => { if (filterType === 'courseid') { // The courseid is a special case. return false; } const filterValues = filterData.values; if (!filterValues.length) { // There are no values for this filter. // Skip it. return false; } return coreFilter.addFilterRow() .then(([filterRow]) => { coreFilter.addFilter(filterRow, filterType, filterValues); return; }); }).filter(promise => promise); if (!filterPromises.length) { return Promise.resolve(); } return Promise.all(filterPromises) .then(() => { return coreFilter.removeEmptyFilters(); }) .then(() => { coreFilter.updateFiltersOptions(); return; }) .then(() => { coreFilter.updateTableFromFilter(); return; }); }; // Initialize DynamicTable for showing result. const tableRoot = DynamicTable.getTableFromId(filterSet.dataset.tableRegion); const initialFilters = DynamicTable.getFilters(tableRoot); if (initialFilters) { const initialFilterPromise = new Pending('core/filter:setFilterFromConfig'); // Apply the initial filter configuration. setFilterFromConfig(initialFilters) .then(() => initialFilterPromise.resolve()) .catch(); } }; amd/src/edit_profile_fields.js 0000644 00000006270 15151162244 0012433 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. import ModalForm from 'core_form/modalform'; import {get_string as getString} from 'core/str'; /** * User profile fields editor * * @module core_user/edit_profile_fields * @copyright 2021 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const Selectors = { actions: { editCategory: '[data-action="editcategory"]', editField: '[data-action="editfield"]', createField: '[data-action="createfield"]', }, }; export const init = () => { document.addEventListener('click', function(e) { let element = e.target.closest(Selectors.actions.editCategory); if (element) { e.preventDefault(); const title = element.getAttribute('data-id') ? getString('profileeditcategory', 'admin', element.getAttribute('data-name')) : getString('profilecreatenewcategory', 'admin'); const form = new ModalForm({ formClass: 'core_user\\form\\profile_category_form', args: {id: element.getAttribute('data-id')}, modalConfig: {title}, returnFocus: element, }); form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload()); form.show(); } element = e.target.closest(Selectors.actions.editField); if (element) { e.preventDefault(); const form = new ModalForm({ formClass: 'core_user\\form\\profile_field_form', args: {id: element.getAttribute('data-id')}, modalConfig: {title: getString('profileeditfield', 'admin', element.getAttribute('data-name'))}, returnFocus: element, }); form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload()); form.show(); } element = e.target.closest(Selectors.actions.createField); if (element) { e.preventDefault(); const form = new ModalForm({ formClass: 'core_user\\form\\profile_field_form', args: {datatype: element.getAttribute('data-datatype'), categoryid: element.getAttribute('data-categoryid')}, modalConfig: {title: getString('profilecreatenewfield', 'admin', element.getAttribute('data-datatypename'))}, returnFocus: element, }); form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload()); form.show(); } }); }; amd/src/local/participants/bulkactions.js 0000644 00000013042 15151162244 0014544 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Bulk actions for lists of participants. * * @module core_user/local/participants/bulkactions * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import * as Repository from 'core_user/repository'; import * as Str from 'core/str'; import ModalEvents from 'core/modal_events'; import ModalFactory from 'core/modal_factory'; import Notification from 'core/notification'; import Templates from 'core/templates'; import {add as notifyUser} from 'core/toast'; /** * Show the add note popup * * @param {Number} courseid * @param {Number[]} users * @param {String[]} noteStateNames * @param {HTMLElement} stateHelpIcon * @return {Promise} */ export const showAddNote = (courseid, users, noteStateNames, stateHelpIcon) => { if (!users.length) { // No users were selected. return Promise.resolve(); } const states = []; for (let key in noteStateNames) { switch (key) { case 'draft': states.push({value: 'personal', label: noteStateNames[key]}); break; case 'public': states.push({value: 'course', label: noteStateNames[key], selected: 1}); break; case 'site': states.push({value: key, label: noteStateNames[key]}); break; } } const context = { stateNames: states, stateHelpIcon: stateHelpIcon.innerHTML, }; let titlePromise = null; if (users.length === 1) { titlePromise = Str.get_string('addbulknotesingle', 'core_notes'); } else { titlePromise = Str.get_string('addbulknote', 'core_notes', users.length); } return ModalFactory.create({ type: ModalFactory.types.SAVE_CANCEL, body: Templates.render('core_user/add_bulk_note', context), title: titlePromise, buttons: { save: titlePromise, }, removeOnClose: true, }) .then(modal => { modal.getRoot().on(ModalEvents.save, () => submitAddNote(courseid, users, modal)); modal.show(); return modal; }); }; /** * Add a note to this list of users. * * @param {Number} courseid * @param {Number[]} users * @param {Modal} modal * @return {Promise} */ const submitAddNote = (courseid, users, modal) => { const text = modal.getRoot().find('form textarea').val(); const publishstate = modal.getRoot().find('form select').val(); const notes = users.map(userid => { return { userid, text, courseid, publishstate, }; }); return Repository.createNotesForUsers(notes) .then(noteIds => { if (noteIds.length === 1) { return Str.get_string('addbulknotedonesingle', 'core_notes'); } else { return Str.get_string('addbulknotedone', 'core_notes', noteIds.length); } }) .then(msg => notifyUser(msg)) .catch(Notification.exception); }; /** * Show the send message popup. * * @param {Number[]} users * @return {Promise} */ export const showSendMessage = users => { if (!users.length) { // Nothing to do. return Promise.resolve(); } let titlePromise; if (users.length === 1) { titlePromise = Str.get_string('sendbulkmessagesingle', 'core_message'); } else { titlePromise = Str.get_string('sendbulkmessage', 'core_message', users.length); } return ModalFactory.create({ type: ModalFactory.types.SAVE_CANCEL, body: Templates.render('core_user/send_bulk_message', {}), title: titlePromise, buttons: { save: titlePromise, }, removeOnClose: true, }) .then(modal => { modal.getRoot().on(ModalEvents.save, (e) => { const text = modal.getRoot().find('form textarea').val(); if (text.trim() === '') { modal.getRoot().find('[data-role="messagetextrequired"]').removeAttr('hidden'); e.preventDefault(); return; } submitSendMessage(modal, users, text); }); modal.show(); return modal; }); }; /** * Send a message to these users. * * @param {Modal} modal * @param {Number[]} users * @param {String} text * @return {Promise} */ const submitSendMessage = (modal, users, text) => { const messages = users.map(touserid => { return { touserid, text, }; }); return Repository.sendMessagesToUsers(messages) .then(messageIds => { if (messageIds.length == 1) { return Str.get_string('sendbulkmessagesentsingle', 'core_message'); } else { return Str.get_string('sendbulkmessagesent', 'core_message', messageIds.length); } }) .then(msg => notifyUser(msg)) .catch(Notification.exception); }; policy.php 0000644 00000006417 15151162244 0006565 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file is part of the User section Moodle * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ // Do not check for the site policies in require_login() to avoid the redirect loop. define('NO_SITEPOLICY_CHECK', true); require_once('../config.php'); require_once($CFG->libdir.'/filelib.php'); require_once($CFG->libdir.'/resourcelib.php'); $agree = optional_param('agree', 0, PARAM_BOOL); $PAGE->set_url('/user/policy.php'); $PAGE->set_popup_notification_allowed(false); if (!isloggedin()) { require_login(); } if (!empty($SESSION->wantsurl)) { $return = $SESSION->wantsurl; } else { $return = $CFG->wwwroot.'/'; } $sitepolicymanager = new \core_privacy\local\sitepolicy\manager(); if (!empty($CFG->sitepolicyhandler)) { // We are on the wrong page, site policies are managed by somebody else. if ($sitepolicyurl = $sitepolicymanager->get_redirect_url(isguestuser())) { redirect($sitepolicyurl); } else { redirect($return); } } $sitepolicy = $sitepolicymanager->get_embed_url(isguestuser()); if (empty($sitepolicy)) { // Nothing to agree to, sorry, hopefully we will not get to infinite loop. redirect($return); } if ($agree and confirm_sesskey()) { // User has agreed. $sitepolicymanager->accept(); unset($SESSION->wantsurl); redirect($return); } $strpolicyagree = get_string('policyagree'); $strpolicyagreement = get_string('policyagreement'); $strpolicyagreementclick = get_string('policyagreementclick'); $PAGE->set_context(context_system::instance()); $PAGE->set_title($strpolicyagreement); $PAGE->set_heading($SITE->fullname); $PAGE->navbar->add($strpolicyagreement); echo $OUTPUT->header(); echo $OUTPUT->heading($strpolicyagreement); $mimetype = mimeinfo('type', $sitepolicy); if ($mimetype == 'document/unknown') { // Fallback for missing index.php, index.html. $mimetype = 'text/html'; } // We can not use our popups here, because the url may be arbitrary, see MDL-9823. $clicktoopen = '<a href="'.$sitepolicy.'" onclick="this.target=\'_blank\'">'.$strpolicyagreementclick.'</a>'; echo '<div class="noticebox">'; echo resourcelib_embed_general($sitepolicy, $strpolicyagreement, $clicktoopen, $mimetype); echo '</div>'; $formcontinue = new single_button(new moodle_url('policy.php', array('agree' => 1)), get_string('yes')); $formcancel = new single_button(new moodle_url($CFG->wwwroot.'/login/logout.php', array('agree' => 0)), get_string('no')); echo $OUTPUT->confirm($strpolicyagree, $formcontinue, $formcancel); echo $OUTPUT->footer(); emailupdate.php 0000644 00000007031 15151162244 0007551 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Change a users email address * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once('../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/user/editlib.php'); require_once($CFG->dirroot.'/user/lib.php'); $key = required_param('key', PARAM_ALPHANUM); $id = required_param('id', PARAM_INT); $PAGE->set_url('/user/emailupdate.php', array('id' => $id, 'key' => $key)); $PAGE->set_context(context_system::instance()); if (!$user = $DB->get_record('user', array('id' => $id))) { throw new \moodle_exception('invaliduserid'); } $preferences = get_user_preferences(null, null, $user->id); $a = new stdClass(); $a->fullname = fullname($user, true); $stremailupdate = get_string('emailupdate', 'auth', $a); $PAGE->set_title(format_string($SITE->fullname) . ": $stremailupdate"); $PAGE->set_heading(format_string($SITE->fullname) . ": $stremailupdate"); if (empty($preferences['newemailattemptsleft'])) { redirect("$CFG->wwwroot/user/view.php?id=$user->id"); } else if ($preferences['newemailattemptsleft'] < 1) { cancel_email_update($user->id); echo $OUTPUT->header(); echo $OUTPUT->box(get_string('auth_outofnewemailupdateattempts', 'auth'), 'center'); echo $OUTPUT->footer(); } else if ($key == $preferences['newemailkey']) { $olduser = clone($user); cancel_email_update($user->id); $user->email = $preferences['newemail']; // Detect duplicate before saving. if (empty($CFG->allowaccountssameemail)) { // Make a case-insensitive query for the given email address. $select = $DB->sql_equal('email', ':email', false) . ' AND mnethostid = :mnethostid AND id <> :userid'; $params = array( 'email' => $user->email, 'mnethostid' => $CFG->mnet_localhost_id, 'userid' => $user->id ); // If there are other user(s) that already have the same email, cancel and redirect. if ($DB->record_exists_select('user', $select, $params)) { redirect(new moodle_url('/user/view.php', ['id' => $user->id]), get_string('emailnowexists', 'auth')); } } // Update user email. $authplugin = get_auth_plugin($user->auth); $authplugin->user_update($olduser, $user); user_update_user($user, false); $a->email = $user->email; redirect( new moodle_url('/user/view.php', ['id' => $user->id]), get_string('emailupdatesuccess', 'auth', $a), null, \core\output\notification::NOTIFY_SUCCESS ); } else { $preferences['newemailattemptsleft']--; set_user_preference('newemailattemptsleft', $preferences['newemailattemptsleft'], $user->id); echo $OUTPUT->header(); echo $OUTPUT->box(get_string('auth_invalidnewemailkey', 'auth'), 'center'); echo $OUTPUT->footer(); } portfoliologs.php 0000644 00000013317 15151162244 0010165 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file is part of the User section Moodle * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once(__DIR__ . '/../config.php'); if (empty($CFG->enableportfolios)) { throw new \moodle_exception('disabled', 'portfolio'); } require_once($CFG->libdir . '/portfoliolib.php'); require_once($CFG->libdir . '/portfolio/exporter.php'); $courseid = optional_param('courseid', SITEID, PARAM_INT); $page = optional_param('page', 0, PARAM_INT); $perpage = optional_param('perpage', 10, PARAM_INT); if (! $course = $DB->get_record("course", array("id" => $courseid))) { throw new \moodle_exception('invalidcourseid'); } require_login($course, false); $user = $USER; $fullname = fullname($user); $strportfolios = get_string('portfolios', 'portfolio'); $url = new moodle_url('/user/portfoliologs.php', array('courseid' => $courseid)); navigation_node::override_active_url(new moodle_url('/user/portfoliologs.php', array('courseid' => $courseid))); if ($page !== 0) { $url->param('page', $page); } if ($perpage !== 0) { $url->param('perpage', $perpage); } $PAGE->set_url($url); $PAGE->set_title(get_string('logs', 'portfolio')); $PAGE->set_heading($fullname); $PAGE->set_context(context_user::instance($user->id)); $PAGE->set_pagelayout('report'); echo $OUTPUT->header(); $showroles = 1; $somethingprinted = false; echo $OUTPUT->box_start(); $queued = $DB->get_records('portfolio_tempdata', array('userid' => $USER->id), 'expirytime DESC', 'id, expirytime'); if (count($queued) > 0) { $table = new html_table(); $table->head = array( get_string('displayarea', 'portfolio'), get_string('plugin', 'portfolio'), get_string('displayinfo', 'portfolio'), get_string('displayexpiry', 'portfolio'), '', ); $table->data = array(); $now = time(); foreach ($queued as $q) { $e = portfolio_exporter::rewaken_object($q->id); $e->verify_rewaken(true); $queued = $e->get('queued'); $baseurl = new moodle_url('/portfolio/add.php', array('id' => $q->id, 'logreturn' => 1, 'sesskey' => sesskey())); $iconstr = $OUTPUT->action_icon(new moodle_url($baseurl, array('cancel' => 1)), new pix_icon('t/stop', get_string('cancel'))); if (!$e->get('queued') && $e->get('expirytime') > $now) { $iconstr .= $OUTPUT->action_icon($baseurl, new pix_icon('t/go', get_string('continue'))); } $table->data[] = array( $e->get('caller')->display_name(), (($e->get('instance')) ? $e->get('instance')->get('name') : get_string('noinstanceyet', 'portfolio')), $e->get('caller')->heading_summary(), userdate($q->expirytime), $iconstr, ); unset($e); // This could potentially be quite big, so free it. } echo $OUTPUT->heading(get_string('queuesummary', 'portfolio')); echo html_writer::table($table); $somethingprinted = true; } // Paging - get total count separately. $logcount = $DB->count_records('portfolio_log', array('userid' => $USER->id)); if ($logcount > 0) { $table = new html_table(); $table->head = array( get_string('plugin', 'portfolio'), get_string('displayarea', 'portfolio'), get_string('transfertime', 'portfolio'), ); $logs = $DB->get_records('portfolio_log', array('userid' => $USER->id), 'time DESC', '*', ($page * $perpage), $perpage); foreach ($logs as $log) { if (!empty($log->caller_file)) { portfolio_include_callback_file($log->caller_file); } else if (!empty($log->caller_component)) { portfolio_include_callback_file($log->caller_component); } else { // Errrmahgerrrd - this should never happen. Skipping. continue; } $class = $log->caller_class; $pluginname = ''; try { $plugin = portfolio_instance($log->portfolio); $url = $plugin->resolve_static_continue_url($log->continueurl); if ($url) { $pluginname = '<a href="' . $url . '">' . $plugin->get('name') . '</a>'; } else { $pluginname = $plugin->get('name'); } } catch (portfolio_exception $e) { // May have been deleted. $pluginname = get_string('unknownplugin', 'portfolio'); } $table->data[] = array( $pluginname, '<a href="' . $log->returnurl . '">' . call_user_func(array($class, 'display_name')) . '</a>', userdate($log->time), ); } echo $OUTPUT->heading(get_string('logsummary', 'portfolio')); $pagingbar = new paging_bar($logcount, $page, $perpage, $CFG->wwwroot . '/user/portfoliologs.php?'); echo $OUTPUT->render($pagingbar); echo html_writer::table($table); echo $OUTPUT->render($pagingbar); $somethingprinted = true; } if (!$somethingprinted) { echo $OUTPUT->heading($strportfolios); echo get_string('nologs', 'portfolio'); } echo $OUTPUT->box_end(); echo $OUTPUT->footer(); filters/profilefield.php 0000644 00000021161 15151162244 0011373 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Profile field filter. * * @package core_user * @category user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/user/filters/lib.php'); /** * User filter based on values of custom profile fields. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_profilefield extends user_filter_type { /** * Constructor * @param string $name the name of the filter instance * @param string $label the label of the filter instance * @param boolean $advanced advanced form element flag */ public function __construct($name, $label, $advanced) { parent::__construct($name, $label, $advanced); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function user_filter_profilefield($name, $label, $advanced) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($name, $label, $advanced); } /** * Returns an array of comparison operators * @return array of comparison operators */ public function get_operators() { return array(0 => get_string('contains', 'filters'), 1 => get_string('doesnotcontain', 'filters'), 2 => get_string('isequalto', 'filters'), 3 => get_string('startswith', 'filters'), 4 => get_string('endswith', 'filters'), 5 => get_string('isempty', 'filters'), 6 => get_string('isnotdefined', 'filters'), 7 => get_string('isdefined', 'filters')); } /** * Returns an array of custom profile fields * @return array of profile fields */ public function get_profile_fields() { global $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); $fieldrecords = profile_get_custom_fields(); foreach ($fieldrecords as $key => $fieldrecord) { $fieldrecords[$key]->name = format_string($fieldrecords[$key]->name, false, ['context' => context_system::instance()]); } $fields = array_combine(array_keys($fieldrecords), array_column($fieldrecords, 'name')); core_collator::asort($fields); $res = array(0 => get_string('anyfield', 'filters')); return $res + $fields; } /** * Adds controls specific to this filter in the form. * @param object $mform a MoodleForm object to setup */ public function setupForm(&$mform) { $profilefields = $this->get_profile_fields(); if (empty($profilefields)) { return; } $objs = array(); $objs['field'] = $mform->createElement('select', $this->_name.'_fld', null, $profilefields); $objs['op'] = $mform->createElement('select', $this->_name.'_op', null, $this->get_operators()); $objs['value'] = $mform->createElement('text', $this->_name, null); $objs['field']->setLabel(get_string('profilefilterfield', 'filters')); $objs['op']->setLabel(get_string('profilefilterlimiter', 'filters')); $objs['value']->setLabel(get_string('valuefor', 'filters', $this->_label)); $grp =& $mform->addElement('group', $this->_name.'_grp', $this->_label, $objs, '', false); $mform->setType($this->_name, PARAM_RAW); if ($this->_advanced) { $mform->setAdvanced($this->_name.'_grp'); } } /** * Retrieves data from the form data * @param object $formdata data submited with the form * @return mixed array filter data or false when filter not set */ public function check_data($formdata) { $profilefields = $this->get_profile_fields(); if (empty($profilefields)) { return false; } $field = $this->_name; $operator = $field.'_op'; $profile = $field.'_fld'; if (property_exists($formdata, $profile)) { if ($formdata->$operator < 5 and $formdata->$field === '') { return false; } return array('value' => (string)$formdata->$field, 'operator' => (int)$formdata->$operator, 'profile' => (int)$formdata->$profile); } } /** * Returns the condition to be used with SQL where * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { global $CFG, $DB; static $counter = 0; $name = 'ex_profilefield'.$counter++; $profilefields = $this->get_profile_fields(); if (empty($profilefields)) { return ''; } $profile = $data['profile']; $operator = $data['operator']; $value = $data['value']; $params = array(); if (!array_key_exists($profile, $profilefields)) { return array('', array()); } $where = ""; $op = " IN "; if ($operator < 5 and $value === '') { return ''; } switch($operator) { case 0: // Contains. $where = $DB->sql_like('data', ":$name", false, false); $params[$name] = "%$value%"; break; case 1: // Does not contain. $where = $DB->sql_like('data', ":$name", false, false, true); $params[$name] = "%$value%"; break; case 2: // Equal to. $where = $DB->sql_like('data', ":$name", false, false); $params[$name] = "$value"; break; case 3: // Starts with. $where = $DB->sql_like('data', ":$name", false, false); $params[$name] = "$value%"; break; case 4: // Ends with. $where = $DB->sql_like('data', ":$name", false, false); $params[$name] = "%$value"; break; case 5: // Empty. $where = "data = :$name"; $params[$name] = ""; break; case 6: // Is not defined. $op = " NOT IN "; break; case 7: // Is defined. break; } if ($profile) { if ($where !== '') { $where = " AND $where"; } $where = "fieldid=$profile $where"; } if ($where !== '') { $where = "WHERE $where"; } return array("id $op (SELECT userid FROM {user_info_data} $where)", $params); } /** * Returns a human friendly description of the filter used as label. * @param array $data filter settings * @return string active filter label */ public function get_label($data) { $operators = $this->get_operators(); $profilefields = $this->get_profile_fields(); if (empty($profilefields)) { return ''; } $profile = $data['profile']; $operator = $data['operator']; $value = $data['value']; if (!array_key_exists($profile, $profilefields)) { return ''; } $a = new stdClass(); $a->label = $this->_label; $a->value = $value; $a->profile = $profilefields[$profile]; $a->operator = $operators[$operator]; switch($operator) { case 0: // Contains. case 1: // Doesn't contain. case 2: // Equal to. case 3: // Starts with. case 4: // Ends with. return get_string('profilelabel', 'filters', $a); case 5: // Empty. case 6: // Is not defined. case 7: // Is defined. return get_string('profilelabelnovalue', 'filters', $a); } return ''; } } filters/anycourses.php 0000644 00000003131 15151162244 0011117 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This is filter is used to see which students are enroled on any courses * * @package core_user * @copyright 2014 Krister Viirsaar * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * User filter to distinguish users with no or any enroled courses. * @copyright 2014 Krister Viirsaar * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_anycourses extends user_filter_yesno { /** * Returns the condition to be used with SQL * * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { $value = $data['value']; $not = $value ? '' : 'NOT'; return array("EXISTS ( SELECT userid FROM {user_enrolments} ) AND " . " id $not IN ( SELECT userid FROM {user_enrolments} )", array()); } } filters/courserole.php 0000644 00000015471 15151162244 0011120 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Course role filter * * @package core_user * @category user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot .'/user/filters/lib.php'); /** * User filter based on roles in a course identified by its shortname. * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_courserole extends user_filter_type { /** * Constructor * @param string $name the name of the filter instance * @param string $label the label of the filter instance * @param boolean $advanced advanced form element flag */ public function __construct($name, $label, $advanced) { parent::__construct($name, $label, $advanced); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function user_filter_courserole($name, $label, $advanced) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($name, $label, $advanced); } /** * Returns an array of available roles * @return array of availble roles */ public function get_roles() { $context = context_system::instance(); $roles = array(0 => get_string('anyrole', 'filters')) + get_default_enrol_roles($context); return $roles; } /** * Returns an array of course categories * @return array of course categories */ public function get_course_categories() { return array(0 => get_string('anycategory', 'filters')) + core_course_category::make_categories_list(); } /** * Adds controls specific to this filter in the form. * @param moodleform $mform a MoodleForm object to setup */ public function setupForm(&$mform) { $objs = array(); $objs['role'] = $mform->createElement('select', $this->_name .'_rl', null, $this->get_roles()); $objs['role']->setLabel(get_string('courserole', 'filters')); $objs['category'] = $mform->createElement('select', $this->_name .'_ct', null, $this->get_course_categories()); $objs['category']->setLabel(get_string('coursecategory', 'filters')); $objs['value'] = $mform->createElement('text', $this->_name, null); $objs['value']->setLabel(get_string('coursevalue', 'filters')); $grp =& $mform->addElement('group', $this->_name.'_grp', $this->_label, $objs, '', false); $mform->setType($this->_name, PARAM_TEXT); if ($this->_advanced) { $mform->setAdvanced($this->_name.'_grp'); } } /** * Retrieves data from the form data * @param stdClass $formdata data submited with the form * @return mixed array filter data or false when filter not set */ public function check_data($formdata) { $field = $this->_name; $role = $field .'_rl'; $category = $field .'_ct'; if (property_exists($formdata, $field)) { if (empty($formdata->$field) and empty($formdata->$role) and empty($formdata->$category)) { // Nothing selected. return false; } return array('value' => (string)$formdata->$field, 'roleid' => (int)$formdata->$role, 'categoryid' => (int)$formdata->$category); } return false; } /** * Returns the condition to be used with SQL where * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { global $CFG, $DB; static $counter = 0; $pref = 'ex_courserole'.($counter++).'_'; $value = $data['value']; $roleid = $data['roleid']; $categoryid = $data['categoryid']; $params = array(); if (empty($value) and empty($roleid) and empty($categoryid)) { return array('', $params); } $where = "b.contextlevel=50"; if ($roleid) { $where .= " AND a.roleid = :{$pref}roleid"; $params[$pref.'roleid'] = $roleid; } if ($categoryid) { $where .= " AND c.category = :{$pref}categoryid"; $params[$pref.'categoryid'] = $categoryid; } if ($value) { $where .= " AND c.shortname = :{$pref}course"; $params[$pref.'course'] = $value; } return array("id IN (SELECT userid FROM {role_assignments} a INNER JOIN {context} b ON a.contextid=b.id INNER JOIN {course} c ON b.instanceid=c.id WHERE $where)", $params); } /** * Returns a human friendly description of the filter used as label. * @param array $data filter settings * @return string active filter label */ public function get_label($data) { global $DB; $value = $data['value']; $roleid = $data['roleid']; $categoryid = $data['categoryid']; $a = new stdClass(); $a->label = $this->_label; if ($roleid) { $role = $DB->get_record('role', array('id' => $roleid)); $a->rolename = '"'.role_get_name($role).'"'; } else { $a->rolename = get_string('anyrole', 'filters'); } if ($categoryid) { $catname = $DB->get_field('course_categories', 'name', array('id' => $categoryid)); $a->categoryname = '"'.format_string($catname).'"'; } else { $a->categoryname = get_string('anycategory', 'filters'); } if ($value) { $a->coursename = '"'.s($value).'"'; if (!$DB->record_exists('course', array('shortname' => $value))) { return '<span class="notifyproblem">'.get_string('courserolelabelerror', 'filters', $a).'</span>'; } } else { $a->coursename = get_string('anycourse', 'filters'); } return get_string('courserolelabel', 'filters', $a); } } filters/text.php 0000644 00000015657 15151162244 0007730 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Text field filter * * @package core_user * @category user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/user/filters/lib.php'); /** * Generic filter for text fields. * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_text extends user_filter_type { /** @var string */ public $_field; /** * Constructor * @param string $name the name of the filter instance * @param string $label the label of the filter instance * @param boolean $advanced advanced form element flag * @param string $field user table filed name */ public function __construct($name, $label, $advanced, $field) { parent::__construct($name, $label, $advanced); $this->_field = $field; } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function user_filter_text($name, $label, $advanced, $field) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($name, $label, $advanced, $field); } /** * Returns an array of comparison operators * @return array of comparison operators */ public function getOperators() { return array(0 => get_string('contains', 'filters'), 1 => get_string('doesnotcontain', 'filters'), 2 => get_string('isequalto', 'filters'), 3 => get_string('startswith', 'filters'), 4 => get_string('endswith', 'filters'), 5 => get_string('isempty', 'filters')); } /** * Adds controls specific to this filter in the form. * @param object $mform a MoodleForm object to setup */ public function setupForm(&$mform) { $objs = array(); $objs['select'] = $mform->createElement('select', $this->_name.'_op', null, $this->getOperators()); $objs['text'] = $mform->createElement('text', $this->_name, null); $objs['select']->setLabel(get_string('limiterfor', 'filters', $this->_label)); $objs['text']->setLabel(get_string('valuefor', 'filters', $this->_label)); $grp =& $mform->addElement('group', $this->_name.'_grp', $this->_label, $objs, '', false); $mform->setType($this->_name, PARAM_RAW); $mform->disabledIf($this->_name, $this->_name.'_op', 'eq', 5); if ($this->_advanced) { $mform->setAdvanced($this->_name.'_grp'); } } /** * Retrieves data from the form data * @param object $formdata data submited with the form * @return mixed array filter data or false when filter not set */ public function check_data($formdata) { $field = $this->_name; $operator = $field.'_op'; if (property_exists($formdata, $operator)) { if ($formdata->$operator != 5 and $formdata->$field == '') { // No data - no change except for empty filter. return false; } // If field value is set then use it, else it's null. $fieldvalue = null; if (isset($formdata->$field)) { $fieldvalue = $formdata->$field; // If we aren't doing a whitespace comparison, an exact match, trim will give us a better result set. $trimmed = trim($fieldvalue); if ($trimmed !== '' && $formdata->$operator != 2) { $fieldvalue = $trimmed; } } return array('operator' => (int)$formdata->$operator, 'value' => $fieldvalue); } return false; } /** * Returns the condition to be used with SQL where * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { global $DB; static $counter = 0; $name = 'ex_text'.$counter++; $operator = $data['operator']; $value = $data['value']; $field = $this->_field; $params = array(); if ($operator != 5 and $value === '') { return ''; } switch($operator) { case 0: // Contains. $res = $DB->sql_like($field, ":$name", false, false); $params[$name] = "%$value%"; break; case 1: // Does not contain. $res = $DB->sql_like($field, ":$name", false, false, true); $params[$name] = "%$value%"; break; case 2: // Equal to. $res = $DB->sql_like($field, ":$name", false, false); $params[$name] = "$value"; break; case 3: // Starts with. $res = $DB->sql_like($field, ":$name", false, false); $params[$name] = "$value%"; break; case 4: // Ends with. $res = $DB->sql_like($field, ":$name", false, false); $params[$name] = "%$value"; break; case 5: // Empty. $res = "$field = :$name"; $params[$name] = ''; break; default: return ''; } return array($res, $params); } /** * Returns a human friendly description of the filter used as label. * @param array $data filter settings * @return string active filter label */ public function get_label($data) { $operator = $data['operator']; $value = $data['value']; $operators = $this->getOperators(); $a = new stdClass(); $a->label = $this->_label; $a->value = '"'.s($value).'"'; $a->operator = $operators[$operator]; switch ($operator) { case 0: // Contains. case 1: // Doesn't contain. case 2: // Equal to. case 3: // Starts with. case 4: // Ends with. return get_string('textlabel', 'filters', $a); case 5: // Empty. return get_string('textlabelnovalue', 'filters', $a); } return ''; } } filters/yesno.php 0000644 00000004607 15151162244 0010072 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Yes/No (boolean) filter. * * @package core_user * @category user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Generic yes/no filter with radio buttons for integer fields. * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_yesno extends user_filter_simpleselect { /** * Constructor * @param string $name the name of the filter instance * @param string $label the label of the filter instance * @param boolean $advanced advanced form element flag * @param string $field user table filed name */ public function __construct($name, $label, $advanced, $field) { parent::__construct($name, $label, $advanced, $field, array(0 => get_string('no'), 1 => get_string('yes'))); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function user_filter_yesno($name, $label, $advanced, $field) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($name, $label, $advanced, $field); } /** * Returns the condition to be used with SQL * * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { static $counter = 0; $name = 'ex_yesno'.$counter++; $value = $data['value']; $field = $this->_field; if ($value == '') { return array(); } return array("$field=:$name", array($name => $value)); } } filters/checkbox.php 0000644 00000011316 15151162244 0010516 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Generic checkbox filter. * * This will create generic filter with checkbox option and can be used for * disabling other elements for specific condition. * * @package core_user * @category user * @copyright 2011 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/user/filters/lib.php'); /** * Generic filter based for checkbox and can be used for disabling items * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_checkbox extends user_filter_type { /** * list of all the fields which needs to be disabled, if checkbox is checked * @var array */ protected $disableelements = array(); /** * name of user table field/fields on which data needs to be compared * @var mixed */ protected $field; /** * Constructor, initalize user_filter_type and sets $disableelements array * with list of elements to be diabled by checkbox. * * @param string $name the name of the filter instance * @param string $label the label of the filter instance * @param boolean $advanced advanced form element flag * @param mixed $field user table field/fields name for comparison * @param array $disableelements name of fields which should be disabled if this checkbox is checked. */ public function __construct($name, $label, $advanced, $field, $disableelements=null) { parent::__construct($name, $label, $advanced); $this->field = $field; if (!empty($disableelements)) { if (!is_array($disableelements)) { $this->disableelements = array($disableelements); } else { $this->disableelements = $disableelements; } } } /** * Adds controls specific to this filter in the form. * * @param moodleform $mform a MoodleQuickForm object in which element will be added */ public function setupForm(&$mform) { $mform->addElement('checkbox', $this->_name, $this->_label, ''); if ($this->_advanced) { $mform->setAdvanced($this->_name); } // Check if disable if options are set. if yes then set rules. if (!empty($this->disableelements) && is_array($this->disableelements)) { foreach ($this->disableelements as $disableelement) { $mform->disabledIf($disableelement, $this->_name, 'checked'); } } } /** * Retrieves data from the form data * * @param object $formdata data submited with the form * @return mixed array filter data or false when filter not set */ public function check_data($formdata) { $field = $this->_name; // Check if disable if options are set. if yes then don't add this.. if (!empty($this->disableelements) && is_array($this->disableelements)) { foreach ($this->disableelements as $disableelement) { if (property_exists($formdata, $disableelement)) { return false; } } } if (property_exists($formdata, $field) and $formdata->$field !== '') { return array('value' => (string)$formdata->$field); } return false; } /** * Returns the condition to be used with SQL where * * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { $field = $this->field; if (is_array($field)) { $res = " {$field[0]} = {$field[1]} "; } else { $res = " {$field} = 0 "; } return array($res, array()); } /** * Returns a human friendly description of the filter used as label. * * @param array $data filter settings * @return string active filter label */ public function get_label($data) { return $this->_label; } } filters/globalrole.php 0000644 00000007654 15151162244 0011064 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Global role filter * * @package core_user * @category user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/user/filters/lib.php'); /** * User filter based on global roles. * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_globalrole extends user_filter_type { /** * Constructor * @param string $name the name of the filter instance * @param string $label the label of the filter instance * @param boolean $advanced advanced form element flag */ public function __construct($name, $label, $advanced) { parent::__construct($name, $label, $advanced); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function user_filter_globalrole($name, $label, $advanced) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($name, $label, $advanced); } /** * Returns an array of available roles * @return array of availble roles */ public function get_roles() { $context = context_system::instance(); $roles = array(0 => get_string('anyrole', 'filters')) + get_assignable_roles($context); return $roles; } /** * Adds controls specific to this filter in the form. * @param object $mform a MoodleForm object to setup */ public function setupForm(&$mform) { $obj =& $mform->addElement('select', $this->_name, $this->_label, $this->get_roles()); $mform->setDefault($this->_name, 0); if ($this->_advanced) { $mform->setAdvanced($this->_name); } } /** * Retrieves data from the form data * @param object $formdata data submited with the form * @return mixed array filter data or false when filter not set */ public function check_data($formdata) { $field = $this->_name; if (property_exists($formdata, $field) and !empty($formdata->$field)) { return array('value' => (int)$formdata->$field); } return false; } /** * Returns the condition to be used with SQL where * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { global $CFG; $value = (int)$data['value']; $timenow = round(time(), 100); $sql = "id IN (SELECT userid FROM {role_assignments} a WHERE a.contextid=".SYSCONTEXTID." AND a.roleid=$value)"; return array($sql, array()); } /** * Returns a human friendly description of the filter used as label. * @param array $data filter settings * @return string active filter label */ public function get_label($data) { global $DB; $role = $DB->get_record('role', array('id' => $data['value'])); $a = new stdClass(); $a->label = $this->_label; $a->value = '"'.role_get_name($role).'"'; return get_string('globalrolelabel', 'filters', $a); } } filters/user_filter_forms.php 0000644 00000010043 15151162244 0012455 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains forms used to filter user. * * @package core_user * @category user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->libdir.'/formslib.php'); /** * Class user_add_filter_form * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_add_filter_form extends moodleform { /** * Form definition. */ public function definition() { global $SESSION; $mform =& $this->_form; $fields = $this->_customdata['fields']; $extraparams = $this->_customdata['extraparams']; $mform->addElement('header', 'newfilter', get_string('newfilter', 'filters')); foreach ($fields as $ft) { $ft->setupForm($mform); } // In case we wasnt to track some page params. if ($extraparams) { foreach ($extraparams as $key => $value) { $mform->addElement('hidden', $key, $value); $mform->setType($key, PARAM_RAW); } } // Add buttons. $replacefiltersbutton = $mform->createElement('submit', 'replacefilters', get_string('replacefilters', 'filters')); $addfilterbutton = $mform->createElement('submit', 'addfilter', get_string('addfilter', 'filters')); $buttons = array_filter([ empty($SESSION->user_filtering) ? null : $replacefiltersbutton, $addfilterbutton, ]); $mform->addGroup($buttons); } } /** * Class user_active_filter_form * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_active_filter_form extends moodleform { /** * Form definition. */ public function definition() { global $SESSION; // This is very hacky :-(. $mform =& $this->_form; $fields = $this->_customdata['fields']; $extraparams = $this->_customdata['extraparams']; if (!empty($SESSION->user_filtering)) { // Add controls for each active filter in the active filters group. $mform->addElement('header', 'actfilterhdr', get_string('actfilterhdr', 'filters')); foreach ($SESSION->user_filtering as $fname => $datas) { if (!array_key_exists($fname, $fields)) { continue; // Filter not used. } $field = $fields[$fname]; foreach ($datas as $i => $data) { $description = $field->get_label($data); $mform->addElement('checkbox', 'filter['.$fname.']['.$i.']', null, $description); } } if ($extraparams) { foreach ($extraparams as $key => $value) { $mform->addElement('hidden', $key, $value); $mform->setType($key, PARAM_RAW); } } $objs = array(); $objs[] = &$mform->createElement('submit', 'removeselected', get_string('removeselected', 'filters')); $objs[] = &$mform->createElement('submit', 'removeall', get_string('removeall', 'filters')); $mform->addElement('group', 'actfiltergrp', '', $objs, ' ', false); } } } filters/cohort.php 0000644 00000015321 15151162244 0010226 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Cohort filter. * * @package core_user * @category user * @copyright 2011 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/user/filters/lib.php'); /** * Generic filter for cohort membership. * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_cohort extends user_filter_type { /** * Constructor * @param boolean $advanced advanced form element flag */ public function __construct($advanced) { parent::__construct('cohort', get_string('idnumber', 'core_cohort'), $advanced); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function user_filter_cohort($advanced) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($advanced); } /** * Returns an array of comparison operators * @return array of comparison operators */ public function getOperators() { return array(0 => get_string('contains', 'filters'), 1 => get_string('doesnotcontain', 'filters'), 2 => get_string('isequalto', 'filters'), 3 => get_string('startswith', 'filters'), 4 => get_string('endswith', 'filters'), 5 => get_string('isempty', 'filters')); } /** * Adds controls specific to this filter in the form. * @param object $mform a MoodleForm object to setup */ public function setupForm(&$mform) { $objs = array(); $objs['select'] = $mform->createElement('select', $this->_name.'_op', null, $this->getOperators()); $objs['text'] = $mform->createElement('text', $this->_name, null); $objs['select']->setLabel(get_string('limiterfor', 'filters', $this->_label)); $objs['text']->setLabel(get_string('valuefor', 'filters', $this->_label)); $grp =& $mform->addElement('group', $this->_name.'_grp', $this->_label, $objs, '', false); $mform->setType($this->_name, PARAM_RAW); $mform->disabledIf($this->_name, $this->_name.'_op', 'eq', 5); if ($this->_advanced) { $mform->setAdvanced($this->_name.'_grp'); } $mform->setDefault($this->_name.'_op', 2); } /** * Retrieves data from the form data * @param object $formdata data submited with the form * @return mixed array filter data or false when filter not set */ public function check_data($formdata) { $field = $this->_name; $operator = $field.'_op'; if (property_exists($formdata, $operator)) { if ($formdata->$operator != 5 and $formdata->$field == '') { // No data - no change except for empty filter. return false; } // If field value is set then use it, else it's null. $fieldvalue = null; if (isset($formdata->$field)) { $fieldvalue = $formdata->$field; } return array('operator' => (int)$formdata->$operator, 'value' => $fieldvalue); } return false; } /** * Returns the condition to be used with SQL where * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { global $DB; static $counter = 0; $name = 'ex_cohort'.$counter++; $operator = $data['operator']; $value = $data['value']; $params = array(); if ($value === '') { return ''; } $not = ''; switch($operator) { case 0: // Contains. $res = $DB->sql_like('idnumber', ":$name", false, false); $params[$name] = "%$value%"; break; case 1: // Does not contain. $not = 'NOT'; $res = $DB->sql_like('idnumber', ":$name", false, false); $params[$name] = "%$value%"; break; case 2: // Equal to. $res = $DB->sql_like('idnumber', ":$name", false, false); $params[$name] = "$value"; break; case 3: // Starts with. $res = $DB->sql_like('idnumber', ":$name", false, false); $params[$name] = "$value%"; break; case 4: // Ends with. $res = $DB->sql_like('idnumber', ":$name", false, false); $params[$name] = "%$value"; break; case 5: // Empty. $not = 'NOT'; $res = '(idnumber IS NOT NULL AND idnumber <> :'.$name.')'; $params[$name] = ''; break; default: return ''; } $sql = "id $not IN (SELECT userid FROM {cohort_members} JOIN {cohort} ON {cohort_members}.cohortid = {cohort}.id WHERE $res)"; return array($sql, $params); } /** * Returns a human friendly description of the filter used as label. * @param array $data filter settings * @return string active filter label */ public function get_label($data) { $operator = $data['operator']; $value = $data['value']; $operators = $this->getOperators(); $a = new stdClass(); $a->label = $this->_label; $a->value = '"'.s($value).'"'; $a->operator = $operators[$operator]; switch ($operator) { case 0: // Contains. case 1: // Doesn't contain. case 2: // Equal to. case 3: // Starts with. case 4: // Ends with. return get_string('textlabel', 'filters', $a); case 5: // Empty. return get_string('textlabelnovalue', 'filters', $a); } return ''; } } filters/select.php 0000644 00000013123 15151162244 0010205 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Value select filter. * * @package core_user * @category user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/user/filters/lib.php'); /** * Generic filter based on a list of values. * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_select extends user_filter_type { /** * options for the list values * @var array */ public $_options; /** @var string */ public $_field; /** @var mixed|null */ public $_default; /** * Constructor * @param string $name the name of the filter instance * @param string $label the label of the filter instance * @param boolean $advanced advanced form element flag * @param string $field user table filed name * @param array $options select options * @param mixed $default option */ public function __construct($name, $label, $advanced, $field, $options, $default=null) { parent::__construct($name, $label, $advanced); $this->_field = $field; $this->_options = $options; $this->_default = $default; } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function user_filter_select($name, $label, $advanced, $field, $options, $default=null) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($name, $label, $advanced, $field, $options, $default=null); } /** * Returns an array of comparison operators * @return array of comparison operators */ public function get_operators() { return array(0 => get_string('isanyvalue', 'filters'), 1 => get_string('isequalto', 'filters'), 2 => get_string('isnotequalto', 'filters')); } /** * Adds controls specific to this filter in the form. * @param moodleform $mform a MoodleForm object to setup */ public function setupForm(&$mform) { $objs = array(); $objs['limiter'] = $mform->createElement('select', $this->_name.'_op', null, $this->get_operators()); $objs['limiter']->setLabel(get_string('limiterfor', 'filters', $this->_label)); $objs['country'] = $mform->createElement('select', $this->_name, null, $this->_options); $objs['country']->setLabel(get_string('valuefor', 'filters', $this->_label)); $grp =& $mform->addElement('group', $this->_name.'_grp', $this->_label, $objs, '', false); $mform->disabledIf($this->_name, $this->_name.'_op', 'eq', 0); if (!is_null($this->_default)) { $mform->setDefault($this->_name, $this->_default); } if ($this->_advanced) { $mform->setAdvanced($this->_name.'_grp'); } } /** * Retrieves data from the form data * @param stdClass $formdata data submited with the form * @return mixed array filter data or false when filter not set */ public function check_data($formdata) { $field = $this->_name; $operator = $field.'_op'; if (property_exists($formdata, $field) and !empty($formdata->$operator)) { return array('operator' => (int)$formdata->$operator, 'value' => (string)$formdata->$field); } return false; } /** * Returns the condition to be used with SQL where * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { static $counter = 0; $name = 'ex_select'.$counter++; $operator = $data['operator']; $value = $data['value']; $field = $this->_field; $params = array(); switch($operator) { case 1: // Equal to. $res = "=:$name"; $params[$name] = $value; break; case 2: // Not equal to. $res = "<>:$name"; $params[$name] = $value; break; default: return array('', array()); } return array($field.$res, $params); } /** * Returns a human friendly description of the filter used as label. * @param array $data filter settings * @return string active filter label */ public function get_label($data) { $operators = $this->get_operators(); $operator = $data['operator']; $value = $data['value']; if (empty($operator)) { return ''; } $a = new stdClass(); $a->label = $this->_label; $a->value = '"'.s($this->_options[$value]).'"'; $a->operator = $operators[$operator]; return get_string('selectlabel', 'filters', $a); } } filters/simpleselect.php 0000644 00000010022 15151162244 0011412 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Simple value select filter. * * @package core_user * @category user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/user/filters/lib.php'); /** * Generic filter based on a list of values. * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_simpleselect extends user_filter_type { /** * options for the list values * @var array */ public $_options; /** * @var string */ public $_field; /** * Constructor * @param string $name the name of the filter instance * @param string $label the label of the filter instance * @param boolean $advanced advanced form element flag * @param string $field user table filed name * @param array $options select options */ public function __construct($name, $label, $advanced, $field, $options) { parent::__construct($name, $label, $advanced); $this->_field = $field; $this->_options = $options; } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function user_filter_simpleselect($name, $label, $advanced, $field, $options) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($name, $label, $advanced, $field, $options); } /** * Adds controls specific to this filter in the form. * @param moodleform $mform a MoodleForm object to setup */ public function setupForm(&$mform) { $choices = array('' => get_string('anyvalue', 'filters')) + $this->_options; $mform->addElement('select', $this->_name, $this->_label, $choices); if ($this->_advanced) { $mform->setAdvanced($this->_name); } } /** * Retrieves data from the form data * @param object $formdata data submited with the form * @return mixed array filter data or false when filter not set */ public function check_data($formdata) { $field = $this->_name; if (property_exists($formdata, $field) and $formdata->$field !== '') { return array('value' => (string)$formdata->$field); } return false; } /** * Returns the condition to be used with SQL where * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { static $counter = 0; $name = 'ex_simpleselect'.$counter++; $value = $data['value']; $params = array(); $field = $this->_field; if ($value == '') { return ''; } return array("$field=:$name", array($name => $value)); } /** * Returns a human friendly description of the filter used as label. * @param array $data filter settings * @return string active filter label */ public function get_label($data) { $value = $data['value']; $a = new stdClass(); $a->label = $this->_label; $a->value = '"'.s($this->_options[$value]).'"'; $a->operator = get_string('isequalto', 'filters'); return get_string('selectlabel', 'filters', $a); } } filters/date.php 0000644 00000012643 15151162244 0007651 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Date filter * * @package core_user * @category user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/user/filters/lib.php'); /** * Generic filter based on a date. * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_date extends user_filter_type { /** * the fields available for comparisson * @var string */ public $_field; /** * Constructor * @param string $name the name of the filter instance * @param string $label the label of the filter instance * @param boolean $advanced advanced form element flag * @param string $field user table filed name */ public function __construct($name, $label, $advanced, $field) { parent::__construct($name, $label, $advanced); $this->_field = $field; } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function user_filter_date($name, $label, $advanced, $field) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($name, $label, $advanced, $field); } /** * Adds controls specific to this filter in the form. * @param object $mform a MoodleForm object to setup */ public function setupForm(&$mform) { $objs = array(); $objs[] = $mform->createElement('static', $this->_name.'_s1', null, html_writer::start_tag('div', array('class' => 'w-100 d-flex align-items-center'))); $objs[] = $mform->createElement('static', $this->_name.'_s2', null, html_writer::tag('div', get_string('isafter', 'filters'), array('class' => 'mr-2'))); $objs[] = $mform->createElement('date_selector', $this->_name.'_sdt', null, array('optional' => true)); $objs[] = $mform->createElement('static', $this->_name.'_s3', null, html_writer::end_tag('div')); $objs[] = $mform->createElement('static', $this->_name.'_s4', null, html_writer::start_tag('div', array('class' => 'w-100 d-flex align-items-center'))); $objs[] = $mform->createElement('static', $this->_name.'_s5', null, html_writer::tag('div', get_string('isbefore', 'filters'), array('class' => 'mr-2'))); $objs[] = $mform->createElement('date_selector', $this->_name.'_edt', null, array('optional' => true)); $objs[] = $mform->createElement('static', $this->_name.'_s6', null, html_writer::end_tag('div')); $grp =& $mform->addElement('group', $this->_name.'_grp', $this->_label, $objs, '', false); if ($this->_advanced) { $mform->setAdvanced($this->_name.'_grp'); } } /** * Retrieves data from the form data * @param object $formdata data submited with the form * @return mixed array filter data or false when filter not set */ public function check_data($formdata) { $sdt = $this->_name.'_sdt'; $edt = $this->_name.'_edt'; if (!$formdata->$sdt and !$formdata->$edt) { return false; } $data = array(); $data['after'] = $formdata->$sdt; $data['before'] = $formdata->$edt; return $data; } /** * Returns the condition to be used with SQL where * @param array $data filter settings * @return array sql string and $params */ public function get_sql_filter($data) { $after = (int)$data['after']; $before = (int)$data['before']; $field = $this->_field; if (empty($after) and empty($before)) { return array('', array()); } $res = " $field >= 0 "; if ($after) { $res .= " AND $field >= $after"; } if ($before) { $res .= " AND $field <= $before"; } return array($res, array()); } /** * Returns a human friendly description of the filter used as label. * @param array $data filter settings * @return string active filter label */ public function get_label($data) { $after = $data['after']; $before = $data['before']; $field = $this->_field; $a = new stdClass(); $a->label = $this->_label; $a->after = userdate($after); $a->before = userdate($before); if ($after and $before) { return get_string('datelabelisbetween', 'filters', $a); } else if ($after) { return get_string('datelabelisafter', 'filters', $a); } else if ($before) { return get_string('datelabelisbefore', 'filters', $a); } return ''; } } filters/lib.php 0000644 00000037025 15151162244 0007503 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the User Filter API. * * @package core_user * @category user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/user/filters/text.php'); require_once($CFG->dirroot.'/user/filters/date.php'); require_once($CFG->dirroot.'/user/filters/select.php'); require_once($CFG->dirroot.'/user/filters/simpleselect.php'); require_once($CFG->dirroot.'/user/filters/courserole.php'); require_once($CFG->dirroot.'/user/filters/globalrole.php'); require_once($CFG->dirroot.'/user/filters/profilefield.php'); require_once($CFG->dirroot.'/user/filters/yesno.php'); require_once($CFG->dirroot.'/user/filters/anycourses.php'); require_once($CFG->dirroot.'/user/filters/cohort.php'); require_once($CFG->dirroot.'/user/filters/user_filter_forms.php'); require_once($CFG->dirroot.'/user/filters/checkbox.php'); /** * User filtering wrapper class. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filtering { /** @var array */ public $_fields; /** @var \user_add_filter_form */ public $_addform; /** @var \user_active_filter_form */ public $_activeform; /** * Contructor * @param array $fieldnames array of visible user fields * @param string $baseurl base url used for submission/return, null if the same of current page * @param array $extraparams extra page parameters */ public function __construct($fieldnames = null, $baseurl = null, $extraparams = null) { global $SESSION; if (!isset($SESSION->user_filtering)) { $SESSION->user_filtering = array(); } if (empty($fieldnames)) { // As a start, add all fields as advanced fields (which are only available after clicking on "Show more"). $fieldnames = array('realname' => 1, 'lastname' => 1, 'firstname' => 1, 'username' => 1, 'email' => 1, 'city' => 1, 'country' => 1, 'confirmed' => 1, 'suspended' => 1, 'profile' => 1, 'courserole' => 1, 'anycourses' => 1, 'systemrole' => 1, 'cohort' => 1, 'firstaccess' => 1, 'lastaccess' => 1, 'neveraccessed' => 1, 'timecreated' => 1, 'timemodified' => 1, 'nevermodified' => 1, 'auth' => 1, 'mnethostid' => 1, 'idnumber' => 1, 'institution' => 1, 'department' => 1, 'lastip' => 1); // Get the config which filters the admin wanted to show by default. $userfiltersdefault = get_config('core', 'userfiltersdefault'); // If the admin did not enable any filter, the form will not make much sense if all fields are hidden behind // "Show more". Thus, we enable the 'realname' filter automatically. if ($userfiltersdefault == '') { $userfiltersdefault = array('realname'); // Otherwise, we split the enabled filters into an array. } else { $userfiltersdefault = explode(',', $userfiltersdefault); } // Show these fields by default which the admin has enabled in the config. foreach ($userfiltersdefault as $key) { $fieldnames[$key] = 0; } } $this->_fields = array(); foreach ($fieldnames as $fieldname => $advanced) { if ($field = $this->get_field($fieldname, $advanced)) { $this->_fields[$fieldname] = $field; } } // Fist the new filter form. $this->_addform = new user_add_filter_form($baseurl, array('fields' => $this->_fields, 'extraparams' => $extraparams)); if ($adddata = $this->_addform->get_data()) { // Clear previous filters. if (!empty($adddata->replacefilters)) { $SESSION->user_filtering = []; } // Add new filters. foreach ($this->_fields as $fname => $field) { $data = $field->check_data($adddata); if ($data === false) { continue; // Nothing new. } if (!array_key_exists($fname, $SESSION->user_filtering)) { $SESSION->user_filtering[$fname] = array(); } $SESSION->user_filtering[$fname][] = $data; } } // Now the active filters. $this->_activeform = new user_active_filter_form($baseurl, array('fields' => $this->_fields, 'extraparams' => $extraparams)); if ($activedata = $this->_activeform->get_data()) { if (!empty($activedata->removeall)) { $SESSION->user_filtering = array(); } else if (!empty($activedata->removeselected) and !empty($activedata->filter)) { foreach ($activedata->filter as $fname => $instances) { foreach ($instances as $i => $val) { if (empty($val)) { continue; } unset($SESSION->user_filtering[$fname][$i]); } if (empty($SESSION->user_filtering[$fname])) { unset($SESSION->user_filtering[$fname]); } } } } // Rebuild the forms if filters data was processed. if ($adddata || $activedata) { $_POST = []; // Reset submitted data. $this->_addform = new user_add_filter_form($baseurl, ['fields' => $this->_fields, 'extraparams' => $extraparams]); $this->_activeform = new user_active_filter_form($baseurl, ['fields' => $this->_fields, 'extraparams' => $extraparams]); } } /** * Creates known user filter if present * @param string $fieldname * @param boolean $advanced * @return object filter */ public function get_field($fieldname, $advanced) { global $USER, $CFG, $DB, $SITE; switch ($fieldname) { case 'username': return new user_filter_text('username', get_string('username'), $advanced, 'username'); case 'realname': return new user_filter_text('realname', get_string('fullnameuser'), $advanced, $DB->sql_fullname()); case 'lastname': return new user_filter_text('lastname', get_string('lastname'), $advanced, 'lastname'); case 'firstname': return new user_filter_text('firstname', get_string('firstname'), $advanced, 'firstname'); case 'email': return new user_filter_text('email', get_string('email'), $advanced, 'email'); case 'city': return new user_filter_text('city', get_string('city'), $advanced, 'city'); case 'country': return new user_filter_select('country', get_string('country'), $advanced, 'country', get_string_manager()->get_list_of_countries(), $USER->country); case 'confirmed': return new user_filter_yesno('confirmed', get_string('confirmed', 'admin'), $advanced, 'confirmed'); case 'suspended': return new user_filter_yesno('suspended', get_string('suspended', 'auth'), $advanced, 'suspended'); case 'profile': return new user_filter_profilefield('profile', get_string('profilefields', 'admin'), $advanced); case 'courserole': return new user_filter_courserole('courserole', get_string('courserole', 'filters'), $advanced); case 'anycourses': return new user_filter_anycourses('anycourses', get_string('anycourses', 'filters'), $advanced, 'user_enrolments'); case 'systemrole': return new user_filter_globalrole('systemrole', get_string('globalrole', 'role'), $advanced); case 'firstaccess': return new user_filter_date('firstaccess', get_string('firstaccess', 'filters'), $advanced, 'firstaccess'); case 'lastaccess': return new user_filter_date('lastaccess', get_string('lastaccess'), $advanced, 'lastaccess'); case 'neveraccessed': return new user_filter_checkbox('neveraccessed', get_string('neveraccessed', 'filters'), $advanced, 'firstaccess', array('lastaccess_sck', 'lastaccess_eck', 'firstaccess_eck', 'firstaccess_sck')); case 'timecreated': return new user_filter_date('timecreated', get_string('timecreated'), $advanced, 'timecreated'); case 'timemodified': return new user_filter_date('timemodified', get_string('lastmodified'), $advanced, 'timemodified'); case 'nevermodified': return new user_filter_checkbox('nevermodified', get_string('nevermodified', 'filters'), $advanced, array('timemodified', 'timecreated'), array('timemodified_sck', 'timemodified_eck')); case 'cohort': return new user_filter_cohort($advanced); case 'idnumber': return new user_filter_text('idnumber', get_string('idnumber'), $advanced, 'idnumber'); case 'institution': return new user_filter_text('institution', get_string('institution'), $advanced, 'institution'); case 'department': return new user_filter_text('department', get_string('department'), $advanced, 'department'); case 'lastip': return new user_filter_text('lastip', get_string('lastip'), $advanced, 'lastip'); case 'auth': $plugins = core_component::get_plugin_list('auth'); $choices = array(); foreach ($plugins as $auth => $unused) { $choices[$auth] = get_string('pluginname', "auth_{$auth}"); } return new user_filter_simpleselect('auth', get_string('authentication'), $advanced, 'auth', $choices); case 'mnethostid': // Include all hosts even those deleted or otherwise problematic. if (!$hosts = $DB->get_records('mnet_host', null, 'id', 'id, wwwroot, name')) { $hosts = array(); } $choices = array(); foreach ($hosts as $host) { if ($host->id == $CFG->mnet_localhost_id) { $choices[$host->id] = format_string($SITE->fullname).' ('.get_string('local').')'; } else if (empty($host->wwwroot)) { // All hosts. continue; } else { $choices[$host->id] = $host->name.' ('.$host->wwwroot.')'; } } if ($usedhosts = $DB->get_fieldset_sql("SELECT DISTINCT mnethostid FROM {user} WHERE deleted=0")) { foreach ($usedhosts as $hostid) { if (empty($hosts[$hostid])) { $choices[$hostid] = 'id: '.$hostid.' ('.get_string('error').')'; } } } if (count($choices) < 2) { return null; // Filter not needed. } return new user_filter_simpleselect('mnethostid', get_string('mnetidprovider', 'mnet'), $advanced, 'mnethostid', $choices); default: return null; } } /** * Returns sql where statement based on active user filters * @param string $extra sql * @param array $params named params (recommended prefix ex) * @return array sql string and $params */ public function get_sql_filter($extra='', array $params=null) { global $SESSION; $sqls = array(); if ($extra != '') { $sqls[] = $extra; } $params = (array)$params; if (!empty($SESSION->user_filtering)) { foreach ($SESSION->user_filtering as $fname => $datas) { if (!array_key_exists($fname, $this->_fields)) { continue; // Filter not used. } $field = $this->_fields[$fname]; foreach ($datas as $i => $data) { list($s, $p) = $field->get_sql_filter($data); $sqls[] = $s; $params = $params + $p; } } } if (empty($sqls)) { return array('', array()); } else { $sqls = implode(' AND ', $sqls); return array($sqls, $params); } } /** * Print the add filter form. */ public function display_add() { $this->_addform->display(); } /** * Print the active filter form. */ public function display_active() { $this->_activeform->display(); } } /** * The base user filter class. All abstract classes must be implemented. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_type { /** * The name of this filter instance. * @var string */ public $_name; /** * The label of this filter instance. * @var string */ public $_label; /** * Advanced form element flag * @var bool */ public $_advanced; /** * Constructor * @param string $name the name of the filter instance * @param string $label the label of the filter instance * @param boolean $advanced advanced form element flag */ public function __construct($name, $label, $advanced) { $this->_name = $name; $this->_label = $label; $this->_advanced = $advanced; } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function user_filter_type($name, $label, $advanced) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($name, $label, $advanced); } /** * Returns the condition to be used with SQL where * @param array $data filter settings * @return string the filtering condition or null if the filter is disabled */ public function get_sql_filter($data) { throw new \moodle_exception('mustbeoveride', 'debug', '', 'get_sql_filter'); } /** * Retrieves data from the form data * @param stdClass $formdata data submited with the form * @return mixed array filter data or false when filter not set */ public function check_data($formdata) { throw new \moodle_exception('mustbeoveride', 'debug', '', 'check_data'); } /** * Adds controls specific to this filter in the form. * @param moodleform $mform a MoodleForm object to setup */ public function setupForm(&$mform) { throw new \moodle_exception('mustbeoveride', 'debug', '', 'setupForm'); } /** * Returns a human friendly description of the filter used as label. * @param array $data filter settings * @return string active filter label */ public function get_label($data) { throw new \moodle_exception('mustbeoveride', 'debug', '', 'get_label'); } } tests/group_non_members_selector_test.php 0000644 00000011567 15151162244 0015111 0 ustar 00 <?php // This file is part of Moodle - https://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user; use group_non_members_selector; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/user/selector/lib.php'); /** * Unit tests for {@link group_non_members_selector} class. * * @package core_user * @category test * @copyright 2019 Huong Nguyen <huongnv13@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_non_members_selector_test extends \advanced_testcase { /** * Test find_users that only return group non members * * @throws coding_exception */ public function test_find_users_only_return_group_non_member() { $this->resetAfterTest(); // Create course. $course = $this->getDataGenerator()->create_course(); // Create users. $user1 = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => '1']); $user2 = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => '2']); $user3 = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => '3']); $user4 = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => '4']); $user5 = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => '5']); // Create group. $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]); // Enroll users to course. Except User5. $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $this->getDataGenerator()->enrol_user($user3->id, $course->id); $this->getDataGenerator()->enrol_user($user4->id, $course->id); // Assign User1 to Group. $this->getDataGenerator()->create_group_member(['groupid' => $group->id, 'userid' => $user1->id]); // User1 and User5 will not exist in the result. // User2, User3 and User4 will exist in the result. $potentialmembersselector = new group_non_members_selector('addselect', ['groupid' => $group->id, 'courseid' => $course->id]); foreach ($potentialmembersselector->find_users('User') as $found) { $this->assertCount(3, $found); $this->assertArrayNotHasKey($user5->id, $found); $this->assertArrayNotHasKey($user1->id, $found); $this->assertArrayHasKey($user2->id, $found); $this->assertArrayHasKey($user3->id, $found); $this->assertArrayHasKey($user4->id, $found); } // Assign User2 to Group. $this->getDataGenerator()->create_group_member(['groupid' => $group->id, 'userid' => $user2->id]); // User1, User2 and User5 will not exist in the result. // User3 and User4 will exist in the result. $potentialmembersselector = new group_non_members_selector('addselect', ['groupid' => $group->id, 'courseid' => $course->id]); foreach ($potentialmembersselector->find_users('User') as $found) { $this->assertCount(2, $found); $this->assertArrayNotHasKey($user5->id, $found); $this->assertArrayNotHasKey($user1->id, $found); $this->assertArrayNotHasKey($user2->id, $found); $this->assertArrayHasKey($user3->id, $found); $this->assertArrayHasKey($user4->id, $found); } // Assign User3 to Group. $this->getDataGenerator()->create_group_member(['groupid' => $group->id, 'userid' => $user3->id]); // User1, User2, User3 and User5 will not exist in the result. // Only User4 will exist in the result. $potentialmembersselector = new group_non_members_selector('addselect', ['groupid' => $group->id, 'courseid' => $course->id]); foreach ($potentialmembersselector->find_users('User') as $found) { $this->assertCount(1, $found); $this->assertArrayNotHasKey($user5->id, $found); $this->assertArrayNotHasKey($user1->id, $found); $this->assertArrayNotHasKey($user2->id, $found); $this->assertArrayNotHasKey($user3->id, $found); $this->assertArrayHasKey($user4->id, $found); } } } tests/fixtures/testable_user_selector.php 0000644 00000004025 15151162244 0015033 0 ustar 00 <?php // This file is part of Moodle - https://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Provides {@link testable_user_selector} class. * * @package core_user * @subpackage fixtures * @category test * @copyright 2018 David Mudrák <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Testable subclass of the user selector base class. * * @copyright 2018 David Mudrák <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class testable_user_selector extends user_selector_base { /** * Basic implementation of the users finder. * * @param string $search * @return array of (string)optgroupname => array of users */ public function find_users($search) { global $DB; list($wherecondition, $whereparams) = $this->search_sql($search, 'u'); list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext); $params = array_merge($whereparams, $sortparams); $fields = $this->required_fields_sql('u'); $sql = "SELECT $fields FROM {user} u WHERE $wherecondition ORDER BY $sort"; $found = $DB->get_records_sql($sql, $params); if (empty($found)) { return []; } return [get_string('potusers', 'core_role') => $found]; } } tests/fixtures/myprofile_fixtures.php 0000644 00000004264 15151162244 0014236 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines fixutres for unit testing of lib/classes/myprofile/. * * @package core_user * @category test * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Class phpunit_fixture_myprofile_category * * @package core_user * @category test * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class phpunit_fixture_myprofile_category extends \core_user\output\myprofile\category { /** * Make protected method public for testing. * * @param node $node * @return node Nodes after the specified node. */ public function find_nodes_after($node) { return parent::find_nodes_after($node); } /** * Make protected method public for testing. */ public function validate_after_order() { parent::validate_after_order(); } } /** * Class phpunit_fixture_myprofile_tree * * @package core_user * @category test * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class phpunit_fixture_myprofile_tree extends \core_user\output\myprofile\tree { /** * Make protected method public for testing. * * @param category $cat Category object * @return array An array of category objects. */ public function find_categories_after($cat) { return parent::find_categories_after($cat); } } tests/privacy/provider_test.php 0000644 00000051043 15151162244 0012771 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy tests for core_user. * * @package core_user * @category test * @copyright 2018 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\tests\provider_testcase; use \core_user\privacy\provider; use \core_privacy\local\request\approved_userlist; use \core_privacy\local\request\transform; require_once($CFG->dirroot . "/user/lib.php"); /** * Unit tests for core_user. * * @copyright 2018 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { /** * Check that context information is returned correctly. */ public function test_get_contexts_for_userid() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); // Create some other users as well. $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertSame($context, $contextlist->current()); } /** * Test that data is exported as expected for a user. */ public function test_export_user_data() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user([ 'firstaccess' => 1535760000, 'lastaccess' => 1541030400, 'currentlogin' => 1541030400, ]); $course = $this->getDataGenerator()->create_course(); $context = \context_user::instance($user->id); $this->create_data_for_user($user, $course); $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'core_user', [$context->id]); $writer = \core_privacy\local\request\writer::with_context($context); provider::export_user_data($approvedlist); // Make sure that the password history only returns a count. $history = $writer->get_data([get_string('privacy:passwordhistorypath', 'user')]); $objectcount = new \ArrayObject($history); // This object should only have one property. $this->assertCount(1, $objectcount); $this->assertEquals(1, $history->password_history_count); // Password resets should have two fields - timerequested and timererequested. $resetarray = (array) $writer->get_data([get_string('privacy:passwordresetpath', 'user')]); $detail = array_shift($resetarray); $this->assertTrue(array_key_exists('timerequested', $detail)); $this->assertTrue(array_key_exists('timererequested', $detail)); // Last access to course. $lastcourseaccess = (array) $writer->get_data([get_string('privacy:lastaccesspath', 'user')]); $entry = array_shift($lastcourseaccess); $this->assertEquals($course->fullname, $entry['course_name']); $this->assertTrue(array_key_exists('timeaccess', $entry)); // User devices. $userdevices = (array) $writer->get_data([get_string('privacy:devicespath', 'user')]); $entry = array_shift($userdevices); $this->assertEquals('com.moodle.moodlemobile', $entry['appid']); // Make sure these fields are not exported. $this->assertFalse(array_key_exists('pushid', $entry)); $this->assertFalse(array_key_exists('uuid', $entry)); // Session data. $sessiondata = (array) $writer->get_data([get_string('privacy:sessionpath', 'user')]); $entry = array_shift($sessiondata); // Make sure that the sid is not exported. $this->assertFalse(array_key_exists('sid', $entry)); // Check that some of the other fields are present. $this->assertTrue(array_key_exists('state', $entry)); $this->assertTrue(array_key_exists('sessdata', $entry)); $this->assertTrue(array_key_exists('timecreated', $entry)); // Course requests $courserequestdata = (array) $writer->get_data([get_string('privacy:courserequestpath', 'user')]); $entry = array_shift($courserequestdata); // Make sure that the password is not exported. $this->assertFalse(property_exists($entry, 'password')); // Check that some of the other fields are present. $this->assertTrue(property_exists($entry, 'fullname')); $this->assertTrue(property_exists($entry, 'shortname')); $this->assertTrue(property_exists($entry, 'summary')); // User details. $userdata = (array) $writer->get_data([]); // Check that the password is not exported. $this->assertFalse(array_key_exists('password', $userdata)); // Check that some critical fields exist. $this->assertTrue(array_key_exists('firstname', $userdata)); $this->assertTrue(array_key_exists('lastname', $userdata)); $this->assertTrue(array_key_exists('email', $userdata)); // Check access times. $this->assertEquals(transform::datetime($user->firstaccess), $userdata['firstaccess']); $this->assertEquals(transform::datetime($user->lastaccess), $userdata['lastaccess']); $this->assertNull($userdata['lastlogin']); $this->assertEquals(transform::datetime($user->currentlogin), $userdata['currentlogin']); } /** * Test that user data is deleted for one user. */ public function test_delete_data_for_all_users_in_context() { global $DB; $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user([ 'idnumber' => 'A0023', 'emailstop' => 1, 'phone1' => '555 3257', 'institution' => 'test', 'department' => 'Science', 'city' => 'Perth', 'country' => 'AU' ]); $user2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $this->create_data_for_user($user, $course); $this->create_data_for_user($user2, $course); provider::delete_data_for_all_users_in_context(\context_user::instance($user->id)); // These tables should not have any user data for $user. Only for $user2. $records = $DB->get_records('user_password_history'); $this->assertCount(1, $records); $data = array_shift($records); $this->assertNotEquals($user->id, $data->userid); $this->assertEquals($user2->id, $data->userid); $records = $DB->get_records('user_password_resets'); $this->assertCount(1, $records); $data = array_shift($records); $this->assertNotEquals($user->id, $data->userid); $this->assertEquals($user2->id, $data->userid); $records = $DB->get_records('user_lastaccess'); $this->assertCount(1, $records); $data = array_shift($records); $this->assertNotEquals($user->id, $data->userid); $this->assertEquals($user2->id, $data->userid); $records = $DB->get_records('user_devices'); $this->assertCount(1, $records); $data = array_shift($records); $this->assertNotEquals($user->id, $data->userid); $this->assertEquals($user2->id, $data->userid); // Now check that there is still a record for the deleted user, but that non-critical information is removed. $record = $DB->get_record('user', ['id' => $user->id]); $this->assertEmpty($record->idnumber); $this->assertEmpty($record->emailstop); $this->assertEmpty($record->phone1); $this->assertEmpty($record->institution); $this->assertEmpty($record->department); $this->assertEmpty($record->city); $this->assertEmpty($record->country); $this->assertEmpty($record->timezone); $this->assertEmpty($record->timecreated); $this->assertEmpty($record->timemodified); $this->assertEmpty($record->firstnamephonetic); // Check for critical fields. // Deleted should now be 1. $this->assertEquals(1, $record->deleted); $this->assertEquals($user->id, $record->id); $this->assertEquals($user->username, $record->username); $this->assertEquals($user->password, $record->password); $this->assertEquals($user->firstname, $record->firstname); $this->assertEquals($user->lastname, $record->lastname); $this->assertEquals($user->email, $record->email); } /** * Test that user data is deleted for one user. */ public function test_delete_data_for_user() { global $DB; $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user([ 'idnumber' => 'A0023', 'emailstop' => 1, 'phone1' => '555 3257', 'institution' => 'test', 'department' => 'Science', 'city' => 'Perth', 'country' => 'AU' ]); $user2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $this->create_data_for_user($user, $course); $this->create_data_for_user($user2, $course); // Provide multiple different context to check that only the correct user is deleted. $contexts = [ \context_user::instance($user->id)->id, \context_user::instance($user2->id)->id, \context_system::instance()->id]; $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'core_user', $contexts); provider::delete_data_for_user($approvedlist); // These tables should not have any user data for $user. Only for $user2. $records = $DB->get_records('user_password_history'); $this->assertCount(1, $records); $data = array_shift($records); $this->assertNotEquals($user->id, $data->userid); $this->assertEquals($user2->id, $data->userid); $records = $DB->get_records('user_password_resets'); $this->assertCount(1, $records); $data = array_shift($records); $this->assertNotEquals($user->id, $data->userid); $this->assertEquals($user2->id, $data->userid); $records = $DB->get_records('user_lastaccess'); $this->assertCount(1, $records); $data = array_shift($records); $this->assertNotEquals($user->id, $data->userid); $this->assertEquals($user2->id, $data->userid); $records = $DB->get_records('user_devices'); $this->assertCount(1, $records); $data = array_shift($records); $this->assertNotEquals($user->id, $data->userid); $this->assertEquals($user2->id, $data->userid); // Now check that there is still a record for the deleted user, but that non-critical information is removed. $record = $DB->get_record('user', ['id' => $user->id]); $this->assertEmpty($record->idnumber); $this->assertEmpty($record->emailstop); $this->assertEmpty($record->phone1); $this->assertEmpty($record->institution); $this->assertEmpty($record->department); $this->assertEmpty($record->city); $this->assertEmpty($record->country); $this->assertEmpty($record->timezone); $this->assertEmpty($record->timecreated); $this->assertEmpty($record->timemodified); $this->assertEmpty($record->firstnamephonetic); // Check for critical fields. // Deleted should now be 1. $this->assertEquals(1, $record->deleted); $this->assertEquals($user->id, $record->id); $this->assertEquals($user->username, $record->username); $this->assertEquals($user->password, $record->password); $this->assertEquals($user->firstname, $record->firstname); $this->assertEquals($user->lastname, $record->lastname); $this->assertEquals($user->email, $record->email); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context() { $this->resetAfterTest(); $component = 'core_user'; // Create a user. $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); $userlist = new \core_privacy\local\request\userlist($usercontext, $component); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users() { global $DB; $this->resetAfterTest(); $component = 'core_user'; // Create user1. $user1 = $this->getDataGenerator()->create_user([ 'idnumber' => 'A0023', 'emailstop' => 1, 'phone1' => '555 3257', 'institution' => 'test', 'department' => 'Science', 'city' => 'Perth', 'country' => 'AU' ]); $usercontext1 = \context_user::instance($user1->id); $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); // Create user2. $user2 = $this->getDataGenerator()->create_user([ 'idnumber' => 'A0024', 'emailstop' => 1, 'phone1' => '555 3258', 'institution' => 'test', 'department' => 'Science', 'city' => 'Perth', 'country' => 'AU' ]); $usercontext2 = \context_user::instance($user2->id); $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); // The list of users for usercontext1 should return user1. provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); // The list of users for usercontext2 should return user2. provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete using delete_data_for_users(). provider::delete_data_for_users($approvedlist); // Now check that there is still a record for user1 (deleted user), but non-critical information is removed. $record = $DB->get_record('user', ['id' => $user1->id]); $this->assertEmpty($record->idnumber); $this->assertEmpty($record->emailstop); $this->assertEmpty($record->phone1); $this->assertEmpty($record->institution); $this->assertEmpty($record->department); $this->assertEmpty($record->city); $this->assertEmpty($record->country); $this->assertEmpty($record->timezone); $this->assertEmpty($record->timecreated); $this->assertEmpty($record->timemodified); $this->assertEmpty($record->firstnamephonetic); // Check for critical fields. // Deleted should now be 1. $this->assertEquals(1, $record->deleted); $this->assertEquals($user1->id, $record->id); $this->assertEquals($user1->username, $record->username); $this->assertEquals($user1->password, $record->password); $this->assertEquals($user1->firstname, $record->firstname); $this->assertEquals($user1->lastname, $record->lastname); $this->assertEquals($user1->email, $record->email); // Now check that the record and information for user2 is still present. $record = $DB->get_record('user', ['id' => $user2->id]); $this->assertNotEmpty($record->idnumber); $this->assertNotEmpty($record->emailstop); $this->assertNotEmpty($record->phone1); $this->assertNotEmpty($record->institution); $this->assertNotEmpty($record->department); $this->assertNotEmpty($record->city); $this->assertNotEmpty($record->country); $this->assertNotEmpty($record->timezone); $this->assertNotEmpty($record->timecreated); $this->assertNotEmpty($record->timemodified); $this->assertNotEmpty($record->firstnamephonetic); $this->assertEquals(0, $record->deleted); $this->assertEquals($user2->id, $record->id); $this->assertEquals($user2->username, $record->username); $this->assertEquals($user2->password, $record->password); $this->assertEquals($user2->firstname, $record->firstname); $this->assertEquals($user2->lastname, $record->lastname); $this->assertEquals($user2->email, $record->email); } /** * Create user data for a user. * * @param stdClass $user A user object. * @param stdClass $course A course. */ protected function create_data_for_user($user, $course) { global $DB; $this->resetAfterTest(); // Last course access. $lastaccess = (object) [ 'userid' => $user->id, 'courseid' => $course->id, 'timeaccess' => time() - DAYSECS ]; $DB->insert_record('user_lastaccess', $lastaccess); // Password history. $history = (object) [ 'userid' => $user->id, 'hash' => 'HID098djJUU', 'timecreated' => time() ]; $DB->insert_record('user_password_history', $history); // Password resets. $passwordreset = (object) [ 'userid' => $user->id, 'timerequested' => time(), 'timererequested' => time(), 'token' => $this->generate_random_string() ]; $DB->insert_record('user_password_resets', $passwordreset); // User mobile devices. $userdevices = (object) [ 'userid' => $user->id, 'appid' => 'com.moodle.moodlemobile', 'name' => 'occam', 'model' => 'Nexus 4', 'platform' => 'Android', 'version' => '4.2.2', 'pushid' => 'kishUhd', 'uuid' => 'KIhud7s', 'timecreated' => time(), 'timemodified' => time() ]; $DB->insert_record('user_devices', $userdevices); // Course request. $courserequest = (object) [ 'fullname' => 'Test Course', 'shortname' => 'TC', 'summary' => 'Summary of course', 'summaryformat' => 1, 'category' => 1, 'reason' => 'Because it would be nice.', 'requester' => $user->id, 'password' => '' ]; $DB->insert_record('course_request', $courserequest); // User session table data. $usersessions = (object) [ 'state' => 0, 'sid' => $this->generate_random_string(), // Needs a unique id. 'userid' => $user->id, 'sessdata' => 'Nothing', 'timecreated' => time(), 'timemodified' => time(), 'firstip' => '0.0.0.0', 'lastip' => '0.0.0.0' ]; $DB->insert_record('sessions', $usersessions); } /** * Create a random string. * * @param integer $length length of the string to generate. * @return string A random string. */ protected function generate_random_string($length = 6) { $response = ''; $source = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; if ($length > 0) { $response = ''; $source = str_split($source, 1); for ($i = 1; $i <= $length; $i++) { $num = mt_rand(1, count($source)); $response .= $source[$num - 1]; } } return $response; } } tests/userroleseditable_test.php 0000644 00000005734 15151162244 0013205 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user; /** * Unit tests for user roles editable class. * * @package core_user * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class userroleseditable_test extends \advanced_testcase { /** * Test user roles editable. */ public function test_update() { global $DB; $this->resetAfterTest(); // Create user and modify user profile. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course1->id); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); role_assign($teacherrole->id, $user1->id, $coursecontext->id); role_assign($teacherrole->id, $user2->id, $coursecontext->id); $this->setAdminUser(); accesslib_clear_all_caches_for_unit_testing(); // Use the userroleseditable api to remove all roles from user1 and give user2 student and teacher. $itemid = $course1->id . ':' . $user1->id; $newvalue = json_encode([]); $result = \core_user\output\user_roles_editable::update($itemid, $newvalue); $this->assertTrue($result instanceof \core_user\output\user_roles_editable); $currentroles = get_user_roles_in_course($user1->id, $course1->id); $this->assertEmpty($currentroles); $this->setAdminUser(); accesslib_clear_all_caches_for_unit_testing(); $itemid = $course1->id . ':' . $user2->id; $newvalue = json_encode([$teacherrole->id, $studentrole->id]); $result = \core_user\output\user_roles_editable::update($itemid, $newvalue); $this->assertTrue($result instanceof \core_user\output\user_roles_editable); $currentroles = get_user_roles_in_course($user2->id, $course1->id); $this->assertStringContainsString('Non-editing teacher', $currentroles); $this->assertStringContainsString('Student', $currentroles); } } tests/myprofile_test.php 0000644 00000037512 15151162244 0011475 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . "/user/tests/fixtures/myprofile_fixtures.php"); /** * Unit tests for core_user\output\myprofile * * @package core_user * @category test * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later (5) */ class myprofile_test extends \advanced_testcase { /** * Test node::__construct(). */ public function test_node__construct() { $node = new \core_user\output\myprofile\node('parentcat', 'nodename', 'nodetitle', 'after', 'www.google.com', 'description', new \pix_icon('i/course', ''), 'class1 class2'); $this->assertSame('parentcat', $node->parentcat); $this->assertSame('nodename', $node->name); $this->assertSame('nodetitle', $node->title); $this->assertSame('after', $node->after); $url = new \moodle_url('www.google.com'); $this->assertEquals($url, $node->url); $this->assertEquals(new \pix_icon('i/course', ''), $node->icon); $this->assertSame('class1 class2', $node->classes); } /** * Test category::node_add(). */ public function test_add_node() { $tree = new \core_user\output\myprofile\tree(); $category = new \core_user\output\myprofile\category('category', 'categorytitle'); $node = new \core_user\output\myprofile\node('category', 'nodename', 'nodetitle', null, 'www.iAmaZombie.com', 'description'); $category->add_node($node); $this->assertCount(1, $category->nodes); $node = new \core_user\output\myprofile\node('category', 'nodename2', 'nodetitle', null, 'www.WorldisGonnaEnd.com', 'description'); $category->add_node($node); $this->assertCount(2, $category->nodes); $node = new \core_user\output\myprofile\node('category', 'nodename3', 'nodetitle', null, 'www.TeamBeardsFTW.com', 'description'); $tree->add_node($node); $tree->add_category($category); $tree->sort_categories(); $category = $tree->categories['category']; $this->assertCount(3, $category->nodes); } /** * Test category::__construct(). */ public function test_category__construct() { $category = new \core_user\output\myprofile\category('categoryname', 'title', 'after', 'class1 class2'); $this->assertSame('categoryname', $category->name); $this->assertSame('title', $category->title); $this->assertSame('after', $category->after); $this->assertSame('class1 class2', $category->classes); } public function test_validate_after_order1() { $category = new \phpunit_fixture_myprofile_category('category', 'title', null); // Create nodes. $node1 = new \core_user\output\myprofile\node('category', 'node1', 'nodetitle', null, null, 'content'); $node2 = new \core_user\output\myprofile\node('category', 'node2', 'nodetitle', 'node1', null, 'content'); $node3 = new \core_user\output\myprofile\node('category', 'node3', 'nodetitle', 'node2', null, null); $category->add_node($node3); $category->add_node($node2); $category->add_node($node1); $this->expectException(\coding_exception::class); $category->validate_after_order(); } public function test_validate_after_order2() { $category = new \phpunit_fixture_myprofile_category('category', 'title', null); // Create nodes. $node1 = new \core_user\output\myprofile\node('category', 'node1', 'nodetitle', null, null, null); $node2 = new \core_user\output\myprofile\node('category', 'node2', 'nodetitle', 'node1', null, 'content'); $node3 = new \core_user\output\myprofile\node('category', 'node3', 'nodetitle', 'node2', null, null); $category->add_node($node3); $category->add_node($node2); $category->add_node($node1); $this->expectException(\coding_exception::class); $category->validate_after_order(); } /** * Test category::find_nodes_after(). */ public function test_find_nodes_after() { $category = new \phpunit_fixture_myprofile_category('category', 'title', null); // Create nodes. $node1 = new \core_user\output\myprofile\node('category', 'node1', 'nodetitle', null); $node2 = new \core_user\output\myprofile\node('category', 'node2', 'nodetitle', 'node1'); $node3 = new \core_user\output\myprofile\node('category', 'node3', 'nodetitle', 'node2'); $node4 = new \core_user\output\myprofile\node('category', 'node4', 'nodetitle', 'node3'); $node5 = new \core_user\output\myprofile\node('category', 'node5', 'nodetitle', 'node3'); $node6 = new \core_user\output\myprofile\node('category', 'node6', 'nodetitle', 'node1'); // Add the nodes in random order. $category->add_node($node3); $category->add_node($node2); $category->add_node($node4); $category->add_node($node1); $category->add_node($node5); $category->add_node($node6); // After node 1 we should have node2 - node3 - node4 - node5 - node6. $return = $category->find_nodes_after($node1); $this->assertCount(5, $return); $node = array_shift($return); $this->assertEquals($node2, $node); $node = array_shift($return); $this->assertEquals($node3, $node); $node = array_shift($return); $this->assertEquals($node4, $node); $node = array_shift($return); $this->assertEquals($node5, $node); $node = array_shift($return); $this->assertEquals($node6, $node); // Last check also verifies calls for all subsequent nodes, still do some random checking. $return = $category->find_nodes_after($node6); $this->assertCount(0, $return); $return = $category->find_nodes_after($node3); $this->assertCount(2, $return); } /** * Test category::sort_nodes(). */ public function test_sort_nodes1() { $category = new \phpunit_fixture_myprofile_category('category', 'title', null); // Create nodes. $node1 = new \core_user\output\myprofile\node('category', 'node1', 'nodetitle', null); $node2 = new \core_user\output\myprofile\node('category', 'node2', 'nodetitle', 'node1'); $node3 = new \core_user\output\myprofile\node('category', 'node3', 'nodetitle', 'node2'); $node4 = new \core_user\output\myprofile\node('category', 'node4', 'nodetitle', 'node3'); $node5 = new \core_user\output\myprofile\node('category', 'node5', 'nodetitle', 'node3'); $node6 = new \core_user\output\myprofile\node('category', 'node6', 'nodetitle', 'node1'); // Add the nodes in random order. $category->add_node($node3); $category->add_node($node2); $category->add_node($node4); $category->add_node($node1); $category->add_node($node5); $category->add_node($node6); // After node 1 we should have node2 - node3 - node4 - node5 - node6. $category->sort_nodes(); $nodes = $category->nodes; $this->assertCount(6, $nodes); $node = array_shift($nodes); $this->assertEquals($node1, $node); $node = array_shift($nodes); $this->assertEquals($node2, $node); $node = array_shift($nodes); $this->assertEquals($node3, $node); $node = array_shift($nodes); $this->assertEquals($node4, $node); $node = array_shift($nodes); $this->assertEquals($node5, $node); $node = array_shift($nodes); $this->assertEquals($node6, $node); // Last check also verifies calls for all subsequent nodes, still do some random checking. $return = $category->find_nodes_after($node6); $this->assertCount(0, $return); $return = $category->find_nodes_after($node3); $this->assertCount(2, $return); // Add a node with invalid 'after' and make sure an exception is thrown. $node7 = new \core_user\output\myprofile\node('category', 'node7', 'nodetitle', 'noderandom'); $category->add_node($node7); $this->expectException(\coding_exception::class); $category->sort_nodes(); } /** * Test category::sort_nodes() with a mix of content and non content nodes. */ public function test_sort_nodes2() { $category = new \phpunit_fixture_myprofile_category('category', 'title', null); // Create nodes. $node1 = new \core_user\output\myprofile\node('category', 'node1', 'nodetitle', null, null, 'content'); $node2 = new \core_user\output\myprofile\node('category', 'node2', 'nodetitle', 'node1', null, 'content'); $node3 = new \core_user\output\myprofile\node('category', 'node3', 'nodetitle', null); $node4 = new \core_user\output\myprofile\node('category', 'node4', 'nodetitle', 'node3'); $node5 = new \core_user\output\myprofile\node('category', 'node5', 'nodetitle', 'node3'); $node6 = new \core_user\output\myprofile\node('category', 'node6', 'nodetitle', 'node1', null, 'content'); // Add the nodes in random order. $category->add_node($node3); $category->add_node($node2); $category->add_node($node4); $category->add_node($node1); $category->add_node($node5); $category->add_node($node6); // After node 1 we should have node2 - node6 - node3 - node4 - node5. $category->sort_nodes(); $nodes = $category->nodes; $this->assertCount(6, $nodes); $node = array_shift($nodes); $this->assertEquals($node1, $node); $node = array_shift($nodes); $this->assertEquals($node2, $node); $node = array_shift($nodes); $this->assertEquals($node6, $node); $node = array_shift($nodes); $this->assertEquals($node3, $node); $node = array_shift($nodes); $this->assertEquals($node4, $node); $node = array_shift($nodes); $this->assertEquals($node5, $node); } /** * Test tree::add_node(). */ public function test_tree_add_node() { $tree = new \phpunit_fixture_myprofile_tree(); $node1 = new \core_user\output\myprofile\node('category', 'node1', 'nodetitle'); $tree->add_node($node1); $nodes = $tree->nodes; $node = array_shift($nodes); $this->assertEquals($node1, $node); // Can't add node with same name. $this->expectException(\coding_exception::class); $tree->add_node($node1); } /** * Test tree::add_category(). */ public function test_tree_add_category() { $tree = new \phpunit_fixture_myprofile_tree(); $category1 = new \core_user\output\myprofile\category('category', 'title'); $tree->add_category($category1); $categories = $tree->categories; $category = array_shift($categories); $this->assertEquals($category1, $category); // Can't add node with same name. $this->expectException(\coding_exception::class); $tree->add_category($category1); } /** * Test tree::find_categories_after(). */ public function test_find_categories_after() { $tree = new \phpunit_fixture_myprofile_tree('category', 'title', null); // Create categories. $category1 = new \core_user\output\myprofile\category('category1', 'category1', null); $category2 = new \core_user\output\myprofile\category('category2', 'category2', 'category1'); $category3 = new \core_user\output\myprofile\category('category3', 'category3', 'category2'); $category4 = new \core_user\output\myprofile\category('category4', 'category4', 'category3'); $category5 = new \core_user\output\myprofile\category('category5', 'category5', 'category3'); $category6 = new \core_user\output\myprofile\category('category6', 'category6', 'category1'); // Add the categories in random order. $tree->add_category($category3); $tree->add_category($category2); $tree->add_category($category4); $tree->add_category($category1); $tree->add_category($category5); $tree->add_category($category6); // After category 1 we should have category2 - category3 - category4 - category5 - category6. $return = $tree->find_categories_after($category1); $this->assertCount(5, $return); $category = array_shift($return); $this->assertEquals($category2, $category); $category = array_shift($return); $this->assertEquals($category3, $category); $category = array_shift($return); $this->assertEquals($category4, $category); $category = array_shift($return); $this->assertEquals($category5, $category); $category = array_shift($return); $this->assertEquals($category6, $category); // Last check also verifies calls for all subsequent categories, still do some random checking. $return = $tree->find_categories_after($category6); $this->assertCount(0, $return); $return = $tree->find_categories_after($category3); $this->assertCount(2, $return); } /** * Test tree::sort_categories(). */ public function test_sort_categories() { $tree = new \phpunit_fixture_myprofile_tree('category', 'title', null); // Create categories. $category1 = new \core_user\output\myprofile\category('category1', 'category1', null); $category2 = new \core_user\output\myprofile\category('category2', 'category2', 'category1'); $category3 = new \core_user\output\myprofile\category('category3', 'category3', 'category2'); $category4 = new \core_user\output\myprofile\category('category4', 'category4', 'category3'); $category5 = new \core_user\output\myprofile\category('category5', 'category5', 'category3'); $category6 = new \core_user\output\myprofile\category('category6', 'category6', 'category1'); // Add the categories in random order. $tree->add_category($category3); $tree->add_category($category2); $tree->add_category($category4); $tree->add_category($category1); $tree->add_category($category5); $tree->add_category($category6); // After category 1 we should have category2 - category3 - category4 - category5 - category6. $tree->sort_categories(); $categories = $tree->categories; $this->assertCount(6, $categories); $category = array_shift($categories); $this->assertEquals($category1, $category); $category = array_shift($categories); $this->assertEquals($category2, $category); $category = array_shift($categories); $this->assertEquals($category3, $category); $category = array_shift($categories); $this->assertEquals($category4, $category); $category = array_shift($categories); $this->assertEquals($category5, $category); $category = array_shift($categories); $this->assertEquals($category6, $category); // Can't add category with same name. $this->expectException(\coding_exception::class); $tree->add_category($category1); } } tests/userselector_test.php 0000644 00000025041 15151162244 0012200 0 ustar 00 <?php // This file is part of Moodle - https://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user; use testable_user_selector; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/user/selector/lib.php'); require_once($CFG->dirroot.'/user/tests/fixtures/testable_user_selector.php'); /** * Tests for the implementation of {@link user_selector_base} class. * * @package core_user * @category test * @copyright 2018 David Mudrák <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class userselector_test extends \advanced_testcase { /** * Setup the environment for the tests. */ protected function setup_hidden_siteidentity() { global $CFG, $DB; $CFG->showuseridentity = 'idnumber,country,city'; $CFG->hiddenuserfields = 'country,city'; $env = new \stdClass(); $env->student = $this->getDataGenerator()->create_user(); $env->teacher = $this->getDataGenerator()->create_user(); $env->manager = $this->getDataGenerator()->create_user(); $env->course = $this->getDataGenerator()->create_course(); $env->coursecontext = \context_course::instance($env->course->id); $env->teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); $env->studentrole = $DB->get_record('role', array('shortname' => 'student')); $env->managerrole = $DB->get_record('role', array('shortname' => 'manager')); role_assign($env->studentrole->id, $env->student->id, $env->coursecontext->id); role_assign($env->teacherrole->id, $env->teacher->id, $env->coursecontext->id); role_assign($env->managerrole->id, $env->manager->id, SYSCONTEXTID); return $env; } /** * No identity fields are not shown to student user (no permission to view identity fields). */ public function test_hidden_siteidentity_fields_no_access() { $this->resetAfterTest(); $env = $this->setup_hidden_siteidentity(); $this->setUser($env->student); $selector = new testable_user_selector('test'); foreach ($selector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectNotHasAttribute('idnumber', $user); $this->assertObjectNotHasAttribute('country', $user); $this->assertObjectNotHasAttribute('city', $user); } } } /** * Teacher can see students' identity fields only within the course. */ public function test_hidden_siteidentity_fields_course_only_access() { $this->resetAfterTest(); $env = $this->setup_hidden_siteidentity(); $this->setUser($env->teacher); $systemselector = new testable_user_selector('test'); $courseselector = new testable_user_selector('test', ['accesscontext' => $env->coursecontext]); foreach ($systemselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectNotHasAttribute('idnumber', $user); $this->assertObjectNotHasAttribute('country', $user); $this->assertObjectNotHasAttribute('city', $user); } } foreach ($courseselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectHasAttribute('idnumber', $user); $this->assertObjectHasAttribute('country', $user); $this->assertObjectHasAttribute('city', $user); } } } /** * Teacher can be prevented from seeing students' identity fields even within the course. */ public function test_hidden_siteidentity_fields_course_prevented_access() { $this->resetAfterTest(); $env = $this->setup_hidden_siteidentity(); $this->setUser($env->teacher); assign_capability('moodle/course:viewhiddenuserfields', CAP_PREVENT, $env->teacherrole->id, $env->coursecontext->id); $courseselector = new testable_user_selector('test', ['accesscontext' => $env->coursecontext]); foreach ($courseselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectHasAttribute('idnumber', $user); $this->assertObjectNotHasAttribute('country', $user); $this->assertObjectNotHasAttribute('city', $user); } } } /** * Manager can see students' identity fields anywhere. */ public function test_hidden_siteidentity_fields_anywhere_access() { $this->resetAfterTest(); $env = $this->setup_hidden_siteidentity(); $this->setUser($env->manager); $systemselector = new testable_user_selector('test'); $courseselector = new testable_user_selector('test', ['accesscontext' => $env->coursecontext]); foreach ($systemselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectHasAttribute('idnumber', $user); $this->assertObjectHasAttribute('country', $user); $this->assertObjectHasAttribute('city', $user); } } foreach ($courseselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectHasAttribute('idnumber', $user); $this->assertObjectHasAttribute('country', $user); $this->assertObjectHasAttribute('city', $user); } } } /** * Manager can be prevented from seeing hidden fields outside the course. */ public function test_hidden_siteidentity_fields_schismatic_access() { $this->resetAfterTest(); $env = $this->setup_hidden_siteidentity(); $this->setUser($env->manager); // Revoke the capability to see hidden user fields outside the course. // Note that inside the course, the manager can still see the hidden identifiers as this is currently // controlled by a separate capability for legacy reasons. This is counter-intuitive behaviour and is // likely to be fixed in MDL-51630. assign_capability('moodle/user:viewhiddendetails', CAP_PREVENT, $env->managerrole->id, SYSCONTEXTID, true); $systemselector = new testable_user_selector('test'); $courseselector = new testable_user_selector('test', ['accesscontext' => $env->coursecontext]); foreach ($systemselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectHasAttribute('idnumber', $user); $this->assertObjectNotHasAttribute('country', $user); $this->assertObjectNotHasAttribute('city', $user); } } foreach ($courseselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectHasAttribute('idnumber', $user); $this->assertObjectHasAttribute('country', $user); $this->assertObjectHasAttribute('city', $user); } } } /** * Two capabilities must be currently set to prevent manager from seeing hidden fields. */ public function test_hidden_siteidentity_fields_hard_to_prevent_access() { $this->resetAfterTest(); $env = $this->setup_hidden_siteidentity(); $this->setUser($env->manager); assign_capability('moodle/user:viewhiddendetails', CAP_PREVENT, $env->managerrole->id, SYSCONTEXTID, true); assign_capability('moodle/course:viewhiddenuserfields', CAP_PREVENT, $env->managerrole->id, SYSCONTEXTID, true); $systemselector = new testable_user_selector('test'); $courseselector = new testable_user_selector('test', ['accesscontext' => $env->coursecontext]); foreach ($systemselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectHasAttribute('idnumber', $user); $this->assertObjectNotHasAttribute('country', $user); $this->assertObjectNotHasAttribute('city', $user); } } foreach ($courseselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectHasAttribute('idnumber', $user); $this->assertObjectNotHasAttribute('country', $user); $this->assertObjectNotHasAttribute('city', $user); } } } /** * For legacy reasons, user selectors supported ability to override $CFG->showuseridentity. * * However, this was found as violating the principle of respecting site privacy settings. So the feature has been * dropped in Moodle 3.6. */ public function test_hidden_siteidentity_fields_explicit_extrafields() { $this->resetAfterTest(); $env = $this->setup_hidden_siteidentity(); $this->setUser($env->manager); $implicitselector = new testable_user_selector('test'); $explicitselector = new testable_user_selector('test', ['extrafields' => ['email', 'department']]); $this->assertDebuggingCalled(); foreach ($implicitselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectHasAttribute('idnumber', $user); $this->assertObjectHasAttribute('country', $user); $this->assertObjectHasAttribute('city', $user); $this->assertObjectNotHasAttribute('email', $user); $this->assertObjectNotHasAttribute('department', $user); } } foreach ($explicitselector->find_users('') as $found) { foreach ($found as $user) { $this->assertObjectHasAttribute('idnumber', $user); $this->assertObjectHasAttribute('country', $user); $this->assertObjectHasAttribute('city', $user); $this->assertObjectNotHasAttribute('email', $user); $this->assertObjectNotHasAttribute('department', $user); } } } } tests/fields_test.php 0000644 00000066015 15151162244 0010735 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user; /** * Unit tests for \core_user\fields * * @package core * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class fields_test extends \advanced_testcase { /** * Tests getting the user picture fields. */ public function test_get_picture_fields() { $this->assertEquals(['id', 'picture', 'firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename', 'alternatename', 'imagealt', 'email'], fields::get_picture_fields()); } /** * Tests getting the user name fields. */ public function test_get_name_fields() { $this->assertEquals(['firstnamephonetic', 'lastnamephonetic', 'middlename', 'alternatename', 'firstname', 'lastname'], fields::get_name_fields()); $this->assertEquals(['firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename', 'alternatename'], fields::get_name_fields(true)); } /** * Tests getting the identity fields. */ public function test_get_identity_fields() { global $DB, $CFG, $COURSE; $this->resetAfterTest(); require_once($CFG->dirroot . '/user/profile/lib.php'); // Create custom profile fields, one with each visibility option. $generator = self::getDataGenerator(); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'a', 'name' => 'A', 'visible' => PROFILE_VISIBLE_ALL]); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'b', 'name' => 'B', 'visible' => PROFILE_VISIBLE_PRIVATE]); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'c', 'name' => 'C', 'visible' => PROFILE_VISIBLE_NONE]); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'd', 'name' => 'D', 'visible' => PROFILE_VISIBLE_TEACHERS]); // Set the extra user fields to include email, department, and all custom profile fields. set_config('showuseridentity', 'email,department,profile_field_a,profile_field_b,' . 'profile_field_c,profile_field_d'); set_config('hiddenuserfields', 'email'); // Create a test course and a student in the course. $course = $generator->create_course(); $coursecontext = \context_course::instance($course->id); $user = $generator->create_user(); $anotheruser = $generator->create_user(); $usercontext = \context_user::instance($anotheruser->id); $generator->enrol_user($user->id, $course->id, 'student'); // When no context is provided, it does no access checks and should return all specified (other than non-visible). $this->assertEquals(['email', 'department', 'profile_field_a', 'profile_field_b', 'profile_field_d'], fields::get_identity_fields(null)); // If you turn off custom profile fields, you don't get those. $this->assertEquals(['email', 'department'], fields::get_identity_fields(null, false)); // Request in context as an administator. $this->setAdminUser(); $this->assertEquals(['email', 'department', 'profile_field_a', 'profile_field_b', 'profile_field_c', 'profile_field_d'], fields::get_identity_fields($coursecontext)); $this->assertEquals(['email', 'department'], fields::get_identity_fields($coursecontext, false)); // Request in context as a student - they don't have any of the capabilities to see identity // fields or profile fields. $this->setUser($user); $this->assertEquals([], fields::get_identity_fields($coursecontext)); // Give the student the basic identity fields permission (also makes them count as 'teacher' // for the teacher-restricted field). $COURSE = $course; // Horrible hack, because PROFILE_VISIBLE_TEACHERS relies on this global. $roleid = $DB->get_field('role', 'id', ['shortname' => 'student']); role_change_permission($roleid, $coursecontext, 'moodle/site:viewuseridentity', CAP_ALLOW); $this->assertEquals(['department', 'profile_field_a', 'profile_field_d'], fields::get_identity_fields($coursecontext)); $this->assertEquals(['department'], fields::get_identity_fields($coursecontext, false)); // Give them permission to view hidden user fields. role_change_permission($roleid, $coursecontext, 'moodle/course:viewhiddenuserfields', CAP_ALLOW); $this->assertEquals(['email', 'department', 'profile_field_a', 'profile_field_d'], fields::get_identity_fields($coursecontext)); $this->assertEquals(['email', 'department'], fields::get_identity_fields($coursecontext, false)); // Also give them permission to view all profile fields. role_change_permission($roleid, $coursecontext, 'moodle/user:viewalldetails', CAP_ALLOW); $this->assertEquals(['email', 'department', 'profile_field_a', 'profile_field_b', 'profile_field_c', 'profile_field_d'], fields::get_identity_fields($coursecontext)); $this->assertEquals(['email', 'department'], fields::get_identity_fields($coursecontext, false)); // Even if we give them student role in the user context they can't view anything... $generator->role_assign($roleid, $user->id, $usercontext->id); $this->assertEquals([], fields::get_identity_fields($usercontext)); // Give them basic permission. role_change_permission($roleid, $usercontext, 'moodle/site:viewuseridentity', CAP_ALLOW); $this->assertEquals(['department', 'profile_field_a', 'profile_field_d'], fields::get_identity_fields($usercontext)); $this->assertEquals(['department'], fields::get_identity_fields($usercontext, false)); // Give them the hidden user fields permission (it's a different one). role_change_permission($roleid, $usercontext, 'moodle/user:viewhiddendetails', CAP_ALLOW); $this->assertEquals(['email', 'department', 'profile_field_a', 'profile_field_d'], fields::get_identity_fields($usercontext)); $this->assertEquals(['email', 'department'], fields::get_identity_fields($usercontext, false)); // Also give them permission to view all profile fields. role_change_permission($roleid, $usercontext, 'moodle/user:viewalldetails', CAP_ALLOW); $this->assertEquals(['email', 'department', 'profile_field_a', 'profile_field_b', 'profile_field_c', 'profile_field_d'], fields::get_identity_fields($usercontext)); $this->assertEquals(['email', 'department'], fields::get_identity_fields($usercontext, false)); } /** * Tests the get_required_fields function. * * This function composes the results of get_identity/name/picture_fields, so we are not going * to test the details of the identity permissions as that was already covered. Just how they * are included/combined. */ public function test_get_required_fields() { $this->resetAfterTest(); // Set up some profile fields. $generator = self::getDataGenerator(); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'a', 'name' => 'A']); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'b', 'name' => 'B']); set_config('showuseridentity', 'email,department,profile_field_a'); // What happens if you don't ask for anything? $fields = fields::empty(); $this->assertEquals([], $fields->get_required_fields()); // Try each invidual purpose. $fields = fields::for_identity(null); $this->assertEquals(['email', 'department', 'profile_field_a'], $fields->get_required_fields()); $fields = fields::for_userpic(); $this->assertEquals(fields::get_picture_fields(), $fields->get_required_fields()); $fields = fields::for_name(); $this->assertEquals(fields::get_name_fields(), $fields->get_required_fields()); // Try combining them all. There should be no duplicates (e.g. email), and the 'id' field // should be moved to the start. $fields = fields::for_identity(null)->with_name()->with_userpic(); $this->assertEquals(['id', 'email', 'department', 'profile_field_a', 'picture', 'firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename', 'alternatename', 'imagealt'], $fields->get_required_fields()); // Add some specified fields to a default result. $fields = fields::for_identity(null, true)->including('city', 'profile_field_b'); $this->assertEquals(['email', 'department', 'profile_field_a', 'city', 'profile_field_b'], $fields->get_required_fields()); // Remove some fields, one of which actually is in the list. $fields = fields::for_identity(null, true)->excluding('email', 'city'); $this->assertEquals(['department', 'profile_field_a'], $fields->get_required_fields()); // Add and remove fields. $fields = fields::for_identity(null, true)->including('city', 'profile_field_b')->excluding('city', 'department'); $this->assertEquals(['email', 'profile_field_a', 'profile_field_b'], $fields->get_required_fields()); // Request the list without profile fields, check that still works with both sources. $fields = fields::for_identity(null, false)->including('city', 'profile_field_b')->excluding('city', 'department'); $this->assertEquals(['email'], $fields->get_required_fields()); } /** * Tests the get_required_fields function when you use the $limitpurposes parameter. */ public function test_get_required_fields_limitpurposes() { $this->resetAfterTest(); // Set up some profile fields. $generator = self::getDataGenerator(); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'a', 'name' => 'A']); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'b', 'name' => 'B']); set_config('showuseridentity', 'email,department,profile_field_a'); // Create a fields object with all three purposes, plus included and excluded fields. $fields = fields::for_identity(null, true)->with_name()->with_userpic() ->including('city', 'profile_field_b')->excluding('firstnamephonetic', 'middlename', 'alternatename'); // Check the result with all purposes. $this->assertEquals(['id', 'email', 'department', 'profile_field_a', 'picture', 'firstname', 'lastname', 'lastnamephonetic', 'imagealt', 'city', 'profile_field_b'], $fields->get_required_fields([fields::PURPOSE_IDENTITY, fields::PURPOSE_NAME, fields::PURPOSE_USERPIC, fields::CUSTOM_INCLUDE])); // Limit to identity and custom includes. $this->assertEquals(['email', 'department', 'profile_field_a', 'city', 'profile_field_b'], $fields->get_required_fields([fields::PURPOSE_IDENTITY, fields::CUSTOM_INCLUDE])); // Limit to name fields. $this->assertEquals(['firstname', 'lastname', 'lastnamephonetic'], $fields->get_required_fields([fields::PURPOSE_NAME])); } /** * There should be an exception if you try to 'limit' purposes to one that wasn't even included. */ public function test_get_required_fields_limitpurposes_not_in_constructor() { $fields = fields::for_identity(null); $this->expectExceptionMessage('$limitpurposes can only include purposes defined in object'); $fields->get_required_fields([fields::PURPOSE_USERPIC]); } /** * Sets up data and a fields object for all the get_sql tests. * * @return fields Constructed fields object for testing */ protected function init_for_sql_tests(): fields { $generator = self::getDataGenerator(); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'a', 'name' => 'A']); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'b', 'name' => 'B']); // Create a couple of users. One doesn't have a profile field set, so we can test that. $generator->create_user(['profile_field_a' => 'A1', 'profile_field_b' => 'B1', 'city' => 'C1', 'department' => 'D1', 'email' => 'e1@example.org', 'idnumber' => 'XXX1', 'username' => 'u1']); $generator->create_user(['profile_field_a' => 'A2', 'city' => 'C2', 'department' => 'D2', 'email' => 'e2@example.org', 'idnumber' => 'XXX2', 'username' => 'u2']); // It doesn't matter how we construct it (we already tested get_required_fields which is // where all those values are actually used) so let's just list the fields we want manually. return fields::empty()->including('department', 'city', 'profile_field_a', 'profile_field_b'); } /** * Tests getting SQL (and actually using it). */ public function test_get_sql_variations() { global $DB; $this->resetAfterTest(); $fields = $this->init_for_sql_tests(); fields::reset_unique_identifier(); // Basic SQL. ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams, 'mappings' => $mappings] = (array)$fields->get_sql(); $sql = "SELECT idnumber $selects FROM {user} $joins WHERE idnumber LIKE ? ORDER BY idnumber"; $records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%'])); $this->assertCount(2, $records); $expected1 = (object)['profile_field_a' => 'A1', 'profile_field_b' => 'B1', 'city' => 'C1', 'department' => 'D1', 'idnumber' => 'XXX1']; $expected2 = (object)['profile_field_a' => 'A2', 'profile_field_b' => null, 'city' => 'C2', 'department' => 'D2', 'idnumber' => 'XXX2']; $this->assertEquals($expected1, $records['XXX1']); $this->assertEquals($expected2, $records['XXX2']); $this->assertEquals([ 'department' => '{user}.department', 'city' => '{user}.city', 'profile_field_a' => $DB->sql_compare_text('uf1d_1.data', 255), 'profile_field_b' => $DB->sql_compare_text('uf1d_2.data', 255)], $mappings); // SQL using named params. ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] = (array)$fields->get_sql('', true); $sql = "SELECT idnumber $selects FROM {user} $joins WHERE idnumber LIKE :idnum ORDER BY idnumber"; $records = $DB->get_records_sql($sql, array_merge($joinparams, ['idnum' => 'X%'])); $this->assertCount(2, $records); $this->assertEquals($expected1, $records['XXX1']); $this->assertEquals($expected2, $records['XXX2']); // SQL using alias for user table. ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams, 'mappings' => $mappings] = (array)$fields->get_sql('u'); $sql = "SELECT idnumber $selects FROM {user} u $joins WHERE idnumber LIKE ? ORDER BY idnumber"; $records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%'])); $this->assertCount(2, $records); $this->assertEquals($expected1, $records['XXX1']); $this->assertEquals($expected2, $records['XXX2']); $this->assertEquals([ 'department' => 'u.department', 'city' => 'u.city', 'profile_field_a' => $DB->sql_compare_text('uf3d_1.data', 255), 'profile_field_b' => $DB->sql_compare_text('uf3d_2.data', 255)], $mappings); // Returning prefixed fields. ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] = (array)$fields->get_sql('', false, 'u_'); $sql = "SELECT idnumber $selects FROM {user} $joins WHERE idnumber LIKE ? ORDER BY idnumber"; $records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%'])); $this->assertCount(2, $records); $expected1 = (object)['u_profile_field_a' => 'A1', 'u_profile_field_b' => 'B1', 'u_city' => 'C1', 'u_department' => 'D1', 'idnumber' => 'XXX1']; $this->assertEquals($expected1, $records['XXX1']); // Renaming the id field. We need to use a different set of fields so it actually has the // id field. $fields = fields::for_userpic(); ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] = (array)$fields->get_sql('', false, '', 'userid'); $sql = "SELECT idnumber $selects FROM {user} $joins WHERE idnumber LIKE ? ORDER BY idnumber"; $records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%'])); $this->assertCount(2, $records); // User id was renamed. $this->assertObjectNotHasAttribute('id', $records['XXX1']); $this->assertObjectHasAttribute('userid', $records['XXX1']); // Other fields are normal (just try a couple). $this->assertObjectHasAttribute('firstname', $records['XXX1']); $this->assertObjectHasAttribute('imagealt', $records['XXX1']); // Check the user id is actually right. $this->assertEquals('XXX1', $DB->get_field('user', 'idnumber', ['id' => $records['XXX1']->userid])); // Rename the id field and also use a prefix. ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] = (array)$fields->get_sql('', false, 'u_', 'userid'); $sql = "SELECT idnumber $selects FROM {user} $joins WHERE idnumber LIKE ? ORDER BY idnumber"; $records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%'])); $this->assertCount(2, $records); // User id was renamed. $this->assertObjectNotHasAttribute('id', $records['XXX1']); $this->assertObjectNotHasAttribute('u_id', $records['XXX1']); $this->assertObjectHasAttribute('userid', $records['XXX1']); // Other fields are prefixed (just try a couple). $this->assertObjectHasAttribute('u_firstname', $records['XXX1']); $this->assertObjectHasAttribute('u_imagealt', $records['XXX1']); // Without a leading comma. ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] = (array)$fields->get_sql('', false, '', '', false); $sql = "SELECT $selects FROM {user} $joins WHERE idnumber LIKE ? ORDER BY idnumber"; $records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%'])); $this->assertCount(2, $records); foreach ($records as $key => $record) { // ID should be the first field used by get_records_sql. $this->assertEquals($key, $record->id); // Check 2 other sample properties. $this->assertObjectHasAttribute('firstname', $record); $this->assertObjectHasAttribute('imagealt', $record); } } /** * Tests what happens if you use the SQL multiple times in a query (i.e. that it correctly * creates the different identifiers). */ public function test_get_sql_multiple() { global $DB; $this->resetAfterTest(); $fields = $this->init_for_sql_tests(); // Inner SQL. ['selects' => $selects1, 'joins' => $joins1, 'params' => $joinparams1] = (array)$fields->get_sql('u1', true); // Outer SQL. $fields2 = fields::empty()->including('profile_field_a', 'email'); ['selects' => $selects2, 'joins' => $joins2, 'params' => $joinparams2] = (array)$fields2->get_sql('u2', true); // Crazy combined query. $sql = "SELECT username, details.profile_field_b AS innerb, details.city AS innerc $selects2 FROM {user} u2 $joins2 LEFT JOIN ( SELECT u1.id $selects1 FROM {user} u1 $joins1 WHERE idnumber LIKE :idnum ) details ON details.id = u2.id ORDER BY username"; $records = $DB->get_records_sql($sql, array_merge($joinparams1, $joinparams2, ['idnum' => 'X%'])); // The left join won't match for admin. $this->assertNull($records['admin']->innerb); $this->assertNull($records['admin']->innerc); // It should match for one of the test users though. $expected1 = (object)['username' => 'u1', 'innerb' => 'B1', 'innerc' => 'C1', 'profile_field_a' => 'A1', 'email' => 'e1@example.org']; $this->assertEquals($expected1, $records['u1']); } /** * Tests the get_sql function when there are no fields to retrieve. */ public function test_get_sql_nothing() { $fields = fields::empty(); ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] = (array)$fields->get_sql(); $this->assertEquals('', $selects); $this->assertEquals('', $joins); $this->assertEquals([], $joinparams); } /** * Tests get_sql when there are no custom fields; in this scenario, the joins and joinparams * are always blank. */ public function test_get_sql_no_custom_fields() { $fields = fields::empty()->including('city', 'country'); ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams, 'mappings' => $mappings] = (array)$fields->get_sql('u'); $this->assertEquals(', u.city, u.country', $selects); $this->assertEquals('', $joins); $this->assertEquals([], $joinparams); $this->assertEquals(['city' => 'u.city', 'country' => 'u.country'], $mappings); } /** * Tests the format of the $selects string, which is important particularly for backward * compatibility. */ public function test_get_sql_selects_format() { global $DB; $this->resetAfterTest(); fields::reset_unique_identifier(); $generator = self::getDataGenerator(); $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'a', 'name' => 'A']); // When we list fields that include custom profile fields... $fields = fields::empty()->including('id', 'profile_field_a'); // Supplying an alias: all fields have alias. $selects = $fields->get_sql('u')->selects; $this->assertEquals(', u.id, ' . $DB->sql_compare_text('uf1d_1.data', 255) . ' AS profile_field_a', $selects); // No alias: all files have {user} because of the joins. $selects = $fields->get_sql()->selects; $this->assertEquals(', {user}.id, ' . $DB->sql_compare_text('uf2d_1.data', 255) . ' AS profile_field_a', $selects); // When the list doesn't include custom profile fields... $fields = fields::empty()->including('id', 'city'); // Supplying an alias: all fields have alias. $selects = $fields->get_sql('u')->selects; $this->assertEquals(', u.id, u.city', $selects); // No alias: fields do not have alias at all. $selects = $fields->get_sql()->selects; $this->assertEquals(', id, city', $selects); } /** * Data provider for {@see test_get_sql_fullname} * * @return array */ public function get_sql_fullname_provider(): array { return [ ['firstname lastname', 'FN LN'], ['lastname, firstname', 'LN, FN'], ['alternatename \'middlename\' lastname!', 'AN \'MN\' LN!'], ['[firstname lastname alternatename]', '[FN LN AN]'], ['firstnamephonetic lastnamephonetic', 'FNP LNP'], ['firstname alternatename lastname', 'FN AN LN'], ]; } /** * Test sql_fullname_display method with various fullname formats * * @param string $fullnamedisplay * @param string $expectedfullname * * @dataProvider get_sql_fullname_provider */ public function test_get_sql_fullname(string $fullnamedisplay, string $expectedfullname): void { global $DB; $this->resetAfterTest(); set_config('fullnamedisplay', $fullnamedisplay); $user = $this->getDataGenerator()->create_user([ 'firstname' => 'FN', 'lastname' => 'LN', 'firstnamephonetic' => 'FNP', 'lastnamephonetic' => 'LNP', 'middlename' => 'MN', 'alternatename' => 'AN', ]); [$sqlfullname, $params] = fields::get_sql_fullname('u'); $fullname = $DB->get_field_sql("SELECT {$sqlfullname} FROM {user} u WHERE u.id = :id", $params + [ 'id' => $user->id, ]); $this->assertEquals($expectedfullname, $fullname); } /** * Test sql_fullname_display when one of the configured name fields is null */ public function test_get_sql_fullname_null_field(): void { global $DB; $this->resetAfterTest(); set_config('fullnamedisplay', 'firstname lastname alternatename'); $user = $this->getDataGenerator()->create_user([ 'firstname' => 'FN', 'lastname' => 'LN', ]); // Set alternatename field to null, ensure we still get result in later assertion. $user->alternatename = null; user_update_user($user, false); [$sqlfullname, $params] = fields::get_sql_fullname('u'); $fullname = $DB->get_field_sql("SELECT {$sqlfullname} FROM {user} u WHERE u.id = :id", $params + [ 'id' => $user->id, ]); $this->assertEquals('FN LN ', $fullname); } } tests/externallib_test.php 0000644 00000226502 15151162244 0011777 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * User external PHPunit tests * * @package core_user * @category external * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.4 */ namespace core_user; use core_files_external; use core_user_external; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/user/externallib.php'); require_once($CFG->dirroot . '/files/externallib.php'); class externallib_test extends externallib_advanced_testcase { /** * Test get_users */ public function test_get_users() { global $USER, $CFG; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $user1 = array( 'username' => 'usernametest1', 'idnumber' => 'idnumbertest1', 'firstname' => 'First Name User Test 1', 'lastname' => 'Last Name User Test 1', 'email' => 'usertest1@example.com', 'address' => '2 Test Street Perth 6000 WA', 'phone1' => '01010101010', 'phone2' => '02020203', 'department' => 'Department of user 1', 'institution' => 'Institution of user 1', 'description' => 'This is a description for user 1', 'descriptionformat' => FORMAT_MOODLE, 'city' => 'Perth', 'country' => 'AU' ); $user1 = self::getDataGenerator()->create_user($user1); set_config('usetags', 1); require_once($CFG->dirroot . '/user/editlib.php'); $user1->interests = array('Cinema', 'Tennis', 'Dance', 'Guitar', 'Cooking'); useredit_update_interests($user1, $user1->interests); $user2 = self::getDataGenerator()->create_user( array('username' => 'usernametest2', 'idnumber' => 'idnumbertest2')); $generatedusers = array(); $generatedusers[$user1->id] = $user1; $generatedusers[$user2->id] = $user2; $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/user:viewdetails', $context->id); // Enrol the users in the course. $this->getDataGenerator()->enrol_user($user1->id, $course->id, $roleid); $this->getDataGenerator()->enrol_user($user2->id, $course->id, $roleid); $this->getDataGenerator()->enrol_user($USER->id, $course->id, $roleid); // call as admin and receive all possible fields. $this->setAdminUser(); $searchparams = array( array('key' => 'invalidkey', 'value' => 'invalidkey'), array('key' => 'email', 'value' => $user1->email), array('key' => 'firstname', 'value' => $user1->firstname)); // Call the external function. $result = core_user_external::get_users($searchparams); // We need to execute the return values cleaning process to simulate the web service server $result = \external_api::clean_returnvalue(core_user_external::get_users_returns(), $result); // Check we retrieve the good total number of enrolled users + no error on capability. $expectedreturnedusers = 1; $returnedusers = $result['users']; $this->assertEquals($expectedreturnedusers, count($returnedusers)); foreach($returnedusers as $returneduser) { $generateduser = ($returneduser['id'] == $USER->id) ? $USER : $generatedusers[$returneduser['id']]; $this->assertEquals($generateduser->username, $returneduser['username']); if (!empty($generateduser->idnumber)) { $this->assertEquals($generateduser->idnumber, $returneduser['idnumber']); } $this->assertEquals($generateduser->firstname, $returneduser['firstname']); $this->assertEquals($generateduser->lastname, $returneduser['lastname']); if ($generateduser->email != $USER->email) { // Don't check the tmp modified $USER email. $this->assertEquals($generateduser->email, $returneduser['email']); } if (!empty($generateduser->address)) { $this->assertEquals($generateduser->address, $returneduser['address']); } if (!empty($generateduser->phone1)) { $this->assertEquals($generateduser->phone1, $returneduser['phone1']); } if (!empty($generateduser->phone2)) { $this->assertEquals($generateduser->phone2, $returneduser['phone2']); } if (!empty($generateduser->department)) { $this->assertEquals($generateduser->department, $returneduser['department']); } if (!empty($generateduser->institution)) { $this->assertEquals($generateduser->institution, $returneduser['institution']); } if (!empty($generateduser->description)) { $this->assertEquals($generateduser->description, $returneduser['description']); } if (!empty($generateduser->descriptionformat)) { $this->assertEquals(FORMAT_HTML, $returneduser['descriptionformat']); } if (!empty($generateduser->city)) { $this->assertEquals($generateduser->city, $returneduser['city']); } if (!empty($generateduser->country)) { $this->assertEquals($generateduser->country, $returneduser['country']); } if (!empty($CFG->usetags) and !empty($generateduser->interests)) { $this->assertEquals(implode(', ', $generateduser->interests), $returneduser['interests']); } } // Test the invalid key warning. $warnings = $result['warnings']; $this->assertEquals(count($warnings), 1); $warning = array_pop($warnings); $this->assertEquals($warning['item'], 'invalidkey'); $this->assertEquals($warning['warningcode'], 'invalidfieldparameter'); // Test sending twice the same search field. try { $searchparams = array( array('key' => 'firstname', 'value' => 'Canard'), array('key' => 'email', 'value' => $user1->email), array('key' => 'firstname', 'value' => $user1->firstname)); // Call the external function. $result = core_user_external::get_users($searchparams); $this->fail('Expecting \'keyalreadyset\' moodle_exception to be thrown.'); } catch (\moodle_exception $e) { $this->assertEquals('keyalreadyset', $e->errorcode); } catch (\Exception $e) { $this->fail('Expecting \'keyalreadyset\' moodle_exception to be thrown.'); } } /** * Test get_users_by_field */ public function test_get_users_by_field() { global $USER, $CFG; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); $user1 = array( 'username' => 'usernametest1', 'idnumber' => 'idnumbertest1', 'firstname' => 'First Name User Test 1', 'lastname' => 'Last Name User Test 1', 'email' => 'usertest1@example.com', 'address' => '2 Test Street Perth 6000 WA', 'phone1' => '01010101010', 'phone2' => '02020203', 'department' => 'Department of user 1', 'institution' => 'Institution of user 1', 'description' => 'This is a description for user 1', 'descriptionformat' => FORMAT_MOODLE, 'city' => 'Perth', 'country' => 'AU', ); $user1 = self::getDataGenerator()->create_user($user1); if (!empty($CFG->usetags)) { require_once($CFG->dirroot . '/user/editlib.php'); $user1->interests = array('Cinema', 'Tennis', 'Dance', 'Guitar', 'Cooking'); useredit_update_interests($user1, $user1->interests); } $user2 = self::getDataGenerator()->create_user( array('username' => 'usernametest2', 'idnumber' => 'idnumbertest2')); $generatedusers = array(); $generatedusers[$user1->id] = $user1; $generatedusers[$user2->id] = $user2; $context = \context_course::instance($course->id); $roleid = $this->assignUserCapability('moodle/user:viewdetails', $context->id); // Enrol the users in the course. $this->getDataGenerator()->enrol_user($user1->id, $course->id, $roleid, 'manual'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, $roleid, 'manual'); $this->getDataGenerator()->enrol_user($USER->id, $course->id, $roleid, 'manual'); // call as admin and receive all possible fields. $this->setAdminUser(); $fieldstosearch = array('id', 'idnumber', 'username', 'email'); foreach ($fieldstosearch as $fieldtosearch) { // Call the external function. $returnedusers = core_user_external::get_users_by_field($fieldtosearch, array($USER->{$fieldtosearch}, $user1->{$fieldtosearch}, $user2->{$fieldtosearch})); $returnedusers = \external_api::clean_returnvalue(core_user_external::get_users_by_field_returns(), $returnedusers); // Expected result differ following the searched field // Admin user in the PHPunit framework doesn't have an idnumber. if ($fieldtosearch == 'idnumber') { $expectedreturnedusers = 2; } else { $expectedreturnedusers = 3; } // Check we retrieve the good total number of enrolled users + no error on capability. $this->assertEquals($expectedreturnedusers, count($returnedusers)); foreach($returnedusers as $returneduser) { $generateduser = ($returneduser['id'] == $USER->id) ? $USER : $generatedusers[$returneduser['id']]; $this->assertEquals($generateduser->username, $returneduser['username']); if (!empty($generateduser->idnumber)) { $this->assertEquals($generateduser->idnumber, $returneduser['idnumber']); } $this->assertEquals($generateduser->firstname, $returneduser['firstname']); $this->assertEquals($generateduser->lastname, $returneduser['lastname']); if ($generateduser->email != $USER->email) { //don't check the tmp modified $USER email $this->assertEquals($generateduser->email, $returneduser['email']); } if (!empty($generateduser->address)) { $this->assertEquals($generateduser->address, $returneduser['address']); } if (!empty($generateduser->phone1)) { $this->assertEquals($generateduser->phone1, $returneduser['phone1']); } if (!empty($generateduser->phone2)) { $this->assertEquals($generateduser->phone2, $returneduser['phone2']); } if (!empty($generateduser->department)) { $this->assertEquals($generateduser->department, $returneduser['department']); } if (!empty($generateduser->institution)) { $this->assertEquals($generateduser->institution, $returneduser['institution']); } if (!empty($generateduser->description)) { $this->assertEquals($generateduser->description, $returneduser['description']); } if (!empty($generateduser->descriptionformat) and isset($returneduser['descriptionformat'])) { $this->assertEquals($generateduser->descriptionformat, $returneduser['descriptionformat']); } if (!empty($generateduser->city)) { $this->assertEquals($generateduser->city, $returneduser['city']); } if (!empty($generateduser->country)) { $this->assertEquals($generateduser->country, $returneduser['country']); } if (!empty($CFG->usetags) and !empty($generateduser->interests)) { $this->assertEquals(implode(', ', $generateduser->interests), $returneduser['interests']); } // Default language and no theme were used for the user. $this->assertEquals($CFG->lang, $returneduser['lang']); $this->assertEmpty($returneduser['theme']); } } // Test that no result are returned for search by username if we are not admin $this->setGuestUser(); // Call the external function. $returnedusers = core_user_external::get_users_by_field('username', array($USER->username, $user1->username, $user2->username)); $returnedusers = \external_api::clean_returnvalue(core_user_external::get_users_by_field_returns(), $returnedusers); // Only the own $USER username should be returned $this->assertEquals(1, count($returnedusers)); // And finally test as one of the enrolled users. $this->setUser($user1); // Call the external function. $returnedusers = core_user_external::get_users_by_field('username', array($USER->username, $user1->username, $user2->username)); $returnedusers = \external_api::clean_returnvalue(core_user_external::get_users_by_field_returns(), $returnedusers); // Only the own $USER username should be returned still. $this->assertEquals(1, count($returnedusers)); } public function get_course_user_profiles_setup($capability) { global $USER, $CFG; $this->resetAfterTest(true); $return = new \stdClass(); // Create the course and fetch its context. $return->course = self::getDataGenerator()->create_course(); $return->user1 = array( 'username' => 'usernametest1', 'idnumber' => 'idnumbertest1', 'firstname' => 'First Name User Test 1', 'lastname' => 'Last Name User Test 1', 'email' => 'usertest1@example.com', 'address' => '2 Test Street Perth 6000 WA', 'phone1' => '01010101010', 'phone2' => '02020203', 'department' => 'Department of user 1', 'institution' => 'Institution of user 1', 'description' => 'This is a description for user 1', 'descriptionformat' => FORMAT_MOODLE, 'city' => 'Perth', 'country' => 'AU' ); $return->user1 = self::getDataGenerator()->create_user($return->user1); if (!empty($CFG->usetags)) { require_once($CFG->dirroot . '/user/editlib.php'); $return->user1->interests = array('Cinema', 'Tennis', 'Dance', 'Guitar', 'Cooking'); useredit_update_interests($return->user1, $return->user1->interests); } $return->user2 = self::getDataGenerator()->create_user(); $context = \context_course::instance($return->course->id); $return->roleid = $this->assignUserCapability($capability, $context->id); // Enrol the users in the course. $this->getDataGenerator()->enrol_user($return->user1->id, $return->course->id, $return->roleid, 'manual'); $this->getDataGenerator()->enrol_user($return->user2->id, $return->course->id, $return->roleid, 'manual'); $this->getDataGenerator()->enrol_user($USER->id, $return->course->id, $return->roleid, 'manual'); $group1 = $this->getDataGenerator()->create_group(['courseid' => $return->course->id, 'name' => 'G1']); $group2 = $this->getDataGenerator()->create_group(['courseid' => $return->course->id, 'name' => 'G2']); groups_add_member($group1->id, $return->user1->id); groups_add_member($group2->id, $return->user2->id); return $return; } /** * Test get_course_user_profiles */ public function test_get_course_user_profiles() { global $USER, $CFG; $this->resetAfterTest(true); $data = $this->get_course_user_profiles_setup('moodle/user:viewdetails'); // Call the external function. $enrolledusers = core_user_external::get_course_user_profiles(array( array('userid' => $USER->id, 'courseid' => $data->course->id))); // We need to execute the return values cleaning process to simulate the web service server. $enrolledusers = \external_api::clean_returnvalue(core_user_external::get_course_user_profiles_returns(), $enrolledusers); // Check we retrieve the good total number of enrolled users + no error on capability. $this->assertEquals(1, count($enrolledusers)); } public function test_get_user_course_profile_as_admin() { global $USER, $CFG; global $USER, $CFG; $this->resetAfterTest(true); $data = $this->get_course_user_profiles_setup('moodle/user:viewdetails'); // Do the same call as admin to receive all possible fields. $this->setAdminUser(); $USER->email = "admin@example.com"; // Call the external function. $enrolledusers = core_user_external::get_course_user_profiles(array( array('userid' => $data->user1->id, 'courseid' => $data->course->id))); // We need to execute the return values cleaning process to simulate the web service server. $enrolledusers = \external_api::clean_returnvalue(core_user_external::get_course_user_profiles_returns(), $enrolledusers); // Check we get the requested user and that is in a group. $this->assertCount(1, $enrolledusers); $this->assertCount(1, $enrolledusers[0]['groups']); foreach($enrolledusers as $enrolleduser) { if ($enrolleduser['username'] == $data->user1->username) { $this->assertEquals($data->user1->idnumber, $enrolleduser['idnumber']); $this->assertEquals($data->user1->firstname, $enrolleduser['firstname']); $this->assertEquals($data->user1->lastname, $enrolleduser['lastname']); $this->assertEquals($data->user1->email, $enrolleduser['email']); $this->assertEquals($data->user1->address, $enrolleduser['address']); $this->assertEquals($data->user1->phone1, $enrolleduser['phone1']); $this->assertEquals($data->user1->phone2, $enrolleduser['phone2']); $this->assertEquals($data->user1->department, $enrolleduser['department']); $this->assertEquals($data->user1->institution, $enrolleduser['institution']); $this->assertEquals($data->user1->description, $enrolleduser['description']); $this->assertEquals(FORMAT_HTML, $enrolleduser['descriptionformat']); $this->assertEquals($data->user1->city, $enrolleduser['city']); $this->assertEquals($data->user1->country, $enrolleduser['country']); if (!empty($CFG->usetags)) { $this->assertEquals(implode(', ', $data->user1->interests), $enrolleduser['interests']); } } } } /** * Test create_users */ public function test_create_users() { global $DB; $this->resetAfterTest(true); $user1 = array( 'username' => 'usernametest1', 'password' => 'Moodle2012!', 'idnumber' => 'idnumbertest1', 'firstname' => 'First Name User Test 1', 'lastname' => 'Last Name User Test 1', 'middlename' => 'Middle Name User Test 1', 'lastnamephonetic' => '最後のお名前のテスト一号', 'firstnamephonetic' => 'お名前のテスト一号', 'alternatename' => 'Alternate Name User Test 1', 'email' => 'usertest1@example.com', 'description' => 'This is a description for user 1', 'city' => 'Perth', 'country' => 'AU', 'preferences' => [[ 'type' => 'htmleditor', 'value' => 'atto' ], [ 'type' => 'invalidpreference', 'value' => 'abcd' ] ], 'department' => 'College of Science', 'institution' => 'National Institute of Physics', 'phone1' => '01 2345 6789', 'maildisplay' => 1, 'interests' => 'badminton, basketball, cooking, ' ); // User with an authentication method done externally. $user2 = array( 'username' => 'usernametest2', 'firstname' => 'First Name User Test 2', 'lastname' => 'Last Name User Test 2', 'email' => 'usertest2@example.com', 'auth' => 'oauth2' ); $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, $user2)); // We need to execute the return values cleaning process to simulate the web service server. $createdusers = \external_api::clean_returnvalue(core_user_external::create_users_returns(), $createdusers); // Check we retrieve the good total number of created users + no error on capability. $this->assertCount(2, $createdusers); foreach($createdusers as $createduser) { $dbuser = $DB->get_record('user', array('id' => $createduser['id'])); if ($createduser['username'] === $user1['username']) { $usertotest = $user1; $this->assertEquals('atto', get_user_preferences('htmleditor', null, $dbuser)); $this->assertEquals(null, get_user_preferences('invalidpreference', null, $dbuser)); // Confirm user interests have been saved. $interests = \core_tag_tag::get_item_tags_array('core', 'user', $createduser['id'], \core_tag_tag::BOTH_STANDARD_AND_NOT, 0, false); // There should be 3 user interests. $this->assertCount(3, $interests); } else if ($createduser['username'] === $user2['username']) { $usertotest = $user2; } foreach ($dbuser as $property => $value) { if ($property === 'password') { if ($usertotest === $user2) { // External auth mechanisms don't store password in the user table. $this->assertEquals(AUTH_PASSWORD_NOT_CACHED, $value); } else { // Skip hashed passwords. continue; } } // Confirm that the values match. if (isset($usertotest[$property])) { $this->assertEquals($usertotest[$property], $value); } } } // Call without required capability $this->unassignUserCapability('moodle/user:create', $context->id, $roleid); $this->expectException('required_capability_exception'); core_user_external::create_users(array($user1)); } /** * Test create_users with password and createpassword parameter not set. */ public function test_create_users_empty_password() { $this->resetAfterTest(); $this->setAdminUser(); $user = [ 'username' => 'usernametest1', 'firstname' => 'First Name User Test 1', 'lastname' => 'Last Name User Test 1', 'email' => 'usertest1@example.com', ]; // This should throw an exception because either password or createpassword param must be passed for auth_manual. $this->expectException(\invalid_parameter_exception::class); core_user_external::create_users([$user]); } /** * Data provider for \core_user_externallib_testcase::test_create_users_with_same_emails(). */ public function create_users_provider_with_same_emails() { return [ 'Same emails allowed, same case' => [ 1, false ], 'Same emails allowed, different case' => [ 1, true ], 'Same emails disallowed, same case' => [ 0, false ], 'Same emails disallowed, different case' => [ 0, true ], ]; } /** * Test for \core_user_external::create_users() when user using the same email addresses are being created. * * @dataProvider create_users_provider_with_same_emails * @param int $sameemailallowed The value to set for $CFG->allowaccountssameemail. * @param boolean $differentcase Whether to user a different case for the other user. */ public function test_create_users_with_same_emails($sameemailallowed, $differentcase) { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Allow multiple users with the same email address. set_config('allowaccountssameemail', $sameemailallowed); $users = [ [ 'username' => 's1', 'firstname' => 'Johnny', 'lastname' => 'Bravo', 'email' => 's1@example.com', 'password' => 'Passw0rd!' ], [ 'username' => 's2', 'firstname' => 'John', 'lastname' => 'Doe', 'email' => $differentcase ? 'S1@EXAMPLE.COM' : 's1@example.com', 'password' => 'Passw0rd!' ], ]; if (!$sameemailallowed) { // This should throw an exception when $CFG->allowaccountssameemail is empty. $this->expectException(\invalid_parameter_exception::class); } // Create our users. core_user_external::create_users($users); // Confirm that the users have been created. list($insql, $params) = $DB->get_in_or_equal(['s1', 's2']); $this->assertEquals(2, $DB->count_records_select('user', 'username ' . $insql, $params)); } /** * Test create_users with invalid parameters * * @dataProvider data_create_users_invalid_parameter * @param array $data User data to attempt to register. * @param string $expectmessage Expected exception message. */ public function test_create_users_invalid_parameter(array $data, $expectmessage) { global $USER, $CFG, $DB; $this->resetAfterTest(true); $this->assignUserCapability('moodle/user:create', SYSCONTEXTID); $this->expectException('invalid_parameter_exception'); $this->expectExceptionMessage($expectmessage); core_user_external::create_users(array($data)); } /** * Data provider for {@see self::test_create_users_invalid_parameter()}. * * @return array */ public function data_create_users_invalid_parameter() { return [ 'blank_username' => [ 'data' => [ 'username' => '', 'firstname' => 'Foo', 'lastname' => 'Bar', 'email' => 'foobar@example.com', 'createpassword' => 1, ], 'expectmessage' => 'The field username cannot be blank', ], 'blank_firtname' => [ 'data' => [ 'username' => 'foobar', 'firstname' => "\t \n", 'lastname' => 'Bar', 'email' => 'foobar@example.com', 'createpassword' => 1, ], 'expectmessage' => 'The field firstname cannot be blank', ], 'blank_lastname' => [ 'data' => [ 'username' => 'foobar', 'firstname' => '0', 'lastname' => ' ', 'email' => 'foobar@example.com', 'createpassword' => 1, ], 'expectmessage' => 'The field lastname cannot be blank', ], 'invalid_email' => [ 'data' => [ 'username' => 'foobar', 'firstname' => 'Foo', 'lastname' => 'Bar', 'email' => '@foobar', 'createpassword' => 1, ], 'expectmessage' => 'Email address is invalid', ], 'missing_password' => [ 'data' => [ 'username' => 'foobar', 'firstname' => 'Foo', 'lastname' => 'Bar', 'email' => 'foobar@example.com', ], 'expectmessage' => 'Invalid password: you must provide a password, or set createpassword', ], ]; } /** * Test delete_users */ public function test_delete_users() { global $USER, $CFG, $DB; $this->resetAfterTest(true); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); // Check the users were correctly created. $this->assertEquals(2, $DB->count_records_select('user', 'deleted = 0 AND (id = :userid1 OR id = :userid2)', array('userid1' => $user1->id, 'userid2' => $user2->id))); $context = \context_system::instance(); $roleid = $this->assignUserCapability('moodle/user:delete', $context->id); // Call the external function. core_user_external::delete_users(array($user1->id, $user2->id)); // Check we retrieve no users + no error on capability. $this->assertEquals(0, $DB->count_records_select('user', 'deleted = 0 AND (id = :userid1 OR id = :userid2)', array('userid1' => $user1->id, 'userid2' => $user2->id))); // Call without required capability. $this->unassignUserCapability('moodle/user:delete', $context->id, $roleid); $this->expectException('required_capability_exception'); core_user_external::delete_users(array($user1->id, $user2->id)); } /** * Test update_users */ public function test_update_users() { global $USER, $CFG, $DB; $this->resetAfterTest(true); $this->preventResetByRollback(); $wsuser = self::getDataGenerator()->create_user(); self::setUser($wsuser); $context = \context_user::instance($USER->id); $contextid = $context->id; $filename = "reddot.png"; $filecontent = "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38" . "GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // Call the files api to create a file. $draftfile = core_files_external::upload($contextid, 'user', 'draft', 0, '/', $filename, $filecontent, null, null); $draftfile = \external_api::clean_returnvalue(core_files_external::upload_returns(), $draftfile); $draftid = $draftfile['itemid']; $user1 = self::getDataGenerator()->create_user(); $user1 = array( 'id' => $user1->id, 'username' => 'usernametest1', 'password' => 'Moodle2012!', 'idnumber' => 'idnumbertest1', 'firstname' => 'First Name User Test 1', 'lastname' => 'Last Name User Test 1', 'middlename' => 'Middle Name User Test 1', 'lastnamephonetic' => '最後のお名前のテスト一号', 'firstnamephonetic' => 'お名前のテスト一号', 'alternatename' => 'Alternate Name User Test 1', 'email' => 'usertest1@example.com', 'description' => 'This is a description for user 1', 'city' => 'Perth', 'userpicture' => $draftid, 'country' => 'AU', 'preferences' => [[ 'type' => 'htmleditor', 'value' => 'atto' ], [ 'type' => 'invialidpreference', 'value' => 'abcd' ] ], 'department' => 'College of Science', 'institution' => 'National Institute of Physics', 'phone1' => '01 2345 6789', 'maildisplay' => 1, 'interests' => 'badminton, basketball, cooking, ' ); $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; $user2['id'] = $CFG->siteguest; $siteadmins = explode(',', $CFG->siteadmins); $user3['id'] = array_shift($siteadmins); $userdeleted = self::getDataGenerator()->create_user(); $user4['id'] = $userdeleted->id; user_delete_user($userdeleted); $user5 = self::getDataGenerator()->create_user(); $user5 = array('id' => $user5->id, 'email' => $user5->email); // Call the external function. $returnvalue = core_user_external::update_users(array($user1, $user2, $user3, $user4)); $returnvalue = \external_api::clean_returnvalue(core_user_external::update_users_returns(), $returnvalue); // Check warnings. $this->assertEquals($user2['id'], $returnvalue['warnings'][0]['itemid']); // Guest user. $this->assertEquals('usernotupdatedguest', $returnvalue['warnings'][0]['warningcode']); $this->assertEquals($user3['id'], $returnvalue['warnings'][1]['itemid']); // Admin user. $this->assertEquals('usernotupdatedadmin', $returnvalue['warnings'][1]['warningcode']); $this->assertEquals($user4['id'], $returnvalue['warnings'][2]['itemid']); // Deleted user. $this->assertEquals('usernotupdateddeleted', $returnvalue['warnings'][2]['warningcode']); $dbuser2 = $DB->get_record('user', array('id' => $user2['id'])); $this->assertNotEquals($dbuser2->username, $user2['username']); $dbuser3 = $DB->get_record('user', array('id' => $user3['id'])); $this->assertNotEquals($dbuser3->username, $user3['username']); $dbuser4 = $DB->get_record('user', array('id' => $user4['id'])); $this->assertNotEquals($dbuser4->username, $user4['username']); $dbuser = $DB->get_record('user', array('id' => $user1['id'])); $this->assertEquals($dbuser->username, $user1['username']); $this->assertEquals($dbuser->idnumber, $user1['idnumber']); $this->assertEquals($dbuser->firstname, $user1['firstname']); $this->assertEquals($dbuser->lastname, $user1['lastname']); $this->assertEquals($dbuser->email, $user1['email']); $this->assertEquals($dbuser->description, $user1['description']); $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($dbuser->department, $user1['department']); $this->assertEquals($dbuser->institution, $user1['institution']); $this->assertEquals($dbuser->phone1, $user1['phone1']); $this->assertEquals($dbuser->maildisplay, $user1['maildisplay']); $this->assertEquals('atto', get_user_preferences('htmleditor', null, $dbuser)); $this->assertEquals(null, get_user_preferences('invalidpreference', null, $dbuser)); // Confirm user interests have been saved. $interests = \core_tag_tag::get_item_tags_array('core', 'user', $user1['id'], \core_tag_tag::BOTH_STANDARD_AND_NOT, 0, false); // There should be 3 user interests. $this->assertCount(3, $interests); // Confirm no picture change when parameter is not supplied. unset($user1['userpicture']); core_user_external::update_users(array($user1)); $dbusernopic = $DB->get_record('user', array('id' => $user1['id'])); $this->assertEquals($dbuser->picture, $dbusernopic->picture, 'Picture not change without the parameter.'); // Confirm delete of picture deletes the picture from the user record. $user1['userpicture'] = 0; core_user_external::update_users(array($user1)); $dbuserdelpic = $DB->get_record('user', array('id' => $user1['id'])); $this->assertEquals(0, $dbuserdelpic->picture, 'Picture must be deleted when sent as 0.'); // Updating user with an invalid email. $user5['email'] = 'bogus'; $returnvalue = core_user_external::update_users(array($user5)); $returnvalue = \external_api::clean_returnvalue(core_user_external::update_users_returns(), $returnvalue); $this->assertEquals('useremailinvalid', $returnvalue['warnings'][0]['warningcode']); $this->assertStringContainsString('Invalid email address', $returnvalue['warnings'][0]['message']); // Updating user with a duplicate email. $user5['email'] = $user1['email']; $returnvalue = core_user_external::update_users(array($user1, $user5)); $returnvalue = \external_api::clean_returnvalue(core_user_external::update_users_returns(), $returnvalue); $this->assertEquals('useremailduplicate', $returnvalue['warnings'][0]['warningcode']); $this->assertStringContainsString('Duplicate email address', $returnvalue['warnings'][0]['message']); // Updating a user that does not exist. $user5['id'] = -1; $returnvalue = core_user_external::update_users(array($user5)); $returnvalue = \external_api::clean_returnvalue(core_user_external::update_users_returns(), $returnvalue); $this->assertEquals('invaliduserid', $returnvalue['warnings'][0]['warningcode']); $this->assertStringContainsString('Invalid user ID', $returnvalue['warnings'][0]['message']); // Updating a remote user. $user1['mnethostid'] = 5; user_update_user($user1); // Update user not using webservice. unset($user1['mnethostid']); // The mnet host ID field is not in the allowed field list for the webservice. $returnvalue = core_user_external::update_users(array($user1)); $returnvalue = \external_api::clean_returnvalue(core_user_external::update_users_returns(), $returnvalue); $this->assertEquals('usernotupdatedremote', $returnvalue['warnings'][0]['warningcode']); $this->assertStringContainsString('User is a remote user', $returnvalue['warnings'][0]['message']); // Call without required capability. $this->unassignUserCapability('moodle/user:update', $context->id, $roleid); $this->expectException('required_capability_exception'); core_user_external::update_users(array($user1)); } /** * Data provider for testing \core_user_external::update_users() for users with same emails * * @return array */ public function users_with_same_emails() { return [ 'Same emails not allowed: Update name using exactly the same email' => [ 0, 'John', 's1@example.com', 'Johnny', 's1@example.com', false, true ], 'Same emails not allowed: Update using someone else\'s email' => [ 0, 'John', 's1@example.com', 'Johnny', 's2@example.com', true, false ], 'Same emails allowed: Update using someone else\'s email' => [ 1, 'John', 's1@example.com', 'Johnny', 's2@example.com', true, true ], 'Same emails not allowed: Update using same email but with different case' => [ 0, 'John', 's1@example.com', 'Johnny', 'S1@EXAMPLE.COM', false, true ], 'Same emails not allowed: Update using another user\'s email similar to user but with different case' => [ 0, 'John', 's1@example.com', 'Johnny', 'S1@EXAMPLE.COM', true, false ], 'Same emails allowed: Update using another user\'s email similar to user but with different case' => [ 1, 'John', 's1@example.com', 'Johnny', 'S1@EXAMPLE.COM', true, true ], ]; } /** * Test update_users using similar emails with varying cases. * * @dataProvider users_with_same_emails * @param boolean $allowsameemail The value to set for $CFG->allowaccountssameemail. * @param string $currentname The user's current name. * @param string $currentemail The user's current email. * @param string $newname The user's new name. * @param string $newemail The user's new email. * @param boolean $withanotheruser Whether to create another user that has the same email as the target user's new email. * @param boolean $successexpected Whether we expect that the target user's email/name will be updated. */ public function test_update_users_emails_with_different_cases($allowsameemail, $currentname, $currentemail, $newname, $newemail, $withanotheruser, $successexpected) { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Set the value for $CFG->allowaccountssameemail. set_config('allowaccountssameemail', $allowsameemail); $generator = self::getDataGenerator(); // Create the user that we wish to update. $usertoupdate = $generator->create_user(['email' => $currentemail, 'firstname' => $currentname]); if ($withanotheruser) { // Create another user that has the same email as the new email that we'd like to update for our target user. $generator->create_user(['email' => $newemail]); } // Build the user update parameters. $updateparams = [ 'id' => $usertoupdate->id, 'email' => $newemail, 'firstname' => $newname ]; // Let's try to update the user's information. core_user_external::update_users([$updateparams]); // Fetch the updated user record. $userrecord = $DB->get_record('user', ['id' => $usertoupdate->id], 'id, email, firstname'); // If we expect the update to succeed, then the email/name would have been changed. if ($successexpected) { $expectedemail = $newemail; $expectedname = $newname; } else { $expectedemail = $currentemail; $expectedname = $currentname; } // Confirm that our expectations are met. $this->assertEquals($expectedemail, $userrecord->email); $this->assertEquals($expectedname, $userrecord->firstname); } /** * Test add_user_private_files */ public function test_add_user_private_files() { global $USER, $CFG, $DB; $this->resetAfterTest(true); $context = \context_system::instance(); $roleid = $this->assignUserCapability('moodle/user:manageownfiles', $context->id); $context = \context_user::instance($USER->id); $contextid = $context->id; $component = "user"; $filearea = "draft"; $itemid = 0; $filepath = "/"; $filename = "Simple.txt"; $filecontent = base64_encode("Let us create a nice simple file"); $contextlevel = null; $instanceid = null; $browser = get_file_browser(); // Call the files api to create a file. $draftfile = core_files_external::upload($contextid, $component, $filearea, $itemid, $filepath, $filename, $filecontent, $contextlevel, $instanceid); $draftfile = \external_api::clean_returnvalue(core_files_external::upload_returns(), $draftfile); $draftid = $draftfile['itemid']; // Make sure the file was created. $file = $browser->get_file_info($context, $component, $filearea, $draftid, $filepath, $filename); $this->assertNotEmpty($file); // Make sure the file does not exist in the user private files. $file = $browser->get_file_info($context, $component, 'private', 0, $filepath, $filename); $this->assertEmpty($file); // Call the external function. core_user_external::add_user_private_files($draftid); // Make sure the file was added to the user private files. $file = $browser->get_file_info($context, $component, 'private', 0, $filepath, $filename); $this->assertNotEmpty($file); } /** * Test add_user_private_files quota */ public function test_add_user_private_files_quota() { global $USER, $CFG, $DB; $this->resetAfterTest(true); $context = \context_system::instance(); $roleid = $this->assignUserCapability('moodle/user:manageownfiles', $context->id); $context = \context_user::instance($USER->id); $contextid = $context->id; $component = "user"; $filearea = "draft"; $itemid = 0; $filepath = "/"; $filename = "Simple.txt"; $filecontent = base64_encode("Let us create a nice simple file"); $contextlevel = null; $instanceid = null; $browser = get_file_browser(); // Call the files api to create a file. $draftfile = core_files_external::upload($contextid, $component, $filearea, $itemid, $filepath, $filename, $filecontent, $contextlevel, $instanceid); $draftfile = \external_api::clean_returnvalue(core_files_external::upload_returns(), $draftfile); $draftid = $draftfile['itemid']; // Call the external function to add the file to private files. core_user_external::add_user_private_files($draftid); // Force the quota so we are sure it won't be space to add the new file. $CFG->userquota = file_get_user_used_space() + 1; // Generate a new draftitemid for the same testfile. $draftfile = core_files_external::upload($contextid, $component, $filearea, $itemid, $filepath, $filename, $filecontent, $contextlevel, $instanceid); $draftid = $draftfile['itemid']; $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('maxareabytes', 'error')); // Call the external function to include the new file. core_user_external::add_user_private_files($draftid); } /** * Test add user device */ public function test_add_user_device() { global $USER, $CFG, $DB; $this->resetAfterTest(true); $device = array( 'appid' => 'com.moodle.moodlemobile', 'name' => 'occam', 'model' => 'Nexus 4', 'platform' => 'Android', 'version' => '4.2.2', 'pushid' => 'apushdkasdfj4835', 'uuid' => 'asdnfl348qlksfaasef859' ); // Call the external function. core_user_external::add_user_device($device['appid'], $device['name'], $device['model'], $device['platform'], $device['version'], $device['pushid'], $device['uuid']); $created = $DB->get_record('user_devices', array('pushid' => $device['pushid'])); $created = (array) $created; $this->assertEquals($device, array_intersect_key((array)$created, $device)); // Test reuse the same pushid value. $warnings = core_user_external::add_user_device($device['appid'], $device['name'], $device['model'], $device['platform'], $device['version'], $device['pushid'], $device['uuid']); // We need to execute the return values cleaning process to simulate the web service server. $warnings = \external_api::clean_returnvalue(core_user_external::add_user_device_returns(), $warnings); $this->assertCount(1, $warnings); // Test update an existing device. $device['pushid'] = 'different than before'; $warnings = core_user_external::add_user_device($device['appid'], $device['name'], $device['model'], $device['platform'], $device['version'], $device['pushid'], $device['uuid']); $warnings = \external_api::clean_returnvalue(core_user_external::add_user_device_returns(), $warnings); $this->assertEquals(1, $DB->count_records('user_devices')); $updated = $DB->get_record('user_devices', array('pushid' => $device['pushid'])); $this->assertEquals($device, array_intersect_key((array)$updated, $device)); // Test creating a new device just changing the uuid. $device['uuid'] = 'newuidforthesameuser'; $device['pushid'] = 'new different than before'; $warnings = core_user_external::add_user_device($device['appid'], $device['name'], $device['model'], $device['platform'], $device['version'], $device['pushid'], $device['uuid']); $warnings = \external_api::clean_returnvalue(core_user_external::add_user_device_returns(), $warnings); $this->assertEquals(2, $DB->count_records('user_devices')); } /** * Test remove user device */ public function test_remove_user_device() { global $USER, $CFG, $DB; $this->resetAfterTest(true); $device = array( 'appid' => 'com.moodle.moodlemobile', 'name' => 'occam', 'model' => 'Nexus 4', 'platform' => 'Android', 'version' => '4.2.2', 'pushid' => 'apushdkasdfj4835', 'uuid' => 'ABCDE3723ksdfhasfaasef859' ); // A device with the same properties except the appid and pushid. $device2 = $device; $device2['pushid'] = "0987654321"; $device2['appid'] = "other.app.com"; $this->setAdminUser(); // Create a user device using the external API function. core_user_external::add_user_device($device['appid'], $device['name'], $device['model'], $device['platform'], $device['version'], $device['pushid'], $device['uuid']); // Create the same device but for a different app. core_user_external::add_user_device($device2['appid'], $device2['name'], $device2['model'], $device2['platform'], $device2['version'], $device2['pushid'], $device2['uuid']); // Try to remove a device that does not exist. $result = core_user_external::remove_user_device('1234567890'); $result = \external_api::clean_returnvalue(core_user_external::remove_user_device_returns(), $result); $this->assertFalse($result['removed']); $this->assertCount(1, $result['warnings']); // Try to remove a device that does not exist for an existing app. $result = core_user_external::remove_user_device('1234567890', $device['appid']); $result = \external_api::clean_returnvalue(core_user_external::remove_user_device_returns(), $result); $this->assertFalse($result['removed']); $this->assertCount(1, $result['warnings']); // Remove an existing device for an existing app. This will remove one of the two devices. $result = core_user_external::remove_user_device($device['uuid'], $device['appid']); $result = \external_api::clean_returnvalue(core_user_external::remove_user_device_returns(), $result); $this->assertTrue($result['removed']); // Remove all the devices. This must remove the remaining device. $result = core_user_external::remove_user_device($device['uuid']); $result = \external_api::clean_returnvalue(core_user_external::remove_user_device_returns(), $result); $this->assertTrue($result['removed']); } /** * Test get_user_preferences */ public function test_get_user_preferences() { $this->resetAfterTest(true); $user = self::getDataGenerator()->create_user(); set_user_preference('calendar_maxevents', 1, $user); set_user_preference('some_random_text', 'text', $user); $this->setUser($user); $result = core_user_external::get_user_preferences(); $result = \external_api::clean_returnvalue(core_user_external::get_user_preferences_returns(), $result); $this->assertCount(0, $result['warnings']); // Expect 3, _lastloaded is always returned. $this->assertCount(3, $result['preferences']); foreach ($result['preferences'] as $pref) { if ($pref['name'] === '_lastloaded') { continue; } // Check we receive the expected preferences. $this->assertEquals(get_user_preferences($pref['name']), $pref['value']); } // Retrieve just one preference. $result = core_user_external::get_user_preferences('some_random_text'); $result = \external_api::clean_returnvalue(core_user_external::get_user_preferences_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(1, $result['preferences']); $this->assertEquals('text', $result['preferences'][0]['value']); // Retrieve non-existent preference. $result = core_user_external::get_user_preferences('non_existent'); $result = \external_api::clean_returnvalue(core_user_external::get_user_preferences_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(1, $result['preferences']); $this->assertEquals(null, $result['preferences'][0]['value']); // Check that as admin we can retrieve all the preferences for any user. $this->setAdminUser(); $result = core_user_external::get_user_preferences('', $user->id); $result = \external_api::clean_returnvalue(core_user_external::get_user_preferences_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(3, $result['preferences']); foreach ($result['preferences'] as $pref) { if ($pref['name'] === '_lastloaded') { continue; } // Check we receive the expected preferences. $this->assertEquals(get_user_preferences($pref['name'], null, $user), $pref['value']); } // Check that as a non admin user we cannot retrieve other users preferences. $anotheruser = self::getDataGenerator()->create_user(); $this->setUser($anotheruser); $this->expectException('required_capability_exception'); $result = core_user_external::get_user_preferences('', $user->id); } /** * Test update_picture */ public function test_update_picture() { global $DB, $USER; $this->resetAfterTest(true); $user = self::getDataGenerator()->create_user(); self::setUser($user); $context = \context_user::instance($USER->id); $contextid = $context->id; $filename = "reddot.png"; $filecontent = "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38" . "GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // Call the files api to create a file. $draftfile = core_files_external::upload($contextid, 'user', 'draft', 0, '/', $filename, $filecontent, null, null); $draftid = $draftfile['itemid']; // Change user profile image. $result = core_user_external::update_picture($draftid); $result = \external_api::clean_returnvalue(core_user_external::update_picture_returns(), $result); $picture = $DB->get_field('user', 'picture', array('id' => $user->id)); // The new revision is in the url for the user. $this->assertStringContainsString($picture, $result['profileimageurl']); // Check expected URL for serving the image. $this->assertStringContainsString("/$contextid/user/icon", $result['profileimageurl']); // Delete image. $result = core_user_external::update_picture(0, true); $result = \external_api::clean_returnvalue(core_user_external::update_picture_returns(), $result); $picture = $DB->get_field('user', 'picture', array('id' => $user->id)); // No picture. $this->assertEquals(0, $picture); // Add again the user profile image (as admin). $this->setAdminUser(); $context = \context_user::instance($USER->id); $admincontextid = $context->id; $draftfile = core_files_external::upload($admincontextid, 'user', 'draft', 0, '/', $filename, $filecontent, null, null); $draftid = $draftfile['itemid']; $result = core_user_external::update_picture($draftid, false, $user->id); $result = \external_api::clean_returnvalue(core_user_external::update_picture_returns(), $result); // The new revision is in the url for the user. $picture = $DB->get_field('user', 'picture', array('id' => $user->id)); $this->assertStringContainsString($picture, $result['profileimageurl']); $this->assertStringContainsString("/$contextid/user/icon", $result['profileimageurl']); } /** * Test update_picture disabled */ public function test_update_picture_disabled() { global $CFG; $this->resetAfterTest(true); $CFG->disableuserimages = true; $this->setAdminUser(); $this->expectException('moodle_exception'); core_user_external::update_picture(0); } /** * Test set_user_preferences */ public function test_set_user_preferences_save() { global $DB; $this->resetAfterTest(true); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); // Save users preferences. $this->setAdminUser(); $preferences = array( array( 'name' => 'htmleditor', 'value' => 'atto', 'userid' => $user1->id, ), array( 'name' => 'htmleditor', 'value' => 'tinymce', 'userid' => $user2->id, ) ); $result = core_user_external::set_user_preferences($preferences); $result = \external_api::clean_returnvalue(core_user_external::set_user_preferences_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(2, $result['saved']); // Get preference from DB to avoid cache. $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'))); } /** * Test set_user_preferences for an invalid user */ public function test_set_user_preferences_invalid_user() { $this->resetAfterTest(true); $this->setAdminUser(); $preferences = array( array( 'name' => 'calendar_maxevents', 'value' => 4, 'userid' => -2 ) ); $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('invaliduser', $result['warnings'][0]['warningcode']); $this->assertEquals(-2, $result['warnings'][0]['itemid']); } /** * Test set_user_preferences using an invalid preference */ public function test_set_user_preferences_invalid_preference() { global $USER, $DB; $this->resetAfterTest(true); // Create a very long value. $this->setAdminUser(); $preferences = array( array( 'name' => 'calendar_maxevents', 'value' => str_repeat('a', 1334), 'userid' => $USER->id ) ); $result = core_user_external::set_user_preferences($preferences); $result = \external_api::clean_returnvalue(core_user_external::set_user_preferences_returns(), $result); $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_capability() { $this->resetAfterTest(true); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $this->setUser($user1); $preferences = array( array( 'name' => 'calendar_maxevents', 'value' => 4, 'userid' => $user2->id ) ); $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']); } /** * Test update_user_preferences unsetting an existing preference. */ public function test_update_user_preferences_unset() { global $DB; $this->resetAfterTest(true); $user = self::getDataGenerator()->create_user(); // Save users preferences. $this->setAdminUser(); $preferences = array( array( 'name' => 'htmleditor', 'value' => 'atto', 'userid' => $user->id, ) ); $result = core_user_external::set_user_preferences($preferences); $result = \external_api::clean_returnvalue(core_user_external::set_user_preferences_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(1, $result['saved']); // Get preference from DB to avoid cache. $this->assertEquals('atto', $DB->get_field('user_preferences', 'value', array('userid' => $user->id, 'name' => 'htmleditor'))); // Now, unset. $result = core_user_external::update_user_preferences($user->id, null, array(array('type' => 'htmleditor'))); $this->assertEquals(0, $DB->count_records('user_preferences', array('userid' => $user->id, 'name' => 'htmleditor'))); } /** * Test agree_site_policy */ public function test_agree_site_policy() { global $CFG, $DB, $USER; $this->resetAfterTest(true); $user = self::getDataGenerator()->create_user(); $this->setUser($user); // Site policy not set. $result = core_user_external::agree_site_policy(); $result = \external_api::clean_returnvalue(core_user_external::agree_site_policy_returns(), $result); $this->assertFalse($result['status']); $this->assertCount(1, $result['warnings']); $this->assertEquals('nositepolicy', $result['warnings'][0]['warningcode']); // Set a policy issue. $CFG->sitepolicy = 'https://moodle.org'; $this->assertEquals(0, $USER->policyagreed); $result = core_user_external::agree_site_policy(); $result = \external_api::clean_returnvalue(core_user_external::agree_site_policy_returns(), $result); $this->assertTrue($result['status']); $this->assertCount(0, $result['warnings']); $this->assertEquals(1, $USER->policyagreed); $this->assertEquals(1, $DB->get_field('user', 'policyagreed', array('id' => $USER->id))); // Try again, we should get a warning. $result = core_user_external::agree_site_policy(); $result = \external_api::clean_returnvalue(core_user_external::agree_site_policy_returns(), $result); $this->assertFalse($result['status']); $this->assertCount(1, $result['warnings']); $this->assertEquals('alreadyagreed', $result['warnings'][0]['warningcode']); // Set something to make require_login throws an exception. $otheruser = self::getDataGenerator()->create_user(); $this->setUser($otheruser); $DB->set_field('user', 'lastname', '', array('id' => $USER->id)); $USER->lastname = ''; try { $result = core_user_external::agree_site_policy(); $this->fail('Expecting \'usernotfullysetup\' moodle_exception to be thrown'); } catch (\moodle_exception $e) { $this->assertEquals('usernotfullysetup', $e->errorcode); } catch (\Exception $e) { $this->fail('Expecting \'usernotfullysetup\' moodle_exception to be thrown.'); } } /** * Test get_private_files_info */ public function test_get_private_files_info() { $this->resetAfterTest(true); $user = self::getDataGenerator()->create_user(); $this->setUser($user); $usercontext = \context_user::instance($user->id); $filerecord = array( 'contextid' => $usercontext->id, 'component' => 'user', 'filearea' => 'private', 'itemid' => 0, 'filepath' => '/', 'filename' => 'thefile', ); $fs = get_file_storage(); $file = $fs->create_file_from_string($filerecord, 'abc'); // Get my private files information. $result = core_user_external::get_private_files_info(); $result = \external_api::clean_returnvalue(core_user_external::get_private_files_info_returns(), $result); $this->assertEquals(1, $result['filecount']); $this->assertEquals($file->get_filesize(), $result['filesize']); $this->assertEquals(0, $result['foldercount']); $this->assertEquals($file->get_filesize(), $result['filesizewithoutreferences']); // As admin, get user information. $this->setAdminUser(); $result = core_user_external::get_private_files_info($user->id); $result = \external_api::clean_returnvalue(core_user_external::get_private_files_info_returns(), $result); $this->assertEquals(1, $result['filecount']); $this->assertEquals($file->get_filesize(), $result['filesize']); $this->assertEquals(0, $result['foldercount']); $this->assertEquals($file->get_filesize(), $result['filesizewithoutreferences']); } /** * Test get_private_files_info missing permissions. */ public function test_get_private_files_info_missing_permissions() { $this->resetAfterTest(true); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $this->setUser($user1); $this->expectException('required_capability_exception'); // Try to retrieve other user private files info. core_user_external::get_private_files_info($user2->id); } /** * Test the functionality of the {@see \core_user\external\search_identity} class. */ public function test_external_search_identity() { global $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $user1 = self::getDataGenerator()->create_user([ 'firstname' => 'Firstone', 'lastname' => 'Lastone', 'username' => 'usernameone', 'idnumber' => 'idnumberone', 'email' => 'userone@example.com', 'phone1' => 'tel1', 'phone2' => 'tel2', 'department' => 'Department Foo', 'institution' => 'Institution Foo', 'city' => 'City One', 'country' => 'AU', ]); $user2 = self::getDataGenerator()->create_user([ 'firstname' => 'Firsttwo', 'lastname' => 'Lasttwo', 'username' => 'usernametwo', 'idnumber' => 'idnumbertwo', 'email' => 'usertwo@example.com', 'phone1' => 'tel1', 'phone2' => 'tel2', 'department' => 'Department Foo', 'institution' => 'Institution Foo', 'city' => 'City One', 'country' => 'AU', ]); $user3 = self::getDataGenerator()->create_user([ 'firstname' => 'Firstthree', 'lastname' => 'Lastthree', 'username' => 'usernamethree', 'idnumber' => 'idnumberthree', 'email' => 'userthree@example.com', 'phone1' => 'tel1', 'phone2' => 'tel2', 'department' => 'Department Foo', 'institution' => 'Institution Foo', 'city' => 'City One', 'country' => 'AU', ]); $CFG->showuseridentity = 'email,idnumber,city'; $CFG->maxusersperpage = 3; $result = \core_user\external\search_identity::execute('Lastt'); $result = \external_api::clean_returnvalue(\core_user\external\search_identity::execute_returns(), $result); $this->assertEquals(2, count($result['list'])); $this->assertEquals(3, $result['maxusersperpage']); $this->assertEquals(false, $result['overflow']); foreach ($result['list'] as $user) { $this->assertEquals(3, count($user['extrafields'])); $this->assertEquals('email', $user['extrafields'][0]['name']); $this->assertEquals('idnumber', $user['extrafields'][1]['name']); $this->assertEquals('city', $user['extrafields'][2]['name']); } $CFG->showuseridentity = 'username'; $CFG->maxusersperpage = 2; $result = \core_user\external\search_identity::execute('Firstt'); $result = \external_api::clean_returnvalue(\core_user\external\search_identity::execute_returns(), $result); $this->assertEquals(2, count($result['list'])); $this->assertEquals(2, $result['maxusersperpage']); $this->assertEquals(false, $result['overflow']); foreach ($result['list'] as $user) { $this->assertEquals(1, count($user['extrafields'])); $this->assertEquals('username', $user['extrafields'][0]['name']); } $CFG->showuseridentity = 'email'; $CFG->maxusersperpage = 2; $result = \core_user\external\search_identity::execute('City One'); $result = \external_api::clean_returnvalue(\core_user\external\search_identity::execute_returns(), $result); $this->assertEquals(0, count($result['list'])); $this->assertEquals(2, $result['maxusersperpage']); $this->assertEquals(false, $result['overflow']); $CFG->showuseridentity = 'city'; $CFG->maxusersperpage = 2; foreach ($result['list'] as $user) { $this->assertEquals(1, count($user['extrafields'])); $this->assertEquals('username', $user['extrafields'][0]['name']); } $result = \core_user\external\search_identity::execute('City One'); $result = \external_api::clean_returnvalue(\core_user\external\search_identity::execute_returns(), $result); $this->assertEquals(2, count($result['list'])); $this->assertEquals(2, $result['maxusersperpage']); $this->assertEquals(true, $result['overflow']); } /** * Test functionality of the {@see \core_user\external\search_identity} class with alternativefullnameformat defined. */ public function test_external_search_identity_with_alternativefullnameformat() { global $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $user1 = self::getDataGenerator()->create_user([ 'lastname' => '小柳', 'lastnamephonetic' => 'Koyanagi', 'firstname' => '秋', 'firstnamephonetic' => 'Aki', 'email' => 'koyanagiaki@example.com', 'country' => 'JP', ]); $CFG->showuseridentity = 'email'; $CFG->maxusersperpage = 3; $CFG->alternativefullnameformat = '<ruby>lastname firstname <rp>(</rp><rt>lastnamephonetic firstnamephonetic</rt><rp>)</rp></ruby>'; $result = \core_user\external\search_identity::execute('Ak'); $result = \external_api::clean_returnvalue(\core_user\external\search_identity::execute_returns(), $result); $this->assertEquals(1, count($result['list'])); $this->assertEquals(3, $result['maxusersperpage']); $this->assertEquals(false, $result['overflow']); foreach ($result['list'] as $user) { $this->assertEquals(1, count($user['extrafields'])); $this->assertEquals('email', $user['extrafields'][0]['name']); } } /** * Test verifying that update_user_preferences prevents changes to the default homepage for other users. */ public function test_update_user_preferences_homepage_permission_callback() { global $DB; $this->resetAfterTest(); $user = self::getDataGenerator()->create_user(); $this->setUser($user); $adminuser = get_admin(); // Allow user selection of the default homepage via preferences. set_config('defaulthomepage', HOMEPAGE_USER); // Try to save another user's home page preference which uses the permissioncallback. $preferences = [ [ 'name' => 'user_home_page_preference', 'value' => '3', 'userid' => $adminuser->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']); // Verify no change to the preference, checking from DB to avoid cache. $this->assertEquals(null, $DB->get_field('user_preferences', 'value', ['userid' => $adminuser->id, 'name' => 'user_home_page_preference'])); } } tests/reportbuilder/datasource/users_test.php 0000644 00000052514 15151162244 0015643 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_user\reportbuilder\datasource; use core_collator; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\filters\tags; use core_reportbuilder\local\filters\text; use core_reportbuilder\local\filters\user as user_filter; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for users datasource * * @package core_user * @covers \core_user\reportbuilder\datasource\users * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class users_test extends core_reportbuilder_testcase { /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); $user2 = $this->getDataGenerator()->create_user(['firstname' => 'Charles']); $user3 = $this->getDataGenerator()->create_user(['firstname' => 'Brian']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(3, $content); // Default columns are fullname, username, email. Results are sorted by the fullname. [$adminrow, $userrow1, $userrow2] = array_map('array_values', $content); $this->assertEquals(['Admin User', 'admin', 'admin@example.com'], $adminrow); $this->assertEquals([fullname($user3), $user3->username, $user3->email], $userrow1); $this->assertEquals([fullname($user2), $user2->username, $user2->email], $userrow2); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user([ 'firstname' => 'Zoe', 'idnumber' => 'U0001', 'city' => 'London', 'country' => 'GB', 'interests' => ['Horses'], ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // User. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithlink']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithpicture']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithpicturelink']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:picture']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:city']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:country']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:description']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstnamephonetic']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastnamephonetic']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:middlename']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:alternatename']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:idnumber']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:institution']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:department']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:phone1']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:phone2']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:address']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastaccess']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:suspended']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:confirmed']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:moodlenetprofile']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:timecreated']); // Tags. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:namewithlink']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(2, $content); // Consistent order by firstname, just in case. core_collator::asort_array_of_arrays_by_key($content, 'c4_firstname'); $content = array_values($content); [$adminrow, $userrow] = array_map('array_values', $content); $this->assertStringContainsString('Admin User', $adminrow[0]); $this->assertStringContainsString('Admin User', $adminrow[1]); $this->assertStringContainsString('Admin User', $adminrow[2]); $this->assertNotEmpty($adminrow[3]); $this->assertEquals('Admin', $adminrow[4]); $this->assertEquals('User', $adminrow[5]); $this->assertStringContainsString(fullname($user), $userrow[0]); $this->assertStringContainsString(fullname($user), $userrow[1]); $this->assertStringContainsString(fullname($user), $userrow[2]); $this->assertNotEmpty($userrow[3]); $this->assertEquals($user->firstname, $userrow[4]); $this->assertEquals($user->lastname, $userrow[5]); $this->assertEquals($user->city, $userrow[6]); $this->assertEquals('United Kingdom', $userrow[7]); $this->assertEquals($user->description, $userrow[8]); $this->assertEquals($user->firstnamephonetic, $userrow[9]); $this->assertEquals($user->lastnamephonetic, $userrow[10]); $this->assertEquals($user->middlename, $userrow[11]); $this->assertEquals($user->alternatename, $userrow[12]); $this->assertEquals($user->idnumber, $userrow[13]); $this->assertEquals($user->institution, $userrow[14]); $this->assertEquals($user->department, $userrow[15]); $this->assertEquals($user->phone1, $userrow[16]); $this->assertEquals($user->phone2, $userrow[17]); $this->assertEquals($user->address, $userrow[18]); $this->assertEmpty($userrow[19]); $this->assertEquals('No', $userrow[20]); $this->assertEquals('Yes', $userrow[21]); $this->assertEquals($user->moodlenetprofile, $userrow[22]); $this->assertNotEmpty($userrow[23]); $this->assertEquals('Horses', $userrow[24]); $this->assertStringContainsString('Horses', $userrow[25]); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public function datasource_filters_provider(): array { return [ // User. 'Filter user' => ['user:userselect', [ 'user:userselect_operator' => user_filter::USER_SELECT, 'user:userselect_value' => [-1], ], false], 'Filter fullname' => ['user:fullname', [ 'user:fullname_operator' => text::CONTAINS, 'user:fullname_value' => 'Zoe', ], true], 'Filter fullname (no match)' => ['user:fullname', [ 'user:fullname_operator' => text::CONTAINS, 'user:fullname_value' => 'Alfie', ], false], 'Filter firstname' => ['user:firstname', [ 'user:firstname_operator' => text::IS_EQUAL_TO, 'user:firstname_value' => 'Zoe', ], true], 'Filter firstname (no match)' => ['user:firstname', [ 'user:firstname_operator' => text::IS_EQUAL_TO, 'user:firstname_value' => 'Alfie', ], false], 'Filter middlename' => ['user:middlename', [ 'user:middlename_operator' => text::IS_EQUAL_TO, 'user:middlename_value' => 'Zebediah', ], true], 'Filter middlename (no match)' => ['user:middlename', [ 'user:middlename_operator' => text::IS_EQUAL_TO, 'user:middlename_value' => 'Aardvark', ], false], 'Filter lastname' => ['user:lastname', [ 'user:lastname_operator' => text::IS_EQUAL_TO, 'user:lastname_value' => 'Zebra', ], true], 'Filter lastname (no match)' => ['user:lastname', [ 'user:lastname_operator' => text::IS_EQUAL_TO, 'user:lastname_value' => 'Aardvark', ], false], 'Filter firstnamephonetic' => ['user:firstnamephonetic', [ 'user:firstnamephonetic_operator' => text::IS_EQUAL_TO, 'user:firstnamephonetic_value' => 'Eoz', ], true], 'Filter firstnamephonetic (no match)' => ['user:firstnamephonetic', [ 'user:firstnamephonetic_operator' => text::IS_EQUAL_TO, 'user:firstnamephonetic_value' => 'Alfie', ], false], 'Filter lastnamephonetic' => ['user:lastnamephonetic', [ 'user:lastnamephonetic_operator' => text::IS_EQUAL_TO, 'user:lastnamephonetic_value' => 'Arbez', ], true], 'Filter lastnamephonetic (no match)' => ['user:lastnamephonetic', [ 'user:lastnamephonetic_operator' => text::IS_EQUAL_TO, 'user:lastnamephonetic_value' => 'Aardvark', ], false], 'Filter alternatename' => ['user:alternatename', [ 'user:alternatename_operator' => text::IS_EQUAL_TO, 'user:alternatename_value' => 'Zee', ], true], 'Filter alternatename (no match)' => ['user:alternatename', [ 'user:alternatename_operator' => text::IS_EQUAL_TO, 'user:alternatename_value' => 'Aardvark', ], false], 'Filter email' => ['user:email', [ 'user:email_operator' => text::CONTAINS, 'user:email_value' => 'zoe1', ], true], 'Filter email (no match)' => ['user:email', [ 'user:email_operator' => text::CONTAINS, 'user:email_value' => 'alfie1', ], false], 'Filter phone1' => ['user:phone1', [ 'user:phone1_operator' => text::IS_EQUAL_TO, 'user:phone1_value' => '111', ], true], 'Filter phone1 (no match)' => ['user:phone1', [ 'user:phone1_operator' => text::IS_EQUAL_TO, 'user:phone1_value' => '119', ], false], 'Filter phone2' => ['user:phone2', [ 'user:phone2_operator' => text::IS_EQUAL_TO, 'user:phone2_value' => '222', ], true], 'Filter phone2 (no match)' => ['user:phone2', [ 'user:phone2_operator' => text::IS_EQUAL_TO, 'user:phone2_value' => '229', ], false], 'Filter address' => ['user:address', [ 'user:address_operator' => text::IS_EQUAL_TO, 'user:address_value' => 'Big Farm', ], true], 'Filter address (no match)' => ['user:address', [ 'user:address_operator' => text::IS_EQUAL_TO, 'user:address_value' => 'Small Farm', ], false], 'Filter city' => ['user:city', [ 'user:city_operator' => text::IS_EQUAL_TO, 'user:city_value' => 'Barcelona', ], true], 'Filter city (no match)' => ['user:city', [ 'user:city_operator' => text::IS_EQUAL_TO, 'user:city_value' => 'Perth', ], false], 'Filter country' => ['user:country', [ 'user:country_operator' => select::EQUAL_TO, 'user:country_value' => 'ES', ], true], 'Filter country (no match)' => ['user:country', [ 'user:country_operator' => select::EQUAL_TO, 'user:country_value' => 'AU', ], false], 'Filter description' => ['user:description', [ 'user:description_operator' => text::CONTAINS, 'user:description_value' => 'Hello there', ], true], 'Filter description (no match)' => ['user:description', [ 'user:description_operator' => text::CONTAINS, 'user:description_value' => 'Goodbye', ], false], 'Filter auth' => ['user:auth', [ 'user:auth_operator' => select::EQUAL_TO, 'user:auth_value' => 'manual', ], true], 'Filter auth (no match)' => ['user:auth', [ 'user:auth_operator' => select::EQUAL_TO, 'user:auth_value' => 'ldap', ], false], 'Filter username' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'zoe1', ], true], 'Filter username (no match)' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'alfie1', ], false], 'Filter idnumber' => ['user:idnumber', [ 'user:idnumber_operator' => text::IS_EQUAL_TO, 'user:idnumber_value' => 'Z0001', ], true], 'Filter idnumber (no match)' => ['user:idnumber', [ 'user:idnumber_operator' => text::IS_EQUAL_TO, 'user:idnumber_value' => 'A0001', ], false], 'Filter institution' => ['user:institution', [ 'user:institution_operator' => text::IS_EQUAL_TO, 'user:institution_value' => 'Farm', ], true], 'Filter institution (no match)' => ['user:institution', [ 'user:institution_operator' => text::IS_EQUAL_TO, 'user:institution_value' => 'University', ], false], 'Filter department' => ['user:department', [ 'user:department_operator' => text::IS_EQUAL_TO, 'user:department_value' => 'Stable', ], true], 'Filter department (no match)' => ['user:department', [ 'user:department_operator' => text::IS_EQUAL_TO, 'user:department_value' => 'Office', ], false], 'Filter moodlenetprofile' => ['user:moodlenetprofile', [ 'user:moodlenetprofile_operator' => text::IS_EQUAL_TO, 'user:moodlenetprofile_value' => '@zoe1@example.com', ], true], 'Filter moodlenetprofile (no match)' => ['user:moodlenetprofile', [ 'user:moodlenetprofile_operator' => text::IS_EQUAL_TO, 'user:moodlenetprofile_value' => '@alfie1@example.com', ], false], 'Filter suspended' => ['user:suspended', [ 'user:suspended_operator' => boolean_select::NOT_CHECKED, ], true], 'Filter suspended (no match)' => ['user:suspended', [ 'user:suspended_operator' => boolean_select::CHECKED, ], false], 'Filter confirmed' => ['user:confirmed', [ 'user:confirmed_operator' => boolean_select::CHECKED, ], true], 'Filter confirmed (no match)' => ['user:confirmed', [ 'user:confirmed_operator' => boolean_select::NOT_CHECKED, ], false], 'Filter timecreated' => ['user:timecreated', [ 'user:timecreated_operator' => date::DATE_RANGE, 'user:timecreated_from' => 1622502000, ], true], 'Filter timecreated (no match)' => ['user:timecreated', [ 'user:timecreated_operator' => date::DATE_RANGE, 'user:timecreated_from' => 1619823600, 'user:timecreated_to' => 1622502000, ], false], 'Filter lastaccess' => ['user:lastaccess', [ 'user:lastaccess_operator' => date::DATE_EMPTY, ], true], 'Filter lastaccess (no match)' => ['user:lastaccess', [ 'user:lastaccess_operator' => date::DATE_RANGE, 'user:lastaccess_from' => 1619823600, 'user:lastaccess_to' => 1622502000, ], false], // Tags. 'Filter tag name' => ['tag:name', [ 'tag:name_operator' => tags::EQUAL_TO, 'tag:name_value' => [-1], ], false], 'Filter tag name not empty' => ['tag:name', [ 'tag:name_operator' => tags::NOT_EMPTY, ], true], ]; } /** * Test datasource filters * * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider datasource_filters_provider */ public function test_datasource_filters(string $filtername, array $filtervalues, bool $expectmatch): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user([ 'username' => 'zoe1', 'email' => 'zoe1@example.com', 'firstname' => 'Zoe', 'middlename' => 'Zebediah', 'lastname' => 'Zebra', 'firstnamephonetic' => 'Eoz', 'lastnamephonetic' => 'Arbez', 'alternatename' => 'Zee', 'idnumber' => 'Z0001', 'institution' => 'Farm', 'department' => 'Stable', 'phone1' => '111', 'phone2' => '222', 'address' => 'Big Farm', 'city' => 'Barcelona', 'country' => 'ES', 'description' => 'Hello there', 'moodlenetprofile' => '@zoe1@example.com', 'interests' => ['Horses'], ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single column, and given filter. $report = $generator->create_report(['name' => 'Tasks', 'source' => users::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); if ($expectmatch) { $this->assertNotEmpty($content); // Merge report usernames into easily traversable array. $usernames = array_merge(...array_map('array_values', $content)); $this->assertContains($user->username, $usernames); } else { $this->assertEmpty($content); } } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $this->getDataGenerator()->create_custom_profile_field(['datatype' => 'text', 'name' => 'Hi', 'shortname' => 'hi']); $user = $this->getDataGenerator()->create_user(['profile_field_hi' => 'Hello']); $this->datasource_stress_test_columns(users::class); $this->datasource_stress_test_columns_aggregation(users::class); $this->datasource_stress_test_conditions(users::class, 'user:username'); } } tests/table/participants_search_test.php 0000644 00000431351 15151162244 0014603 0 ustar 00 <?php // This file is part of Moodle - https://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Provides {@link core_user_table_participants_search_test} class. * * @package core_user * @category test * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace core_user\table; use advanced_testcase; use context_course; use context_coursecat; use core_table\local\filter\filter; use core_table\local\filter\integer_filter; use core_table\local\filter\string_filter; use core_user\table\participants_filterset; use core_user\table\participants_search; use moodle_recordset; use stdClass; /** * Tests for the implementation of {@link core_user_table_participants_search} class. * * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class participants_search_test extends advanced_testcase { /** * Helper to convert a moodle_recordset to an array of records. * * @param moodle_recordset $recordset * @return array */ protected function convert_recordset_to_array(moodle_recordset $recordset): array { $records = []; foreach ($recordset as $record) { $records[$record->id] = $record; } $recordset->close(); return $records; } /** * Create and enrol a set of users into the specified course. * * @param stdClass $course * @param int $count * @param null|string $role * @return array */ protected function create_and_enrol_users(stdClass $course, int $count, ?string $role = null): array { $this->resetAfterTest(true); $users = []; for ($i = 0; $i < $count; $i++) { $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id, $role); $users[] = $user; } return $users; } /** * Create a new course with several types of user. * * @param int $editingteachers The number of editing teachers to create in the course. * @param int $teachers The number of non-editing teachers to create in the course. * @param int $students The number of students to create in the course. * @param int $norole The number of users with no role to create in the course. * @return stdClass */ protected function create_course_with_users(int $editingteachers, int $teachers, int $students, int $norole): stdClass { $data = (object) [ 'course' => $this->getDataGenerator()->create_course(), 'editingteachers' => [], 'teachers' => [], 'students' => [], 'norole' => [], ]; $data->context = context_course::instance($data->course->id); $data->editingteachers = $this->create_and_enrol_users($data->course, $editingteachers, 'editingteacher'); $data->teachers = $this->create_and_enrol_users($data->course, $teachers, 'teacher'); $data->students = $this->create_and_enrol_users($data->course, $students, 'student'); $data->norole = $this->create_and_enrol_users($data->course, $norole); return $data; } /** * Ensure that the roles filter works as expected with the provided test cases. * * @param array $usersdata The list of users and their roles to create * @param array $testroles The list of roles to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @dataProvider role_provider */ public function test_roles_filter(array $usersdata, array $testroles, int $jointype, int $count, array $expectedusers): void { global $DB; $roles = $DB->get_records_menu('role', [], '', 'shortname, id'); // Remove the default role. set_config('roleid', 0, 'enrol_manual'); $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $category = $DB->get_record('course_categories', ['id' => $course->category]); $categorycontext = context_coursecat::instance($category->id); $users = []; foreach ($usersdata as $username => $userdata) { $user = $this->getDataGenerator()->create_user(['username' => $username]); if (array_key_exists('courseroles', $userdata)) { $this->getDataGenerator()->enrol_user($user->id, $course->id, null); foreach ($userdata['courseroles'] as $rolename) { $this->getDataGenerator()->role_assign($roles[$rolename], $user->id, $coursecontext->id); } } if (array_key_exists('categoryroles', $userdata)) { foreach ($userdata['categoryroles'] as $rolename) { $this->getDataGenerator()->role_assign($roles[$rolename], $user->id, $categorycontext->id); } } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the role filter. $rolefilter = new integer_filter('roles'); $filterset->add_filter($rolefilter); // Configure the filter. foreach ($testroles as $rolename) { $rolefilter->add_filter_value((int) $roles[$rolename]); } $rolefilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for role tests. * * @return array */ public function role_provider(): array { $tests = [ // Users who only have one role each. 'Users in each role' => (object) [ 'users' => [ 'a' => [ 'courseroles' => [ 'student', ], ], 'b' => [ 'courseroles' => [ 'student', ], ], 'c' => [ 'courseroles' => [ 'editingteacher', ], ], 'd' => [ 'courseroles' => [ 'editingteacher', ], ], 'e' => [ 'courseroles' => [ 'teacher', ], ], 'f' => [ 'courseroles' => [ 'teacher', ], ], // User is enrolled in the course without role. 'g' => [ 'courseroles' => [ ], ], // User is a category manager and also enrolled without role in the course. 'h' => [ 'courseroles' => [ ], 'categoryroles' => [ 'manager', ], ], // User is a category manager and not enrolled in the course. // This user should not show up in any filter. 'i' => [ 'categoryroles' => [ 'manager', ], ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No role filter' => (object) [ 'roles' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 8, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', ], ], 'ANY: Filter on student' => (object) [ 'roles' => ['student'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'b', ], ], 'ANY: Filter on student, teacher' => (object) [ 'roles' => ['student', 'teacher'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'e', 'f', ], ], 'ANY: Filter on student, manager (category level role)' => (object) [ 'roles' => ['student', 'manager'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'h', ], ], 'ANY: Filter on student, coursecreator (not assigned)' => (object) [ 'roles' => ['student', 'coursecreator'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'b', ], ], // Tests for jointype: ALL. 'ALL: No role filter' => (object) [ 'roles' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 8, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', ], ], 'ALL: Filter on student' => (object) [ 'roles' => ['student'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'b', ], ], 'ALL: Filter on student, teacher' => (object) [ 'roles' => ['student', 'teacher'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on student, manager (category level role))' => (object) [ 'roles' => ['student', 'manager'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on student, coursecreator (not assigned))' => (object) [ 'roles' => ['student', 'coursecreator'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], // Tests for jointype: NONE. 'NONE: No role filter' => (object) [ 'roles' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 8, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', ], ], 'NONE: Filter on student' => (object) [ 'roles' => ['student'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 6, 'expectedusers' => [ 'c', 'd', 'e', 'f', 'g', 'h', ], ], 'NONE: Filter on student, teacher' => (object) [ 'roles' => ['student', 'teacher'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'c', 'd', 'g', 'h', ], ], 'NONE: Filter on student, manager (category level role))' => (object) [ 'roles' => ['student', 'manager'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'c', 'd', 'e', 'f', 'g', ], ], 'NONE: Filter on student, coursecreator (not assigned))' => (object) [ 'roles' => ['student', 'coursecreator'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 6, 'expectedusers' => [ 'c', 'd', 'e', 'f', 'g', 'h', ], ], ], ], 'Users with multiple roles' => (object) [ 'users' => [ 'a' => [ 'courseroles' => [ 'student', ], ], 'b' => [ 'courseroles' => [ 'student', 'teacher', ], ], 'c' => [ 'courseroles' => [ 'editingteacher', ], ], 'd' => [ 'courseroles' => [ 'editingteacher', ], ], 'e' => [ 'courseroles' => [ 'teacher', 'editingteacher', ], ], 'f' => [ 'courseroles' => [ 'teacher', ], ], // User is enrolled in the course without role. 'g' => [ 'courseroles' => [ ], ], // User is a category manager and also enrolled without role in the course. 'h' => [ 'courseroles' => [ ], 'categoryroles' => [ 'manager', ], ], // User is a category manager and not enrolled in the course. // This user should not show up in any filter. 'i' => [ 'categoryroles' => [ 'manager', ], ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No role filter' => (object) [ 'roles' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 8, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', ], ], 'ANY: Filter on student' => (object) [ 'roles' => ['student'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'b', ], ], 'ANY: Filter on teacher' => (object) [ 'roles' => ['teacher'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'b', 'e', 'f', ], ], 'ANY: Filter on editingteacher' => (object) [ 'roles' => ['editingteacher'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'ANY: Filter on student, teacher' => (object) [ 'roles' => ['student', 'teacher'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'e', 'f', ], ], 'ANY: Filter on teacher, editingteacher' => (object) [ 'roles' => ['teacher', 'editingteacher'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, 'expectedusers' => [ 'b', 'c', 'd', 'e', 'f', ], ], 'ANY: Filter on student, manager (category level role)' => (object) [ 'roles' => ['student', 'manager'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'h', ], ], 'ANY: Filter on student, coursecreator (not assigned)' => (object) [ 'roles' => ['student', 'coursecreator'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'b', ], ], // Tests for jointype: ALL. 'ALL: No role filter' => (object) [ 'roles' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 8, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', ], ], 'ALL: Filter on student' => (object) [ 'roles' => ['student'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'b', ], ], 'ALL: Filter on teacher' => (object) [ 'roles' => ['teacher'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'b', 'e', 'f', ], ], 'ALL: Filter on editingteacher' => (object) [ 'roles' => ['editingteacher'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'ALL: Filter on student, teacher' => (object) [ 'roles' => ['student', 'teacher'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'b', ], ], 'ALL: Filter on teacher, editingteacher' => (object) [ 'roles' => ['teacher', 'editingteacher'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'e', ], ], 'ALL: Filter on student, manager (category level role)' => (object) [ 'roles' => ['student', 'manager'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on student, coursecreator (not assigned)' => (object) [ 'roles' => ['student', 'coursecreator'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], // Tests for jointype: NONE. 'NONE: No role filter' => (object) [ 'roles' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 8, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', ], ], 'NONE: Filter on student' => (object) [ 'roles' => ['student'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 6, 'expectedusers' => [ 'c', 'd', 'e', 'f', 'g', 'h', ], ], 'NONE: Filter on teacher' => (object) [ 'roles' => ['teacher'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'a', 'c', 'd', 'g', 'h', ], ], 'NONE: Filter on editingteacher' => (object) [ 'roles' => ['editingteacher'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'f', 'g', 'h', ], ], 'NONE: Filter on student, teacher' => (object) [ 'roles' => ['student', 'teacher'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'c', 'd', 'e', 'g', 'h', ], ], 'NONE: Filter on student, teacher' => (object) [ 'roles' => ['teacher', 'editingteacher'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'g', 'h', ], ], 'NONE: Filter on student, manager (category level role)' => (object) [ 'roles' => ['student', 'manager'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'c', 'd', 'e', 'f', 'g', ], ], 'NONE: Filter on student, coursecreator (not assigned)' => (object) [ 'roles' => ['student', 'coursecreator'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 6, 'expectedusers' => [ 'c', 'd', 'e', 'f', 'g', 'h', ], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'roles' => $expectdata->roles, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } /** * Test participant search country filter * * @param array $usersdata * @param array $countries * @param int $jointype * @param array $expectedusers * * @dataProvider country_provider */ public function test_country_filter(array $usersdata, array $countries, int $jointype, array $expectedusers): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $users = []; foreach ($usersdata as $username => $country) { $users[$username] = $this->getDataGenerator()->create_and_enrol($course, 'student', (object) [ 'username' => $username, 'country' => $country, ]); } // Add filters (courseid is required). $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); $filterset->add_filter(new string_filter('country', $jointype, $countries)); // Run the search, assert count matches the number of expected users. $search = new participants_search($course, context_course::instance($course->id), $filterset); $this->assertEquals(count($expectedusers), $search->get_total_participants_count()); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); // Assert that each expected user is within the participant records. $records = $this->convert_recordset_to_array($rs); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for {@see test_country_filter} * * @return array */ public function country_provider(): array { $tests = [ 'users' => [ 'user1' => 'DE', 'user2' => 'ES', 'user3' => 'ES', 'user4' => 'GB', ], 'expects' => [ // Tests for jointype: ANY. 'ANY: No filter' => (object) [ 'countries' => [], 'jointype' => filter::JOINTYPE_ANY, 'expectedusers' => [ 'user1', 'user2', 'user3', 'user4', ], ], 'ANY: Matching filters' => (object) [ 'countries' => [ 'DE', 'GB', ], 'jointype' => filter::JOINTYPE_ANY, 'expectedusers' => [ 'user1', 'user4', ], ], 'ANY: Non-matching filters' => (object) [ 'countries' => [ 'RU', ], 'jointype' => filter::JOINTYPE_ANY, 'expectedusers' => [], ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'countries' => [], 'jointype' => filter::JOINTYPE_ALL, 'expectedusers' => [ 'user1', 'user2', 'user3', 'user4', ], ], 'ALL: Matching filters' => (object) [ 'countries' => [ 'DE', 'GB', ], 'jointype' => filter::JOINTYPE_ALL, 'expectedusers' => [ 'user1', 'user4', ], ], 'ALL: Non-matching filters' => (object) [ 'countries' => [ 'RU', ], 'jointype' => filter::JOINTYPE_ALL, 'expectedusers' => [], ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'countries' => [], 'jointype' => filter::JOINTYPE_NONE, 'expectedusers' => [ 'user1', 'user2', 'user3', 'user4', ], ], 'NONE: Matching filters' => (object) [ 'countries' => [ 'DE', 'GB', ], 'jointype' => filter::JOINTYPE_NONE, 'expectedusers' => [ 'user2', 'user3', ], ], 'NONE: Non-matching filters' => (object) [ 'countries' => [ 'RU', ], 'jointype' => filter::JOINTYPE_NONE, 'expectedusers' => [ 'user1', 'user2', 'user3', 'user4', ], ], ], ]; $finaltests = []; foreach ($tests['expects'] as $testname => $test) { $finaltests[$testname] = [ 'users' => $tests['users'], 'countries' => $test->countries, 'jointype' => $test->jointype, 'expectedusers' => $test->expectedusers, ]; } return $finaltests; } /** * Ensure that the keywords filter works as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $keywords The list of keywords to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @param string $asuser If non-blank, uses that user account (for identify field permission checks) * @dataProvider keywords_provider */ public function test_keywords_filter(array $usersdata, array $keywords, int $jointype, int $count, array $expectedusers, string $asuser): void { global $DB; $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; // Create the custom user profile field and put it into showuseridentity. $this->getDataGenerator()->create_custom_profile_field( ['datatype' => 'text', 'shortname' => 'frog', 'name' => 'Fave frog']); set_config('showuseridentity', 'email,profile_field_frog'); foreach ($usersdata as $username => $userdata) { // Prevent randomly generated field values that may cause false fails. $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname']; $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname']; $userdata['middlename'] = $userdata['middlename'] ?? ''; $userdata['alternatename'] = $userdata['alternatename'] ?? $username; $user = $this->getDataGenerator()->create_user($userdata); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(10, 10, 10, 10); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the keyword filter. $keywordfilter = new string_filter('keywords'); $filterset->add_filter($keywordfilter); // Configure the filter. foreach ($keywords as $keyword) { $keywordfilter->add_filter_value($keyword); } $keywordfilter->set_join_type($jointype); if ($asuser) { $this->setUser($DB->get_record('user', ['username' => $asuser])); } // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for keywords tests. * * @return array */ public function keywords_provider(): array { $tests = [ // Users where the keyword matches basic user fields such as names and email. 'Users with basic names' => (object) [ 'users' => [ 'adam.ant' => [ 'firstname' => 'Adam', 'lastname' => 'Ant', ], 'barbara.bennett' => [ 'firstname' => 'Barbara', 'lastname' => 'Bennett', 'alternatename' => 'Babs', 'firstnamephonetic' => 'Barbra', 'lastnamephonetic' => 'Benit', 'profile_field_frog' => 'Kermit', ], 'colin.carnforth' => [ 'firstname' => 'Colin', 'lastname' => 'Carnforth', 'middlename' => 'Jeffery', ], 'tony.rogers' => [ 'firstname' => 'Anthony', 'lastname' => 'Rogers', 'lastnamephonetic' => 'Rowjours', 'profile_field_frog' => 'Mr Toad', ], 'sarah.rester' => [ 'firstname' => 'Sarah', 'lastname' => 'Rester', 'email' => 'zazu@example.com', 'firstnamephonetic' => 'Sera', ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter' => (object) [ 'keywords' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'ANY: Filter on first name only' => (object) [ 'keywords' => ['adam'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'adam.ant', ], ], 'ANY: Filter on last name only' => (object) [ 'keywords' => ['BeNNeTt'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'ANY: Filter on first/Last name' => (object) [ 'keywords' => ['ant'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'adam.ant', 'tony.rogers', ], ], 'ANY: Filter on fullname only' => (object) [ 'keywords' => ['Barbara Bennett'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'ANY: Filter on middlename only' => (object) [ 'keywords' => ['Jeff'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'colin.carnforth', ], ], 'ANY: Filter on username (no match)' => (object) [ 'keywords' => ['sara.rester'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 0, 'expectedusers' => [], ], 'ANY: Filter on email only' => (object) [ 'keywords' => ['zazu'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'sarah.rester', ], ], 'ANY: Filter on first name phonetic only' => (object) [ 'keywords' => ['Sera'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'sarah.rester', ], ], 'ANY: Filter on last name phonetic only' => (object) [ 'keywords' => ['jour'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'tony.rogers', ], ], 'ANY: Filter on alternate name only' => (object) [ 'keywords' => ['Babs'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'ANY: Filter on multiple keywords (first/middle/last name)' => (object) [ 'keywords' => ['ant', 'Jeff', 'rog'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'tony.rogers', ], ], 'ANY: Filter on multiple keywords (phonetic/alternate names)' => (object) [ 'keywords' => ['era', 'Bab', 'ours'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'barbara.bennett', 'sarah.rester', 'tony.rogers', ], ], 'ANY: Filter on custom profile field' => (object) [ 'keywords' => ['Kermit', 'Mr Toad'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'barbara.bennett', 'tony.rogers', ], 'asuser' => 'admin' ], 'ANY: Filter on custom profile field (no permissions)' => (object) [ 'keywords' => ['Kermit', 'Mr Toad'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 0, 'expectedusers' => [], 'asuser' => 'barbara.bennett' ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'keywords' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 5, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'ALL: Filter on first name only' => (object) [ 'keywords' => ['adam'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'adam.ant', ], ], 'ALL: Filter on last name only' => (object) [ 'keywords' => ['BeNNeTt'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'ALL: Filter on first/Last name' => (object) [ 'keywords' => ['ant'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'adam.ant', 'tony.rogers', ], ], 'ALL: Filter on middlename only' => (object) [ 'keywords' => ['Jeff'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'colin.carnforth', ], ], 'ALL: Filter on username (no match)' => (object) [ 'keywords' => ['sara.rester'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on email only' => (object) [ 'keywords' => ['zazu'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'sarah.rester', ], ], 'ALL: Filter on first name phonetic only' => (object) [ 'keywords' => ['Sera'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'sarah.rester', ], ], 'ALL: Filter on last name phonetic only' => (object) [ 'keywords' => ['jour'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'tony.rogers', ], ], 'ALL: Filter on alternate name only' => (object) [ 'keywords' => ['Babs'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'ALL: Filter on multiple keywords (first/last name)' => (object) [ 'keywords' => ['ant', 'rog'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'tony.rogers', ], ], 'ALL: Filter on multiple keywords (first/middle/last name)' => (object) [ 'keywords' => ['ant', 'Jeff', 'rog'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on multiple keywords (phonetic/alternate names)' => (object) [ 'keywords' => ['Bab', 'bra', 'nit'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'ALL: Filter on custom profile field' => (object) [ 'keywords' => ['Kermit', 'Kermi'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], 'asuser' => 'admin', ], 'ALL: Filter on custom profile field (no permissions)' => (object) [ 'keywords' => ['Kermit', 'Kermi'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], 'asuser' => 'barbara.bennett', ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'keywords' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on first name only' => (object) [ 'keywords' => ['ara'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'tony.rogers', ], ], 'NONE: Filter on last name only' => (object) [ 'keywords' => ['BeNNeTt'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on first/Last name' => (object) [ 'keywords' => ['ar'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'adam.ant', 'tony.rogers', ], ], 'NONE: Filter on middlename only' => (object) [ 'keywords' => ['Jeff'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on username (no match)' => (object) [ 'keywords' => ['sara.rester'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on email' => (object) [ 'keywords' => ['zazu'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', ], ], 'NONE: Filter on first name phonetic only' => (object) [ 'keywords' => ['Sera'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', ], ], 'NONE: Filter on last name phonetic only' => (object) [ 'keywords' => ['jour'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'sarah.rester', ], ], 'NONE: Filter on alternate name only' => (object) [ 'keywords' => ['Babs'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on multiple keywords (first/last name)' => (object) [ 'keywords' => ['ara', 'rog'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', ], ], 'NONE: Filter on multiple keywords (first/middle/last name)' => (object) [ 'keywords' => ['ant', 'Jeff', 'rog'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'barbara.bennett', 'sarah.rester', ], ], 'NONE: Filter on multiple keywords (phonetic/alternate names)' => (object) [ 'keywords' => ['Bab', 'bra', 'nit'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on custom profile field' => (object) [ 'keywords' => ['Kermit', 'Mr Toad'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'sarah.rester', ], 'asuser' => 'admin', ], 'NONE: Filter on custom profile field (no permissions)' => (object) [ 'keywords' => ['Kermit', 'Mr Toad'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], 'asuser' => 'barbara.bennett', ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'keywords' => $expectdata->keywords, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, 'asuser' => $expectdata->asuser ?? '' ]; } } return $finaltests; } /** * Ensure that the enrolment status filter works as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $statuses The list of statuses to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @dataProvider status_provider */ public function test_status_filter(array $usersdata, array $statuses, int $jointype, int $count, array $expectedusers): void { $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; // Ensure sufficient capabilities to view all statuses. $this->setAdminUser(); // Ensure all enrolment methods enabled. $enrolinstances = enrol_get_instances($course->id, false); foreach ($enrolinstances as $instance) { $plugin = enrol_get_plugin($instance->enrol); $plugin->update_status($instance, ENROL_INSTANCE_ENABLED); } foreach ($usersdata as $username => $userdata) { $user = $this->getDataGenerator()->create_user(['username' => $username]); if (array_key_exists('status', $userdata)) { foreach ($userdata['status'] as $enrolmethod => $status) { $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student', $enrolmethod, 0, 0, $status); } } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the status filter. $statusfilter = new integer_filter('status'); $filterset->add_filter($statusfilter); // Configure the filter. foreach ($statuses as $status) { $statusfilter->add_filter_value($status); } $statusfilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for status filter tests. * * @return array */ public function status_provider(): array { $tests = [ // Users with different statuses and enrolment methods (so multiple statuses are possible for the same user). 'Users with different enrolment statuses' => (object) [ 'users' => [ 'a' => [ 'status' => [ 'manual' => ENROL_USER_ACTIVE, ] ], 'b' => [ 'status' => [ 'self' => ENROL_USER_ACTIVE, ] ], 'c' => [ 'status' => [ 'manual' => ENROL_USER_SUSPENDED, ] ], 'd' => [ 'status' => [ 'self' => ENROL_USER_SUSPENDED, ] ], 'e' => [ 'status' => [ 'manual' => ENROL_USER_ACTIVE, 'self' => ENROL_USER_SUSPENDED, ] ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter' => (object) [ 'status' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'ANY: Filter on active only' => (object) [ 'status' => [ENROL_USER_ACTIVE], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'e', ], ], 'ANY: Filter on suspended only' => (object) [ 'status' => [ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'ANY: Filter on multiple statuses' => (object) [ 'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'status' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'ALL: Filter on active only' => (object) [ 'status' => [ENROL_USER_ACTIVE], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'e', ], ], 'ALL: Filter on suspended only' => (object) [ 'status' => [ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'ALL: Filter on multiple statuses' => (object) [ 'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'e', ], ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'status' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'NONE: Filter on active only' => (object) [ 'status' => [ENROL_USER_ACTIVE], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'NONE: Filter on suspended only' => (object) [ 'status' => [ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'e', ], ], 'NONE: Filter on multiple statuses' => (object) [ 'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'status' => $expectdata->status, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } /** * Ensure that the enrolment methods filter works as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $enrolmethods The list of enrolment methods to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @dataProvider enrolments_provider */ public function test_enrolments_filter(array $usersdata, array $enrolmethods, int $jointype, int $count, array $expectedusers): void { $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; // Ensure all enrolment methods enabled and mapped for setting the filter later. $enrolinstances = enrol_get_instances($course->id, false); $enrolinstancesmap = []; foreach ($enrolinstances as $instance) { $plugin = enrol_get_plugin($instance->enrol); $plugin->update_status($instance, ENROL_INSTANCE_ENABLED); $enrolinstancesmap[$instance->enrol] = (int) $instance->id; } foreach ($usersdata as $username => $userdata) { $user = $this->getDataGenerator()->create_user(['username' => $username]); if (array_key_exists('enrolmethods', $userdata)) { foreach ($userdata['enrolmethods'] as $enrolmethod) { $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student', $enrolmethod); } } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the enrolment methods filter. $enrolmethodfilter = new integer_filter('enrolments'); $filterset->add_filter($enrolmethodfilter); // Configure the filter. foreach ($enrolmethods as $enrolmethod) { $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]); } $enrolmethodfilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for enrolments filter tests. * * @return array */ public function enrolments_provider(): array { $tests = [ // Users with different enrolment methods. 'Users with different enrolment methods' => (object) [ 'users' => [ 'a' => [ 'enrolmethods' => [ 'manual', ] ], 'b' => [ 'enrolmethods' => [ 'self', ] ], 'c' => [ 'enrolmethods' => [ 'manual', 'self', ] ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter' => (object) [ 'enrolmethods' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ANY: Filter by manual enrolments only' => (object) [ 'enrolmethods' => ['manual'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: Filter by self enrolments only' => (object) [ 'enrolmethods' => ['self'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'b', 'c', ], ], 'ANY: Filter by multiple enrolment methods' => (object) [ 'enrolmethods' => ['manual', 'self'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'enrolmethods' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ALL: Filter by manual enrolments only' => (object) [ 'enrolmethods' => ['manual'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: Filter by multiple enrolment methods' => (object) [ 'enrolmethods' => ['manual', 'self'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'c', ], ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'enrolmethods' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'NONE: Filter by manual enrolments only' => (object) [ 'enrolmethods' => ['manual'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'b', ], ], 'NONE: Filter by multiple enrolment methods' => (object) [ 'enrolmethods' => ['manual', 'self'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'enrolmethods' => $expectdata->enrolmethods, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } /** * Ensure that the groups filter works as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $groupsavailable The names of groups that should be created in the course * @param array $filtergroups The names of groups to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @dataProvider groups_provider */ public function test_groups_filter(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype, int $count, array $expectedusers): void { $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; // Prepare data for filtering by users in no groups. $nogroupsdata = (object) [ 'id' => USERSWITHOUTGROUP, ]; // Map group names to group data. $groupsdata = ['nogroups' => $nogroupsdata]; foreach ($groupsavailable as $groupname) { $groupinfo = [ 'courseid' => $course->id, 'name' => $groupname, ]; $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo); } foreach ($usersdata as $username => $userdata) { $user = $this->getDataGenerator()->create_user(['username' => $username]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); if (array_key_exists('groups', $userdata)) { foreach ($userdata['groups'] as $groupname) { $userinfo = [ 'userid' => $user->id, 'groupid' => (int) $groupsdata[$groupname]->id, ]; $this->getDataGenerator()->create_group_member($userinfo); } } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the groups filter. $groupsfilter = new integer_filter('groups'); $filterset->add_filter($groupsfilter); // Configure the filter. foreach ($filtergroups as $filtergroupname) { $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id); } $groupsfilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for groups filter tests. * * @return array */ public function groups_provider(): array { $tests = [ 'Users in different groups' => (object) [ 'groupsavailable' => [ 'groupa', 'groupb', 'groupc', ], 'users' => [ 'a' => [ 'groups' => ['groupa'], ], 'b' => [ 'groups' => ['groupb'], ], 'c' => [ 'groups' => ['groupa', 'groupb'], ], 'd' => [ 'groups' => [], ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter' => (object) [ 'groups' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], 'ANY: Filter on a single group' => (object) [ 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: Filter on a group with no members' => (object) [ 'groups' => ['groupc'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 0, 'expectedusers' => [], ], 'ANY: Filter on multiple groups' => (object) [ 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ANY: Filter on members of no groups only' => (object) [ 'groups' => ['nogroups'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'd', ], ], 'ANY: Filter on a single group or no groups' => (object) [ 'groups' => ['groupa', 'nogroups'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'c', 'd', ], ], 'ANY: Filter on multiple groups or no groups' => (object) [ 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'groups' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], 'ALL: Filter on a single group' => (object) [ 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: Filter on a group with no members' => (object) [ 'groups' => ['groupc'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on members of no groups only' => (object) [ 'groups' => ['nogroups'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'd', ], ], 'ALL: Filter on multiple groups' => (object) [ 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'c', ], ], 'ALL: Filter on a single group and no groups' => (object) [ 'groups' => ['groupa', 'nogroups'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on multiple groups and no groups' => (object) [ 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'groups' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], 'NONE: Filter on a single group' => (object) [ 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'b', 'd', ], ], 'NONE: Filter on a group with no members' => (object) [ 'groups' => ['groupc'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], 'NONE: Filter on members of no groups only' => (object) [ 'groups' => ['nogroups'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'NONE: Filter on multiple groups' => (object) [ 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'd', ], ], 'NONE: Filter on a single group and no groups' => (object) [ 'groups' => ['groupa', 'nogroups'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'b', ], ], 'NONE: Filter on multiple groups and no groups' => (object) [ 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'groupsavailable' => $testdata->groupsavailable, 'filtergroups' => $expectdata->groups, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } /** * Ensure that the groups filter works as expected when separate groups mode is enabled, with the provided test cases. * * @param array $usersdata The list of users to create * @param array $groupsavailable The names of groups that should be created in the course * @param array $filtergroups The names of groups to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @param string $loginusername The user to login as for the tests * @dataProvider groups_separate_provider */ public function test_groups_filter_separate_groups(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype, int $count, array $expectedusers, string $loginusername): void { $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; // Enable separate groups mode on the course. $course->groupmode = SEPARATEGROUPS; $course->groupmodeforce = true; update_course($course); // Prepare data for filtering by users in no groups. $nogroupsdata = (object) [ 'id' => USERSWITHOUTGROUP, ]; // Map group names to group data. $groupsdata = ['nogroups' => $nogroupsdata]; foreach ($groupsavailable as $groupname) { $groupinfo = [ 'courseid' => $course->id, 'name' => $groupname, ]; $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo); } foreach ($usersdata as $username => $userdata) { $user = $this->getDataGenerator()->create_user(['username' => $username]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); if (array_key_exists('groups', $userdata)) { foreach ($userdata['groups'] as $groupname) { $userinfo = [ 'userid' => $user->id, 'groupid' => (int) $groupsdata[$groupname]->id, ]; $this->getDataGenerator()->create_group_member($userinfo); } } $users[$username] = $user; if ($username == $loginusername) { $loginuser = $user; } } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Log in as the user to be tested. $this->setUser($loginuser); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the groups filter. $groupsfilter = new integer_filter('groups'); $filterset->add_filter($groupsfilter); // Configure the filter. foreach ($filtergroups as $filtergroupname) { $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id); } $groupsfilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); // Tests on user in no groups should throw an exception as they are not supported (participants are not visible to them). if (in_array('exception', $expectedusers)) { $this->expectException(\coding_exception::class); $rs = $search->get_participants(); } else { // All other cases are tested as normal. $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } } /** * Data provider for groups filter tests. * * @return array */ public function groups_separate_provider(): array { $tests = [ 'Users in different groups with separate groups mode enabled' => (object) [ 'groupsavailable' => [ 'groupa', 'groupb', 'groupc', ], 'users' => [ 'a' => [ 'groups' => ['groupa'], ], 'b' => [ 'groups' => ['groupb'], ], 'c' => [ 'groups' => ['groupa', 'groupb'], ], 'd' => [ 'groups' => [], ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: No filter, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ANY: No filter, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 0, 'expectedusers' => ['exception'], ], 'ANY: Filter on a single group, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: Filter on a single group, user in multple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: Filter on a single group, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 0, 'expectedusers' => ['exception'], ], 'ANY: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: Filter on multiple groups, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ANY: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], // Tests for jointype: ALL. 'ALL: No filter, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: No filter, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ALL: No filter, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => ['exception'], ], 'ALL: Filter on a single group, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: Filter on a single group, user in multple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: Filter on a single group, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => ['exception'], ], 'ALL: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: Filter on multiple groups, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'c', ], ], 'ALL: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'c', ], ], // Tests for jointype: NONE. 'NONE: No filter, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'NONE: No filter, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'NONE: No filter, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => ['exception'], ], 'NONE: Filter on a single group, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], 'NONE: Filter on a single group, user in multple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'b', ], ], 'NONE: Filter on a single group, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => ['exception'], ], 'NONE: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], 'NONE: Filter on multiple groups, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], 'NONE: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'groupsavailable' => $testdata->groupsavailable, 'filtergroups' => $expectdata->groups, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, 'loginusername' => $expectdata->loginuser, ]; } } return $finaltests; } /** * Ensure that the last access filter works as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $accesssince The last access data to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @dataProvider accesssince_provider */ public function test_accesssince_filter(array $usersdata, array $accesssince, int $jointype, int $count, array $expectedusers): void { $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; foreach ($usersdata as $username => $userdata) { $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']); $user = $this->getDataGenerator()->create_user(['username' => $username]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); // Create the record of the user's last access to the course. if ($usertimestamp > 0) { $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp); } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the last access filter. $lastaccessfilter = new integer_filter('accesssince'); $filterset->add_filter($lastaccessfilter); // Configure the filter. foreach ($accesssince as $accessstring) { $lastaccessfilter->add_filter_value(strtotime($accessstring)); } $lastaccessfilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for last access filter tests. * * @return array */ public function accesssince_provider(): array { $tests = [ // Users with different last access times. 'Users in different groups' => (object) [ 'users' => [ 'a' => [ 'lastlogin' => '-3 days', ], 'b' => [ 'lastlogin' => '-2 weeks', ], 'c' => [ 'lastlogin' => '-5 months', ], 'd' => [ 'lastlogin' => '-11 months', ], 'e' => [ // Never logged in. 'lastlogin' => '', ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter' => (object) [ 'accesssince' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'ANY: Filter on last login more than 1 year ago' => (object) [ 'accesssince' => ['-1 year'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'e', ], ], 'ANY: Filter on last login more than 6 months ago' => (object) [ 'accesssince' => ['-6 months'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'd', 'e', ], ], 'ANY: Filter on last login more than 3 weeks ago' => (object) [ 'accesssince' => ['-3 weeks'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'ANY: Filter on last login more than 5 days ago' => (object) [ 'accesssince' => ['-5 days'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 4, 'expectedusers' => [ 'b', 'c', 'd', 'e', ], ], 'ANY: Filter on last login more than 2 days ago' => (object) [ 'accesssince' => ['-2 days'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'accesssince' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'ALL: Filter on last login more than 1 year ago' => (object) [ 'accesssince' => ['-1 year'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'e', ], ], 'ALL: Filter on last login more than 6 months ago' => (object) [ 'accesssince' => ['-6 months'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'd', 'e', ], ], 'ALL: Filter on last login more than 3 weeks ago' => (object) [ 'accesssince' => ['-3 weeks'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'ALL: Filter on last login more than 5 days ago' => (object) [ 'accesssince' => ['-5 days'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 4, 'expectedusers' => [ 'b', 'c', 'd', 'e', ], ], 'ALL: Filter on last login more than 2 days ago' => (object) [ 'accesssince' => ['-2 days'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'accesssince' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'NONE: Filter on last login more than 1 year ago' => (object) [ 'accesssince' => ['-1 year'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], 'NONE: Filter on last login more than 6 months ago' => (object) [ 'accesssince' => ['-6 months'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'NONE: Filter on last login more than 3 weeks ago' => (object) [ 'accesssince' => ['-3 weeks'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'a', 'b', ], ], 'NONE: Filter on last login more than 5 days ago' => (object) [ 'accesssince' => ['-5 days'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'a', ], ], 'NONE: Filter on last login more than 2 days ago' => (object) [ 'accesssince' => ['-2 days'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'accesssince' => $expectdata->accesssince, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } /** * Ensure that the joins between filters in the filterset work as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $filterdata The data to filter by * @param array $groupsavailable The names of groups that should be created in the course * @param int $jointype The join type to used between each filter being applied * @param int $count The expected count * @param array $expectedusers * @dataProvider filterset_joins_provider */ public function test_filterset_joins(array $usersdata, array $filterdata, array $groupsavailable, int $jointype, int $count, array $expectedusers): void { global $DB; // Ensure sufficient capabilities to view all statuses. $this->setAdminUser(); // Remove the default role. set_config('roleid', 0, 'enrol_manual'); $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $roles = $DB->get_records_menu('role', [], '', 'shortname, id'); $users = []; // Ensure all enrolment methods are enabled (and mapped where required for filtering later). $enrolinstances = enrol_get_instances($course->id, false); $enrolinstancesmap = []; foreach ($enrolinstances as $instance) { $plugin = enrol_get_plugin($instance->enrol); $plugin->update_status($instance, ENROL_INSTANCE_ENABLED); $enrolinstancesmap[$instance->enrol] = (int) $instance->id; } // Create the required course groups and mapping. $nogroupsdata = (object) [ 'id' => USERSWITHOUTGROUP, ]; $groupsdata = ['nogroups' => $nogroupsdata]; foreach ($groupsavailable as $groupname) { $groupinfo = [ 'courseid' => $course->id, 'name' => $groupname, ]; $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo); } // Create test users. foreach ($usersdata as $username => $userdata) { $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']); unset($userdata['lastlogin']); // Prevent randomly generated field values that may cause false fails. $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname']; $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname']; $userdata['middlename'] = $userdata['middlename'] ?? ''; $userdata['alternatename'] = $userdata['alternatename'] ?? $username; $user = $this->getDataGenerator()->create_user($userdata); foreach ($userdata['enrolments'] as $details) { $this->getDataGenerator()->enrol_user($user->id, $course->id, $roles[$details['role']], $details['method'], 0, 0, $details['status']); } foreach ($userdata['groups'] as $groupname) { $userinfo = [ 'userid' => $user->id, 'groupid' => (int) $groupsdata[$groupname]->id, ]; $this->getDataGenerator()->create_group_member($userinfo); } if ($usertimestamp > 0) { $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp); } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(10, 10, 10, 10); // Create the basic filterset. $filterset = new participants_filterset(); $filterset->set_join_type($jointype); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Apply the keywords filter if required. if (array_key_exists('keywords', $filterdata)) { $keywordfilter = new string_filter('keywords'); $filterset->add_filter($keywordfilter); foreach ($filterdata['keywords']['values'] as $keyword) { $keywordfilter->add_filter_value($keyword); } $keywordfilter->set_join_type($filterdata['keywords']['jointype']); } // Apply enrolment methods filter if required. if (array_key_exists('enrolmethods', $filterdata)) { $enrolmethodfilter = new integer_filter('enrolments'); $filterset->add_filter($enrolmethodfilter); foreach ($filterdata['enrolmethods']['values'] as $enrolmethod) { $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]); } $enrolmethodfilter->set_join_type($filterdata['enrolmethods']['jointype']); } // Apply roles filter if required. if (array_key_exists('courseroles', $filterdata)) { $rolefilter = new integer_filter('roles'); $filterset->add_filter($rolefilter); foreach ($filterdata['courseroles']['values'] as $rolename) { $rolefilter->add_filter_value((int) $roles[$rolename]); } $rolefilter->set_join_type($filterdata['courseroles']['jointype']); } // Apply status filter if required. if (array_key_exists('status', $filterdata)) { $statusfilter = new integer_filter('status'); $filterset->add_filter($statusfilter); foreach ($filterdata['status']['values'] as $status) { $statusfilter->add_filter_value($status); } $statusfilter->set_join_type($filterdata['status']['jointype']); } // Apply groups filter if required. if (array_key_exists('groups', $filterdata)) { $groupsfilter = new integer_filter('groups'); $filterset->add_filter($groupsfilter); foreach ($filterdata['groups']['values'] as $filtergroupname) { $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id); } $groupsfilter->set_join_type($filterdata['groups']['jointype']); } // Apply last access filter if required. if (array_key_exists('accesssince', $filterdata)) { $lastaccessfilter = new integer_filter('accesssince'); $filterset->add_filter($lastaccessfilter); foreach ($filterdata['accesssince']['values'] as $accessstring) { $lastaccessfilter->add_filter_value(strtotime($accessstring)); } $lastaccessfilter->set_join_type($filterdata['accesssince']['jointype']); } // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for filterset join tests. * * @return array */ public function filterset_joins_provider(): array { $tests = [ // Users with different configurations. 'Users with different configurations' => (object) [ 'groupsavailable' => [ 'groupa', 'groupb', 'groupc', ], 'users' => [ 'adam.ant' => [ 'firstname' => 'Adam', 'lastname' => 'Ant', 'enrolments' => [ [ 'role' => 'student', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], ], 'groups' => ['groupa'], 'lastlogin' => '-3 days', ], 'barbara.bennett' => [ 'firstname' => 'Barbara', 'lastname' => 'Bennett', 'enrolments' => [ [ 'role' => 'student', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], [ 'role' => 'teacher', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], ], 'groups' => ['groupb'], 'lastlogin' => '-2 weeks', ], 'colin.carnforth' => [ 'firstname' => 'Colin', 'lastname' => 'Carnforth', 'enrolments' => [ [ 'role' => 'editingteacher', 'method' => 'self', 'status' => ENROL_USER_SUSPENDED, ], ], 'groups' => ['groupa', 'groupb'], 'lastlogin' => '-5 months', ], 'tony.rogers' => [ 'firstname' => 'Anthony', 'lastname' => 'Rogers', 'enrolments' => [ [ 'role' => 'editingteacher', 'method' => 'self', 'status' => ENROL_USER_SUSPENDED, ], ], 'groups' => [], 'lastlogin' => '-10 months', ], 'sarah.rester' => [ 'firstname' => 'Sarah', 'lastname' => 'Rester', 'email' => 'zazu@example.com', 'enrolments' => [ [ 'role' => 'teacher', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], [ 'role' => 'editingteacher', 'method' => 'self', 'status' => ENROL_USER_SUSPENDED, ], ], 'groups' => [], 'lastlogin' => '-11 months', ], 'morgan.crikeyson' => [ 'firstname' => 'Morgan', 'lastname' => 'Crikeyson', 'enrolments' => [ [ 'role' => 'teacher', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], ], 'groups' => ['groupa'], 'lastlogin' => '-1 week', ], 'jonathan.bravo' => [ 'firstname' => 'Jonathan', 'lastname' => 'Bravo', 'enrolments' => [ [ 'role' => 'student', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], ], 'groups' => [], // Never logged in. 'lastlogin' => '', ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filters in filterset' => (object) [ 'filterdata' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 7, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', 'morgan.crikeyson', 'jonathan.bravo', ], ], 'ANY: Filterset containing a single filter type' => (object) [ 'filterdata' => [ 'enrolmethods' => [ 'values' => ['self'], 'jointype' => filter::JOINTYPE_ANY, ], ], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'ANY: Filterset matching all filter types on different users' => (object) [ 'filterdata' => [ // Match Adam only. 'keywords' => [ 'values' => ['adam'], 'jointype' => filter::JOINTYPE_ALL, ], // Match Sarah only. 'enrolmethods' => [ 'values' => ['manual', 'self'], 'jointype' => filter::JOINTYPE_ALL, ], // Match Barbara only. 'courseroles' => [ 'values' => ['student', 'teacher'], 'jointype' => filter::JOINTYPE_ALL, ], // Match Sarah only. 'status' => [ 'values' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ALL, ], // Match Colin only. 'groups' => [ 'values' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ALL, ], // Match Jonathan only. 'accesssince' => [ 'values' => ['-1 year'], 'jointype' => filter::JOINTYPE_ALL, ], ], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, // Morgan and Tony are not matched, to confirm filtering is not just returning all users. 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'sarah.rester', 'jonathan.bravo', ], ], // Tests for jointype: ALL. 'ALL: No filters in filterset' => (object) [ 'filterdata' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 7, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', 'morgan.crikeyson', 'jonathan.bravo', ], ], 'ALL: Filterset containing a single filter type' => (object) [ 'filterdata' => [ 'enrolmethods' => [ 'values' => ['self'], 'jointype' => filter::JOINTYPE_ANY, ], ], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'ALL: Filterset combining all filter types' => (object) [ 'filterdata' => [ // Exclude Adam, Tony, Morgan and Jonathan. 'keywords' => [ 'values' => ['ar'], 'jointype' => filter::JOINTYPE_ANY, ], // Exclude Colin and Tony. 'enrolmethods' => [ 'values' => ['manual'], 'jointype' => filter::JOINTYPE_ANY, ], // Exclude Adam, Barbara and Jonathan. 'courseroles' => [ 'values' => ['student'], 'jointype' => filter::JOINTYPE_NONE, ], // Exclude Colin and Tony. 'status' => [ 'values' => [ENROL_USER_ACTIVE], 'jointype' => filter::JOINTYPE_ALL, ], // Exclude Barbara. 'groups' => [ 'values' => ['groupa', 'nogroups'], 'jointype' => filter::JOINTYPE_ANY, ], // Exclude Adam, Colin and Barbara. 'accesssince' => [ 'values' => ['-6 months'], 'jointype' => filter::JOINTYPE_ALL, ], ], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'sarah.rester', ], ], // Tests for jointype: NONE. 'NONE: No filters in filterset' => (object) [ 'filterdata' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 7, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', 'morgan.crikeyson', 'jonathan.bravo', ], ], 'NONE: Filterset containing a single filter type' => (object) [ 'filterdata' => [ 'enrolmethods' => [ 'values' => ['self'], 'jointype' => filter::JOINTYPE_ANY, ], ], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'morgan.crikeyson', 'jonathan.bravo', ], ], 'NONE: Filterset combining all filter types' => (object) [ 'filterdata' => [ // Excludes Adam. 'keywords' => [ 'values' => ['adam'], 'jointype' => filter::JOINTYPE_ANY, ], // Excludes Colin, Tony and Sarah. 'enrolmethods' => [ 'values' => ['self'], 'jointype' => filter::JOINTYPE_ANY, ], // Excludes Jonathan. 'courseroles' => [ 'values' => ['student'], 'jointype' => filter::JOINTYPE_NONE, ], // Excludes Colin, Tony and Sarah. 'status' => [ 'values' => [ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ALL, ], // Excludes Adam, Colin, Tony, Sarah, Morgan and Jonathan. 'groups' => [ 'values' => ['groupa', 'nogroups'], 'jointype' => filter::JOINTYPE_ANY, ], // Excludes Tony and Sarah. 'accesssince' => [ 'values' => ['-6 months'], 'jointype' => filter::JOINTYPE_ALL, ], ], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'NONE: Filterset combining several filter types and a double-negative on keyword' => (object) [ 'jointype' => filter::JOINTYPE_NONE, 'filterdata' => [ // Note: This is a jointype NONE on the parent jointype NONE. // The result therefore negated in this instance. // Include Adam and Anthony. 'keywords' => [ 'values' => ['ant'], 'jointype' => filter::JOINTYPE_NONE, ], // Excludes Tony. 'status' => [ 'values' => [ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ALL, ], ], 'count' => 1, 'expectedusers' => [ 'adam.ant', ], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'filterdata' => $expectdata->filterdata, 'groupsavailable' => $testdata->groupsavailable, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } } tests/profilelib_test.php 0000644 00000035521 15151162244 0011614 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user; /** * Unit tests for user/profile/lib.php. * * @package core_user * @copyright 2014 The Open University * @licensehttp://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profilelib_test extends \advanced_testcase { /** * Load required test libraries */ public static function setUpBeforeClass(): void { global $CFG; require_once("{$CFG->dirroot}/user/profile/lib.php"); } /** * Tests profile_get_custom_fields function and checks it is consistent * with profile_user_record. */ public function test_get_custom_fields() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); // Add a custom field of textarea type. $id1 = $this->getDataGenerator()->create_custom_profile_field([ 'shortname' => 'frogdesc', 'name' => 'Description of frog', 'datatype' => 'textarea'])->id; // Check the field is returned. $result = profile_get_custom_fields(); $this->assertArrayHasKey($id1, $result); $this->assertEquals('frogdesc', $result[$id1]->shortname); // Textarea types are not included in user data though, so if we // use the 'only in user data' parameter, there is still nothing. $this->assertArrayNotHasKey($id1, profile_get_custom_fields(true)); // Check that profile_user_record returns same (no) fields. $this->assertObjectNotHasAttribute('frogdesc', profile_user_record($user->id)); // Check that profile_user_record returns all the fields when requested. $this->assertObjectHasAttribute('frogdesc', profile_user_record($user->id, false)); // Add another custom field, this time of normal text type. $id2 = $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'frogname', 'name' => 'Name of frog', 'datatype' => 'text'))->id; // Check both are returned using normal option. $result = profile_get_custom_fields(); $this->assertArrayHasKey($id2, $result); $this->assertEquals('frogname', $result[$id2]->shortname); // And check that only the one is returned the other way. $this->assertArrayHasKey($id2, profile_get_custom_fields(true)); // Check profile_user_record returns same field. $this->assertObjectHasAttribute('frogname', profile_user_record($user->id)); // Check that profile_user_record returns all the fields when requested. $this->assertObjectHasAttribute('frogname', profile_user_record($user->id, false)); } /** * Make sure that all profile fields can be initialised without arguments. */ public function test_default_constructor() { global $DB, $CFG; require_once($CFG->dirroot . '/user/profile/definelib.php'); $datatypes = profile_list_datatypes(); foreach ($datatypes as $datatype => $datatypename) { require_once($CFG->dirroot . '/user/profile/field/' . $datatype . '/field.class.php'); $newfield = 'profile_field_' . $datatype; $formfield = new $newfield(); $this->assertNotNull($formfield); } } /** * Test profile_view function */ public function test_profile_view() { global $USER; $this->resetAfterTest(); // Course without sections. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); $this->setUser($user); // Redirect events to the sink, so we can recover them later. $sink = $this->redirectEvents(); profile_view($user, $context, $course); $events = $sink->get_events(); $event = reset($events); // Check the event details are correct. $this->assertInstanceOf('\core\event\user_profile_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEquals($user->id, $event->relateduserid); $this->assertEquals($course->id, $event->other['courseid']); $this->assertEquals($course->shortname, $event->other['courseshortname']); $this->assertEquals($course->fullname, $event->other['coursefullname']); profile_view($user, $usercontext); $events = $sink->get_events(); $event = array_pop($events); $sink->close(); $this->assertInstanceOf('\core\event\user_profile_viewed', $event); $this->assertEquals($usercontext, $event->get_context()); $this->assertEquals($user->id, $event->relateduserid); } /** * Test that {@link user_not_fully_set_up()} takes required custom fields into account. */ public function test_profile_has_required_custom_fields_set() { global $CFG; require_once($CFG->dirroot.'/mnet/lib.php'); $this->resetAfterTest(); // Add a required, visible, unlocked custom field. $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'house', 'name' => 'House', 'required' => 1, 'visible' => 1, 'locked' => 0, 'datatype' => 'text']); // Add an optional, visible, unlocked custom field. $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'pet', 'name' => 'Pet', 'required' => 0, 'visible' => 1, 'locked' => 0, 'datatype' => 'text']); // Add required but invisible custom field. $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'secretid', 'name' => 'Secret ID', 'required' => 1, 'visible' => 0, 'locked' => 0, 'datatype' => 'text']); // Add required but locked custom field. $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'muggleborn', 'name' => 'Muggle-born', 'required' => 1, 'visible' => 1, 'locked' => 1, 'datatype' => 'checkbox']); // Create some student accounts. $hermione = $this->getDataGenerator()->create_user(); $harry = $this->getDataGenerator()->create_user(); $ron = $this->getDataGenerator()->create_user(); $draco = $this->getDataGenerator()->create_user(); // Hermione has all available custom fields filled (of course she has). profile_save_data((object)['id' => $hermione->id, 'profile_field_house' => 'Gryffindor']); profile_save_data((object)['id' => $hermione->id, 'profile_field_pet' => 'Crookshanks']); // Harry has only the optional field filled. profile_save_data((object)['id' => $harry->id, 'profile_field_pet' => 'Hedwig']); // Draco has only the required field filled. profile_save_data((object)['id' => $draco->id, 'profile_field_house' => 'Slytherin']); // Only students with required fields filled should be considered as fully set up in the default (strict) mode. $this->assertFalse(user_not_fully_set_up($hermione)); $this->assertFalse(user_not_fully_set_up($draco)); $this->assertTrue(user_not_fully_set_up($harry)); $this->assertTrue(user_not_fully_set_up($ron)); // In the lax mode, students do not need to have required fields filled. $this->assertFalse(user_not_fully_set_up($hermione, false)); $this->assertFalse(user_not_fully_set_up($draco, false)); $this->assertFalse(user_not_fully_set_up($harry, false)); $this->assertFalse(user_not_fully_set_up($ron, false)); // Lack of required core field is seen as a problem in either mode. unset($hermione->email); $this->assertTrue(user_not_fully_set_up($hermione, true)); $this->assertTrue(user_not_fully_set_up($hermione, false)); // When confirming remote MNet users, we do not have custom fields available. $roamingharry = mnet_strip_user($harry, ['firstname', 'lastname', 'email']); $roaminghermione = mnet_strip_user($hermione, ['firstname', 'lastname', 'email']); $this->assertTrue(user_not_fully_set_up($roamingharry, true)); $this->assertFalse(user_not_fully_set_up($roamingharry, false)); $this->assertTrue(user_not_fully_set_up($roaminghermione, true)); $this->assertTrue(user_not_fully_set_up($roaminghermione, false)); } /** * Test that user generator sets the custom profile fields */ public function test_profile_fields_in_generator() { global $CFG; require_once($CFG->dirroot.'/mnet/lib.php'); $this->resetAfterTest(); // Add a required, visible, unlocked custom field. $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'house', 'name' => 'House', 'required' => 1, 'visible' => 1, 'locked' => 0, 'datatype' => 'text', 'defaultdata' => null]); // Create some student accounts. $hermione = $this->getDataGenerator()->create_user(['profile_field_house' => 'Gryffindor']); $harry = $this->getDataGenerator()->create_user(); // Only students with required fields filled should be considered as fully set up. $this->assertFalse(user_not_fully_set_up($hermione)); $this->assertTrue(user_not_fully_set_up($harry)); // Test that the profile fields were actually set. $profilefields1 = profile_user_record($hermione->id); $this->assertEquals('Gryffindor', $profilefields1->house); $profilefields2 = profile_user_record($harry->id); $this->assertObjectHasAttribute('house', $profilefields2); $this->assertNull($profilefields2->house); } /** * Tests the profile_get_custom_field_data_by_shortname function when working normally. */ public function test_profile_get_custom_field_data_by_shortname_normal() { global $DB; $this->resetAfterTest(); // Create 3 profile fields. $generator = $this->getDataGenerator(); $field1 = $generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'speciality', 'name' => 'Speciality', 'visible' => PROFILE_VISIBLE_ALL]); $field2 = $generator->create_custom_profile_field(['datatype' => 'menu', 'shortname' => 'veggie', 'name' => 'Vegetarian', 'visible' => PROFILE_VISIBLE_PRIVATE]); // Get the first field data and check it is correct. $data = profile_get_custom_field_data_by_shortname('speciality'); $this->assertEquals('Speciality', $data->name); $this->assertEquals(PROFILE_VISIBLE_ALL, $data->visible); $this->assertEquals($field1->id, $data->id); // Get the second field data, checking there is no database query this time. $before = $DB->perf_get_queries(); $data = profile_get_custom_field_data_by_shortname('veggie'); $this->assertEquals($before, $DB->perf_get_queries()); $this->assertEquals('Vegetarian', $data->name); $this->assertEquals(PROFILE_VISIBLE_PRIVATE, $data->visible); $this->assertEquals($field2->id, $data->id); } /** * Tests the profile_get_custom_field_data_by_shortname function with a field that doesn't exist. */ public function test_profile_get_custom_field_data_by_shortname_missing() { $this->assertNull(profile_get_custom_field_data_by_shortname('speciality')); } /** * Data provider for {@see test_profile_get_custom_field_data_by_shortname_case_sensitivity} * * @return array[] */ public function profile_get_custom_field_data_by_shortname_case_sensitivity_provider(): array { return [ 'Matching case, case-sensitive search' => ['hello', 'hello', true, true], 'Matching case, case-insensitive search' => ['hello', 'hello', false, true], 'Non-matching case, case-sensitive search' => ['hello', 'Hello', true, false], 'Non-matching case, case-insensitive search' => ['hello', 'Hello', false, true], 'Non-matching, case-sensitive search' => ['hello', 'hola', true, false], 'Non-matching, case-insensitive search' => ['hello', 'hola', false, false], ]; } /** * Test retrieving custom field by shortname, specifying case-sensitivity when matching * * @param string $shortname * @param string $shortnamesearch * @param bool $casesensitive * @param bool $expectmatch * * @dataProvider profile_get_custom_field_data_by_shortname_case_sensitivity_provider */ public function test_profile_get_custom_field_data_by_shortname_case_sensitivity( string $shortname, string $shortnamesearch, bool $casesensitive, bool $expectmatch ): void { $this->resetAfterTest(); $this->getDataGenerator()->create_custom_profile_field([ 'datatype' => 'text', 'shortname' => $shortname, 'name' => 'My field', ]); $customfield = profile_get_custom_field_data_by_shortname($shortnamesearch, $casesensitive); if ($expectmatch) { $this->assertInstanceOf(\stdClass::class, $customfield); $this->assertEquals('text', $customfield->datatype); $this->assertEquals($shortname, $customfield->shortname); $this->assertEquals('My field', $customfield->name); } else { $this->assertNull($customfield); } } /** * Test profile field loading via profile_get_user_field helper * * @covers ::profile_get_user_field */ public function test_profile_get_user_field(): void { $this->resetAfterTest(); $profilefield = $this->getDataGenerator()->create_custom_profile_field([ 'shortname' => 'fruit', 'name' => 'Fruit', 'datatype' => 'text', ]); $user = $this->getDataGenerator()->create_user(['profile_field_fruit' => 'Apple']); $fieldinstance = profile_get_user_field('text', $profilefield->id, $user->id); $this->assertInstanceOf(\profile_field_text::class, $fieldinstance); $this->assertEquals($profilefield->id, $fieldinstance->fieldid); $this->assertEquals($user->id, $fieldinstance->userid); $this->assertEquals('Apple', $fieldinstance->data); } } tests/behat/filter_trim.feature 0000644 00000003054 15151162244 0012671 0 ustar 00 @core @core_user Feature: Trim entered user filters As a system administrator I need to be able to filter users ignoring whitespace So that I can find users even when entered data has surrounding whitespace. Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher@example.com | | student1 | Student | 1 | student@example.com | And I log in as "admin" And I navigate to "Users > Accounts > Browse list of users" in site administration @javascript Scenario: Filtering username - with case "contains" When I set the field "id_realname_op" to "contains" And I set the field "id_realname" to " Teacher " And I press "Add filter" # We should see the teacher user, with the trimmed string present. Then I should see "User full name contains \"Teacher\"" And I should see "Teacher" in the "users" "table" And I should not see "Student" in the "users" "table" @javascript Scenario: Filtering username - with case "contains" and a whitespace string When I set the field "id_realname_op" to "contains" And I set the field "id_realname" to " " And I press "Add filter" Then I should see "User full name contains \" \"" @javascript Scenario: Filtering username - with case "is equal to" When I set the field "id_realname_op" to "is equal to" And I set the field "id_realname" to " Teacher" And I press "Add filter" Then I should see "User full name is equal to \" Teacher\"" tests/behat/user_private_files.feature 0000644 00000001744 15151162244 0014247 0 ustar 00 @core @core_user @_file_upload @javascript Feature: The private files page allows users to store files privately in moodle. In order to store a private file in moodle As an authenticated user I can upload the file to my private files area from the private files page Background: Given the following "users" exist: | username | firstname | lastname | email | | user1 | User | 1 | user1@example.com | Scenario: Upload a file to the private files area from the private files page Given I log in as "user1" And I follow "Private files" in the user menu And I should see "User 1" in the ".page-context-header" "css_element" And I should see "Private files" in the "region-main" "region" And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager When I press "Save changes" Then I should see "1" elements in "Files" filemanager And I should see "empty.txt" in the ".fp-content .fp-file" "css_element" tests/behat/user_grade_navigation.feature 0000644 00000011037 15151162244 0014710 0 ustar 00 @core @core_user @javascript Feature: The student can navigate to their grades page and user grade report. In order to view my grades and the user grade report As a user I need to log in and browse to my grades. Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | | parent1 | Parent | 1 | parent1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | | Course 2 | C2 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C1 | editingteacher | | student1 | C2 | student | And the following "activities" exist: | activity | course | idnumber | name | intro | grade | | assign | C1 | a1 | Test assignment one | Submit something! | 300 | | assign | C1 | a2 | Test assignment two | Submit something! | 100 | | assign | C1 | a3 | Test assignment three | Submit something! | 150 | | assign | C2 | a4 | Test assignment four | Submit something! | 150 | And I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "150.00" to the user "Student 1" for the grade item "Test assignment one" And I give the grade "67.00" to the user "Student 1" for the grade item "Test assignment two" And I press "Save changes" And I log out Scenario: Navigation to Grades and the user grade report. When I log in as "student1" And I follow "Grades" in the user menu Then the following should exist in the "overview-grade" table: | Course name | Grade | | Course 2 | - | | Course 1 | 217.00 | And I click on "Course 1" "link" in the "region-main" "region" And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 75.00 % | 150.00 | 0–300 | 50.00 % | 37.50 % | | Test assignment two | 25.00 % | 67.00 | 0–100 | 67.00 % | 16.75 % | | Test assignment three | 0.00 %( Empty ) | - | 0–150 | - | 0.00 % | Scenario: Change Grades settings to go to a custom url. Given the following config values are set as admin: | grade_mygrades_report | external | | gradereport_mygradeurl | /badges/mybadges.php | And I log in as "student1" And I follow "Grades" in the user menu Then I should see "My badges from Acceptance test site web site" Scenario: Log in as a parent and view a childs grades. When I log in as "admin" And I am on site homepage And I turn editing mode on And I add the "Mentees" block And I navigate to "Users > Permissions > Define roles" in site administration And I click on "Add a new role" "button" And I click on "Continue" "button" And I set the following fields to these values: | Short name | Parent | | Custom full name | Parent | | contextlevel30 | 1 | | moodle/user:editprofile | 1 | | moodle/user:viewalldetails | 1 | | moodle/user:viewuseractivitiesreport | 1 | | moodle/user:viewdetails | 1 | And I click on "Create this role" "button" And I am on the "student1" "user > profile" page And I click on "Preferences" "link" in the ".profile_tree" "css_element" And I follow "Assign roles relative to this user" And I follow "Parent" And I set the field "Potential users" to "Parent 1 (parent1@example.com)" And I click on "Add" "button" in the "#page-content" "css_element" And I log out And I log in as "parent1" And I am on site homepage And I follow "Student 1" And I follow "Grades overview" Then the following should exist in the "overview-grade" table: | Course name | Grade | | Course 2 | - | | Course 1 | 217.00 | And I click on "Course 1" "link" in the "region-main" "region" And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 75.00 % | 150.00 | 0–300 | 50.00 % | 37.50 % | | Test assignment two | 25.00 % | 67.00 | 0–100 | 67.00 % | 16.75 % | | Test assignment three | 0.00 %( Empty ) | - | 0–150 | - | 0.00 % | tests/behat/filter_participants.feature 0000644 00000136254 15151162244 0014430 0 ustar 00 @core @core_user Feature: Course participants can be filtered In order to filter the list of course participants As a user I need to visit the course participants page and apply the appropriate filters Background: Given the following "courses" exist: | fullname | shortname | groupmode | startdate | | Course 1 | C1 | 1 | ##5 months ago## | | Course 2 | C2 | 0 | ##4 months ago## | | Course 3 | C3 | 0 | ##3 months ago## | And the following "custom profile fields" exist: | datatype | shortname | name | | text | frog | Favourite frog | And the following "users" exist: | username | firstname | lastname | email | idnumber | country | city | maildisplay | profile_field_frog | | student1 | Student | 1 | student1@example.com | SID1 | | SCITY1 | 0 | Kermit | | student2 | Student | 2 | student2@example.com | SID2 | GB | SCITY2 | 1 | Mr Toad | | student3 | Student | 3 | student3@example.com | SID3 | AU | SCITY3 | 0 | | | student4 | Student | 4 | student4@moodle.com | SID4 | AT | SCITY4 | 0 | | | student5 | Trendy | Learnson | trendy@learnson.com | SID5 | AU | SCITY5 | 0 | | | patricia | Patricia | Pea | patricia.pea1@example.org | TID1 | US | TCITY1 | 0 | | And the following "course enrolments" exist: | user | course | role | status | timeend | | student1 | C1 | student | 0 | | | student2 | C1 | student | 1 | | | student3 | C1 | student | 0 | | | student4 | C1 | student | 0 | ##yesterday## | | student1 | C2 | student | 0 | | | student2 | C2 | student | 0 | | | student3 | C2 | student | 0 | | | student5 | C2 | student | 0 | | | student1 | C3 | student | 0 | | | student2 | C3 | student | 0 | | | student3 | C3 | student | 0 | | | patricia | C1 | editingteacher | 0 | | | patricia | C2 | editingteacher | 0 | | | patricia | C3 | editingteacher | 0 | | And the following "last access times" exist: | user | course | lastaccess | | student1 | C1 | ##yesterday## | | student1 | C2 | ##2 weeks ago## | | student2 | C1 | ##4 days ago## | | student3 | C1 | ##2 weeks ago## | | student4 | C1 | ##3 weeks ago## | And the following "groups" exist: | name | course | idnumber | | Group 1 | C1 | G1 | | Group 2 | C1 | G2 | | Group A | C3 | GA | | Group B | C3 | GB | And the following "group members" exist: | user | group | | student2 | G1 | | student2 | G2 | | student3 | G2 | | student1 | GA | | student2 | GA | | student2 | GB | @javascript Scenario: No filters applied Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants Then I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" @javascript Scenario Outline: Filter users for a course with a single value Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants And I set the field "Match" in the "Filter 1" "fieldset" to "<matchtype>" And I set the field "type" in the "Filter 1" "fieldset" to "<filtertype>" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "<filtervalue>" When I click on "Apply filters" "button" Then I should see "<expected1>" in the "participants" "table" And I should see "<expected2>" in the "participants" "table" And I should see "<expected3>" in the "participants" "table" And I should not see "<notexpected1>" in the "participants" "table" And I should not see "<notexpected2>" in the "participants" "table" # Note the 'XX-IGNORE-XX' elements are for when there is less than 2 'not expected' items. Examples: | matchtype | filtertype | filtervalue | expected1 | expected2 | expected3 | notexpected1 | notexpected2 | | Any | Groups | No group | Student 1 | Student 4 | Patricia Pea | Student 2 | Student 3 | | All | Groups | No group | Student 1 | Student 4 | Patricia Pea | Student 2 | Student 3 | | None | Groups | No group | Student 2 | Student 3 | | Student 1 | Patricia Pea | | Any | Role | Student | Student 1 | Student 2 | Student 3 | Patricia Pea | XX-IGNORE-XX | | All | Role | Student | Student 1 | Student 2 | Student 3 | Patricia Pea | XX-IGNORE-XX | | None | Role | Student | Patricia Pea | | | Student 1 | Student 2 | | Any | Status | Active | Student 1 | Student 3 | Patricia Pea | Student 2 | Student 4 | | All | Status | Active | Student 1 | Student 3 | Patricia Pea | Student 2 | Student 4 | | None | Status | Active | Student 2 | Student 4 | | Student 1 | Student 3 | | Any | Inactive for more than | 1 week | Student 3 | Student 4 | | Student 1 | Student 2 | | All | Inactive for more than | 1 week | Student 3 | Student 4 | | Student 1 | Student 2 | | None | Inactive for more than | 1 week | Student 1 | Student 2 | Patricia Pea | Student 3 | XX-IGNORE-XX | @javascript Scenario Outline: Filter users for a course with multiple values for a single filter Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants And I set the field "Match" in the "Filter 1" "fieldset" to "<matchtype>" And I set the field "type" in the "Filter 1" "fieldset" to "<filtertype>" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "<filtervalue1>,<filtervalue2>" When I click on "Apply filters" "button" Then I should see "<expected1>" in the "participants" "table" And I should see "<expected2>" in the "participants" "table" And I should see "<expected3>" in the "participants" "table" And I should not see "<notexpected1>" in the "participants" "table" And I should not see "<notexpected2>" in the "participants" "table" # Note the 'XX-IGNORE-XX' elements are for when there is less than 2 'not expected' items. Examples: | matchtype | filtertype | filtervalue1 | filtervalue2 | expected1 | expected2 | expected3 | notexpected1 | notexpected2 | | Any | Groups | Group 1 | Group 2 | Student 2 | Student 3 | | Student 1 | XX-IGNORE-XX | | All | Groups | Group 1 | Group 2 | Student 2 | | | Student 1 | Student 3 | | None | Groups | Group 1 | Group 2 | Student 1 | Patricia Pea | | Student 2 | Student 3 | @javascript Scenario Outline: Filter users which are group members in several courses Given I am on the "C3" "Course" page logged in as "patricia" And I navigate to course participants And I set the field "type" in the "Filter 1" "fieldset" to "<filtertype>" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "<filtervalue>" When I click on "Apply filters" "button" Then I should see "<expected1>" in the "participants" "table" And I should see "<expected2>" in the "participants" "table" And I should not see "<notexpected1>" in the "participants" "table" And I should not see "<notexpected2>" in the "participants" "table" # Note the 'XX-IGNORE-XX' elements are for when there is less than 2 'not expected' items. Examples: | filtertype | filtervalue | expected1 | expected2 | notexpected1 | notexpected2 | | Groups | No group | Student 3 | | Student 1 | Student 2 | | Groups | Group A | Student 1 | Student 2 | Student 3 | XX-IGNORE-XX | | Groups | Group B | Student 2 | | Student 1 | Student 3 | @javascript Scenario: In separate groups mode, a student in a single group can only view and filter by users in their own group Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants # Unsuspend student 2 for to improve coverage of this test. And I click on "Edit enrolment" "icon" in the "Student 2" "table_row" And I set the field "Status" to "Active" And I click on "Save changes" "button" And I log out # Default view should have groups filter pre-set. # Match: # Groups Any ["Group 2"]. When I log in as "student3" And I am on "Course 1" course homepage And I navigate to course participants Then I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Group 2" in the "Filter 1" "fieldset" But I should not see "Student 1" in the "participants" "table" And I should not see "Group 1" in the "Filter 1" "fieldset" # Testing result of removing groups filter row. # Match any available user. When I click on "Remove filter row" "button" in the "Filter 1" "fieldset" Then I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" # Testing result of applying groups filter manually. # Match: # Group Any ["Group 2"]. # Match Groups Any ["Group 2"] Given I set the field "Match" in the "Filter 1" "fieldset" to "Any" And I set the field "type" in the "Filter 1" "fieldset" to "Groups" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group 2" And I open the autocomplete suggestions list in the "Filter 1" "fieldset" And I should not see "Group 1" in the ".form-autocomplete-suggestions" "css_element" And I click on "Apply filters" "button" Then I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" # Testing result of removing groups filter by clearing all filters. # Match any available user. When I click on "Clear filters" "button" Then I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" @javascript Scenario: In separate groups mode, a student in multiple groups can only view and filter by users in their own groups Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants # Unsuspend student 2 for to improve coverage of this test. And I click on "Edit enrolment" "icon" in the "Student 2" "table_row" And I set the field "Status" to "Active" And I click on "Save changes" "button" And I log out When I log in as "student2" And I am on "Course 1" course homepage And I navigate to course participants # Default view should have groups filter pre-set. # Match: # Groups Any ["Group 1", "Group 2"]. Then I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should not see "Student 1" in the "participants" "table" And I should see "Group 1" in the "Filter 1" "fieldset" And I should see "Group 2" in the "Filter 1" "fieldset" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should not see "Student 1" in the "participants" "table" # Testing result of removing groups filter row. # Match any available user. When I click on "Remove filter row" "button" in the "Filter 1" "fieldset" Then I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" # Testing result of applying groups filter manually. # Match: # Groups Any ["Group 1"]. # Match Groups Any ["Group 1"] And I set the field "Match" in the "Filter 1" "fieldset" to "Any" And I set the field "type" in the "Filter 1" "fieldset" to "Groups" And I open the autocomplete suggestions list in the "Filter 1" "fieldset" And I should see "Group 2" in the ".form-autocomplete-suggestions" "css_element" And I press the escape key And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group 1" And I click on "Apply filters" "button" And I should see "Student 2" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" # Testing result of removing groups filter by clearing all filters. # Match any available user. When I click on "Clear filters" "button" Then I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" @javascript Scenario: Filter users who have no role in a course Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants # Remove the user role. And I click on "Student 1's role assignments" "link" And I click on ".form-autocomplete-selection [aria-selected=true]" "css_element" And I press the escape key And I click on "Save changes" "link" # Match: # Roles All ["No roles"]. # Match Roles All ["No roles"]. When I set the field "type" in the "Filter 1" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "No roles" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" But I should not see "Student 2" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" @javascript Scenario: Multiple filters applied (All filterset match type) Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants # Match Any: # Roles All ["Student"] and # Status Any ["Active"]. # Match Roles All ["Student"]. When I set the field "Match" in the "Filter 1" "fieldset" to "All" And I set the field "type" in the "Filter 1" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student" # Match Status All ["Active"]. And I click on "Add condition" "button" # Set filterset to match all. And I set the field "Match" to "All" And I set the field "Match" in the "Filter 2" "fieldset" to "Any" And I set the field "type" in the "Filter 2" "fieldset" to "Status" And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Active" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 2" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" # Match Any: # Roles All ["Student"]; and # Status Any ["Active"]; and # Enrolment method Any ["Manual"]; and # Groups Any ["Group 2"]. # Match enrolment method Any ["Manual"] When I click on "Add condition" "button" And I set the field "Match" in the "Filter 3" "fieldset" to "Any" And I set the field "type" in the "Filter 3" "fieldset" to "Enrolment methods" And I set the field "Type or select..." in the "Filter 3" "fieldset" to "Manual enrolments" # Match Groups Any ["Group 2"] And I click on "Add condition" "button" And I set the field "Match" in the "Filter 4" "fieldset" to "All" And I set the field "type" in the "Filter 4" "fieldset" to "Groups" And I set the field "Type or select..." in the "Filter 4" "fieldset" to "Group 2" And I click on "Apply filters" "button" Then I should see "Student 3" in the "participants" "table" But I should not see "Patricia Pea" in the "participants" "table" And I should not see "Student 1" in the "participants" "table" And I should not see "Student 2" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" # Change the active status filter to inactive. # Match Any: # Roles All ["Student"]; and # Status Any ["Inactive"]; and # Enrolment method Any ["Manual"]; and # Groups Any ["Group 2"]. # Match Status All ["Inactive"]. And I click on "Active" "autocomplete_selection" And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Inactive" And I click on "Apply filters" "button" Then I should see "Student 2" in the "participants" "table" But I should not see "Student 4" in the "participants" "table" And I should not see "Student 1" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" # Set both statuses (match any). # Match Any: # Roles All ["Student"]; and # Status Any ["Active", "Inactive"]; and # Enrolment method Any ["Manual"]; and # Groups Any ["Group 2"]. # Match Status Any ["Active", "Inactive"]. And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Active,Inactive" And I click on "Apply filters" "button" Then I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" # Set both statuses (match all). # Match Any: # Roles All ["Student"]; and # Status Any ["Active", "Inactive"]; and # Enrolment method Any ["Manual"]; and # Groups Any ["Group 2"]. # Match Status All ["Active", "Inactive"]. When I set the field "Match" in the "Filter 2" "fieldset" to "All" And I click on "Apply filters" "button" Then I should see "Nothing to display" @javascript Scenario: Multiple filters applied (Any filterset match type) Given I log in as "patricia" And I am on "Course 1" course homepage And I navigate to course participants # Match Any: # Roles All ["Teacher"] and # Status Any ["Active"]. # Match Roles all Roles ["Teacher"]. When I set the field "Match" in the "Filter 1" "fieldset" to "All" And I set the field "type" in the "Filter 1" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Teacher" # Match Status Any ["Active"]. And I click on "Add condition" "button" And I set the field "Match" in the "Filter 2" "fieldset" to "Any" And I set the field "type" in the "Filter 2" "fieldset" to "Status" And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Active" # Set filterset to match any. And I set the field "Match" to "Any" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 2" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" # Match Any: # Roles All ["Teacher"] and # Status None ["Active"]. # Match Status Not ["Active"] When I set the field "Match" in the "Filter 2" "fieldset" to "None" And I click on "Apply filters" "button" Then I should see "Student 2" in the "participants" "table" And I should see "Student 4" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" # Add a keyword filter. # Match Any: # Roles All ["Teacher"]; and # Status None ["Active"]; and # Keyword Any ["patricia"]. # Match Keyword Any ["patricia"]. When I click on "Add condition" "button" And I set the field "Match" in the "Filter 3" "fieldset" to "Any" And I set the field "type" in the "Filter 3" "fieldset" to "Keyword" And I set the field "Type..." in the "Filter 3" "fieldset" to "patricia" And I click on "Apply filters" "button" Then I should see "Student 2" in the "participants" "table" And I should see "Student 4" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" @javascript Scenario: Multiple filters applied (None filterset match type) Given I log in as "patricia" And I am on "Course 1" course homepage And I navigate to course participants # Match None: # Roles All ["Teacher"] and # Status Any ["Active"]. # Set the Roles to "All" ["Teacher"]. When I set the field "Match" in the "Filter 1" "fieldset" to "All" And I set the field "type" in the "Filter 1" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Teacher" # Set the Status to "Any" ["Active"]. And I click on "Add condition" "button" And I set the field "Match" in the "Filter 2" "fieldset" to "Any" And I set the field "type" in the "Filter 2" "fieldset" to "Status" And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Active" # Set filterset to match None. And I set the field "Match" to "None" And I click on "Apply filters" "button" Then I should see "Student 2" in the "participants" "table" And I should see "Student 4" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" # Match None: # Roles All ["Teacher"] and # Status None ["Active"] # Set the Status to "None ["Active"] When I set the field "Match" in the "Filter 2" "fieldset" to "None" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 2" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" # Match None: # Roles All ["Teacher"] and # Status None ["Active"] and # Keyword Any ["3@"] # Set the Keyword to "Any" ["3@"] When I click on "Add condition" "button" Then I set the field "Match" in the "Filter 3" "fieldset" to "Any" And I set the field "type" in the "Filter 3" "fieldset" to "Keyword" And I set the field "Type..." in the "Filter 3" "fieldset" to "3@" When I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should not see "Student 2" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" # Match None: # Roles All ["Teacher"] and # Status None ["Active"] and # Keyword None ["3@"]. # Set the Keyword to "None" ["3@"] When I set the field "Match" in the "Filter 3" "fieldset" to "None" And I click on "Apply filters" "button" Then I should see "Student 3" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 2" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" @javascript Scenario: Filter match by one or more keywords and modified match types Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants # Match: # Keyword Any ["1@example"]. # Set the Keyword to "Any" ["1@example"] When I set the field "Match" in the "Filter 1" "fieldset" to "Any" And I set the field "type" in the "Filter 1" "fieldset" to "Keyword" And I set the field "Type..." in the "Filter 1" "fieldset" to "1@example" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" But I should not see "Student 2" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" # Match: # Keyword All ["1@example"]. When I set the field "Match" in the "Filter 1" "fieldset" to "All" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" But I should not see "Student 2" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" # Match: # Keyword None ["1@example"]. When I set the field "Match" in the "Filter 1" "fieldset" to "None" And I click on "Apply filters" "button" Then I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Student 4" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" # Set two keyword values. # Match: # Keyword None ["1@example", "moodle"]. When I set the field "Type..." to "1@example, moodle" And I click on "Apply filters" "button" Then I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" # Set two keyword values. # Match: # Keyword Any ["1@example", "moodle"]. When I set the field "Match" in the "Filter 1" "fieldset" to "Any" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" And I should see "Student 4" in the "participants" "table" But I should not see "Student 2" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" # Match: # Keyword All ["1@example", "moodle"]. When I set the field "Match" in the "Filter 1" "fieldset" to "All" And I click on "Apply filters" "button" Then I should see "Nothing to display" @javascript Scenario: Reorder users without losing filter Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants When I set the field "type" in the "Filter 1" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student" And I click on "Apply filters" "button" And I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Student 4" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" When I click on "Last name" "link" Then I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Student 4" in the "participants" "table" But I should not see "Patricia Pea" in the "participants" "table" @javascript Scenario: Only possible to add filter rows for the number of filters available Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants When I set the field "type" in the "Filter 1" "fieldset" to "Keyword" And I click on "Add condition" "button" And I set the field "type" in the "Filter 2" "fieldset" to "Status" And I click on "Add condition" "button" And I set the field "type" in the "Filter 3" "fieldset" to "Roles" And I click on "Add condition" "button" And I set the field "type" in the "Filter 4" "fieldset" to "Enrolment methods" And I click on "Add condition" "button" And I set the field "type" in the "Filter 5" "fieldset" to "Groups" And I click on "Add condition" "button" And I set the field "type" in the "Filter 6" "fieldset" to "Inactive for more than" Then the "Add condition" "button" should be disabled @javascript Scenario: Rendering filter options for teachers in a course that don't support groups Given I am on the "C2" "Course" page logged in as "patricia" When I navigate to course participants Then I should see "Roles" in the "type" "field" And I should see "Enrolment methods" in the "type" "field" But I should not see "Groups" in the "type" "field" @javascript Scenario: Rendering filter options for students who have limited privileges Given I am on the "C2" "Course" page logged in as "student1" When I navigate to course participants Then I should see "Roles" in the "type" "field" But I should not see "Status" in the "type" "field" And I should not see "Enrolment methods" in the "type" "field" @javascript Scenario: Filter by user identity fields Given the following config values are set as admin: | showuseridentity | idnumber,email,city,country | And I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants # Search by email (only) - should only see visible email + own. # Match: # Keyword Any ["student1@example.com"]. # Set the Keyword to "Any" ["student1@example.com"] When I set the field "type" in the "Filter 1" "fieldset" to "Keyword" And I set the field "Type..." in the "Filter 1" "fieldset" to "student1@example.com" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" But I should not see "Student 2" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" # Search by idnumber (only). # Match: # Keyword Any ["SID"]. # Set the Keyword to "Any" ["SID"] And I click on "student1@example.com" "autocomplete_selection" And I set the field "Type..." in the "Filter 1" "fieldset" to "SID" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Student 4" in the "participants" "table" But I should not see "Patricia Pea" in the "participants" "table" # Search by city (only). # Match: # Keyword Any ["SCITY"]. # Set the Keyword to "Any" ["SCITY"] And I click on "SID" "autocomplete_selection" And I set the field "Type..." in the "Filter 1" "fieldset" to "SCITY" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Student 4" in the "participants" "table" But I should not see "Patricia Pea" in the "participants" "table" # Search by country text (only) - should not match. # Match: # Keyword Any ["GB"]. # Set the Keyword to "Any" ["GB"] And I click on "SCITY" "autocomplete_selection" And I set the field "Type..." in the "Filter 1" "fieldset" to "GB" And I click on "Apply filters" "button" Then I should see "Nothing to display" # Check no match. # Match: # Keyword Any ["NOTHING"]. # Set the Keyword to "Any" ["NOTHING"] And I click on "GB" "autocomplete_selection" And I set the field "Type..." in the "Filter 1" "fieldset" to "NOTHING" And I click on "Apply filters" "button" Then I should see "Nothing to display" @javascript @skip_chrome_zerosize Scenario: Filter by user identity fields when cannot see the field data Given I log in as "admin" And I set the following system permissions of "Teacher" role: | moodle/site:viewuseridentity | Prevent | And the following config values are set as admin: | showuseridentity | idnumber,email,city,country | And I log out And I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants # Match: # Keyword Any ["@example.com"]. # Search by email (only) - should only see visible email + own. # Set the Keyword to "Any" ["@example.com"] When I set the field "type" in the "Filter 1" "fieldset" to "Keyword" And I set the field "Type..." in the "Filter 1" "fieldset" to "@example." And I click on "Apply filters" "button" Then I should see "Student 2" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" # Search for other fields - should only see own results. # Match: # Keyword Any ["SID"]. # Set the Keyword to "Any" ["SID"] And I click on "@example." "autocomplete_selection" And I set the field "Type..." in the "Filter 1" "fieldset" to "SID" And I click on "Apply filters" "button" Then I should see "Nothing to display" # Match: # Keyword Any ["TID"]. # Set the Keyword to "Any" ["TID"] And I click on "SID" "autocomplete_selection" And I set the field "Type..." in the "Filter 1" "fieldset" to "TID" And I click on "Apply filters" "button" Then I should see "Patricia Pea" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" # Match: # Keyword Any ["CITY"]. # Set the Keyword to "Any" ["CITY"] And I click on "TID" "autocomplete_selection" And I set the field "Type..." in the "Filter 1" "fieldset" to "CITY" And I click on "Apply filters" "button" Then I should see "Patricia Pea" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" # No data matches regardless of capabilities. # Match: # Keyword Any ["NOTHING"]. # Set the Keyword to "Any" ["NOTHING"] And I click on "CITY" "autocomplete_selection" And I set the field "Type..." in the "Filter 1" "fieldset" to "NOTHING" And I click on "Apply filters" "button" Then I should see "Nothing to display" @javascript Scenario: Individual filters can be removed, which will automatically refresh the participants list # Match All: # Roles All ["Student"]; and # Keyword Any ["@example.com"]. # Set the Roles to "All" ["Student"]. Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants And I set the field "Match" in the "Filter 1" "fieldset" to "All" And I set the field "type" in the "Filter 1" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student" # Set the Keyword to "Any" ["@example.com"] And I click on "Add condition" "button" And I set the field "Match" in the "Filter 2" "fieldset" to "Any" And I set the field "type" in the "Filter 2" "fieldset" to "Keyword" And I set the field "Type..." in the "Filter 2" "fieldset" to "@example" # Set filterset to match all. And I set the field "Match" to "All" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 4" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" # Match: # Keyword Any ["@example.com"]. When I click on "Remove filter row" "button" in the "Filter 1" "fieldset" Then I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" But I should not see "Student 4" in the "participants" "table" @javascript Scenario: All filters can be cleared at once # Match All: # Roles All ["Student"]; and # Keyword Any ["@example.com"]. # Set the Roles to "All" ["Student"]. Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants When I set the field "Match" in the "Filter 1" "fieldset" to "All" And I set the field "type" in the "Filter 1" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student" # Set the Keyword to "All" ["@example.com"]. And I click on "Add condition" "button" And I set the field "Match" in the "Filter 2" "fieldset" to "Any" And I set the field "type" in the "Filter 2" "fieldset" to "Keyword" And I set the field "Type..." in the "Filter 2" "fieldset" to "@example" # Set filterset to match all. And I set the field "Match" to "All" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 4" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" # Match Any. When I click on "Clear filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Student 4" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" @javascript Scenario: Filterset match type is reset when reducing to a single filter # Match None: # Keyword Any ["@example.com"]; and # Roles All ["Teacher"]. Given I am on the "C1" "Course" page logged in as "patricia" And I navigate to course participants # Set the Keyword to "Any" ["@example.com"] When I set the field "Match" in the "Filter 1" "fieldset" to "Any" And I set the field "type" in the "Filter 1" "fieldset" to "Keyword" And I set the field "Type..." to "@example.com" # Set the Roles to "All" ["Student"]. And I click on "Add condition" "button" And I set the field "Match" in the "Filter 2" "fieldset" to "All" And I set the field "type" in the "Filter 2" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Student" # Match none of student role and @example.com keyword. And I set the field "Match" to "None" And I click on "Apply filters" "button" Then I should see "Patricia Pea" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 2" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" # Match: # Keyword Any ["@example.com"]. # When removing the pen-ultimate filter, the filterset match type is removed too. When I click on "Remove filter row" "button" in the "Filter 2" "fieldset" Then I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" But I should not see "Student 4" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" # Match Any: # Keyword Any ["@example.com"]; and # Role All ["Student"]. # The default filterset match (Any) should apply. # Set the Roles to "All" ["Student"]. When I click on "Add condition" "button" And I set the field "Match" in the "Filter 2" "fieldset" to "All" And I set the field "type" in the "Filter 2" "fieldset" to "Role" And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Student" And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should not see "Student 4" in the "participants" "table" But I should not see "Patricia Pea" in the "participants" "table" @javascript Scenario: Filter users by first initial # Match: # No filters; and # First initial "T". Given I am on the "C2" "Course" page logged in as "patricia" And I navigate to course participants And I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Trendy Learnson" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" When I click on "T" "link" in the ".firstinitial" "css_element" Then I should see "Trendy Learnson" in the "participants" "table" But I should not see "Patricia Pea" in the "participants" "table" And I should not see "Student 1" in the "participants" "table" And I should not see "Student 2" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" @javascript Scenario: Filter users by last initial # Match: # No filters; and # Last initial "L". Given I am on the "C2" "Course" page logged in as "patricia" And I navigate to course participants And I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Trendy Learnson" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" When I click on "L" "link" in the ".lastinitial" "css_element" Then I should see "Trendy Learnson" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 2" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" @javascript Scenario: Filter users by first and last initials # Match: # No filters; and # First initial "T"; and # Last initial "L". Given I am on the "C2" "Course" page logged in as "patricia" And I navigate to course participants And I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Trendy Learnson" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" When I click on "T" "link" in the ".firstinitial" "css_element" And I click on "L" "link" in the ".lastinitial" "css_element" Then I should see "Trendy Learnson" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 2" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" @javascript Scenario: Initials filtering is always applied in addition to any other filtering # Match: # Roles All ["Teacher"]; and # First initial "T". Given I am on the "C2" "Course" page logged in as "patricia" And I navigate to course participants And I should see "Student 1" in the "participants" "table" And I should see "Student 2" in the "participants" "table" And I should see "Student 3" in the "participants" "table" And I should see "Trendy Learnson" in the "participants" "table" And I should see "Patricia Pea" in the "participants" "table" # Set the Role to "Any" ["Student"]. When I set the field "Match" in the "Filter 1" "fieldset" to "Any" And I set the field "type" in the "Filter 1" "fieldset" to "Role" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student" And I click on "Apply filters" "button" # Last initial "T". And I click on "T" "link" in the ".firstinitial" "css_element" Then I should see "Trendy Learnson" in the "participants" "table" But I should not see "Student 1" in the "participants" "table" And I should not see "Student 2" in the "participants" "table" And I should not see "Student 3" in the "participants" "table" And I should not see "Patricia Pea" in the "participants" "table" @javascript Scenario: Filtering works correctly with custom profile fields Given the following config values are set as admin: | showuseridentity | email,profile_field_frog | And I am on the "C2" "Course" page logged in as "patricia" And I navigate to course participants And I set the field "type" in the "Filter 1" "fieldset" to "Keyword" And I set the field "Type..." to "Kermit" And I press enter And I click on "Apply filters" "button" Then I should see "Student 1" in the "participants" "table" And I should not see "Student 2" in the "participants" "table" tests/behat/behat_user.php 0000644 00000013416 15151162244 0011631 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * User steps definition. * * @package core_user * @category test * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); use Behat\Mink\Exception\ExpectationException as ExpectationException; /** * Steps definitions for users. * * @package core_user * @category test * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_user extends behat_base { /** * Choose from the bulk action menu. * * @Given /^I choose "(?P<nodetext_string>(?:[^"]|\\")*)" from the participants page bulk action menu$/ * @param string $nodetext The menu item to select. */ public function i_choose_from_the_participants_page_bulk_action_menu($nodetext) { $this->execute("behat_forms::i_set_the_field_to", [ "With selected users...", $this->escape($nodetext) ]); } /** * The input field should have autocomplete set to this value. * * @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" should have purpose "(?P<purpose_string>(?:[^"]|\\")*)"$/ * @param string $field The field to select. * @param string $purpose The expected purpose. */ public function the_field_should_have_purpose($field, $purpose) { $fld = behat_field_manager::get_form_field_from_label($field, $this); $value = $fld->get_attribute('autocomplete'); if ($value != $purpose) { $reason = 'The "' . $field . '" field does not have purpose "' . $purpose . '"'; throw new ExpectationException($reason, $this->getSession()); } } /** * The input field should not have autocomplete set to this value. * * @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" should not have purpose "(?P<purpose_string>(?:[^"]|\\")*)"$/ * @param string $field The field to select. * @param string $purpose The expected purpose we do not want. */ public function the_field_should_not_have_purpose($field, $purpose) { $fld = behat_field_manager::get_form_field_from_label($field, $this); $value = $fld->get_attribute('autocomplete'); if ($value == $purpose) { throw new ExpectationException('The "' . $field . '" field does have purpose "' . $purpose . '"', $this->getSession()); } } /** * Convert page names to URLs for steps like 'When I am on the "[page name]" page'. * * Recognised page names are: * | Page name | Description | * | Contact Site Support | The Contact Site Support page (user/contactsitesupport.php) | * * @param string $page name of the page, with the component name removed e.g. 'Admin notification'. * @return moodle_url the corresponding URL. * @throws Exception with a meaningful error message if the specified page cannot be found. */ protected function resolve_page_url(string $page): moodle_url { switch (strtolower($page)) { case 'contact site support': return new moodle_url('/user/contactsitesupport.php'); default: throw new Exception("Unrecognised core_user page type '{$page}'."); } } /** * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. * * Recognised page names are: * | Page Type | Identifier meaning | Description | * | editing | username or email | User editing page (/user/editadvanced.php) | * | profile | username or email | User profile page (/user/profile.php) | * * @param string $type identifies which type of page this is, e.g. 'Editing'. * @param string $identifier identifies the user, e.g. 'student1'. * @return moodle_url the corresponding URL. * @throws Exception with a meaningful error message if the specified page cannot be found. */ protected function resolve_page_instance_url(string $type, string $identifier): moodle_url { switch (strtolower($type)) { case 'editing': $userid = $this->get_user_id_by_identifier($identifier); if (!$userid) { throw new Exception('The specified user with username or email "' . $identifier . '" does not exist'); } return new moodle_url('/user/editadvanced.php', ['id' => $userid]); case 'profile': $userid = $this->get_user_id_by_identifier($identifier); if (!$userid) { throw new Exception('The specified user with username or email "' . $identifier . '" does not exist'); } return new moodle_url('/user/profile.php', ['id' => $userid]); default: throw new Exception("Unrecognised page type '{$type}'."); } } } tests/behat/table_sorting.feature 0000644 00000006011 15151162244 0013201 0 ustar 00 @core @core_user Feature: Tables can be sorted by additional names In order to sort fields by additional names As a user I need to browse to a page with users in a table. Background: Given the following "users" exist: | username | firstname | lastname | middlename | alternatename | email | idnumber | | student1 | Annie | Edison | Faith | Anne | student1@example.com | s1 | | student2 | George | Bradley | David | Gman | student2@example.com | s2 | | student3 | Travis | Sutcliff | Peter | Mr T | student3@example.com | s3 | And I log in as "admin" And I navigate to "Users > Permissions > User policies" in site administration And the following config values are set as admin: | fullnamedisplay | firstname middlename lastname | | alternativefullnameformat | firstname middlename alternatename lastname | @javascript Scenario: All user names are show and sortable in the administration user list. Given I navigate to "Users > Accounts > Browse list of users" in site administration Then the following should exist in the "users" table: | First name / Middle name / Alternate name / Last name | Email address | | Admin User | moodle@example.com | | Annie Faith Anne Edison | student1@example.com | | George David Gman Bradley | student2@example.com | | Travis Peter Mr T Sutcliff | student3@example.com | And "Annie Faith Anne Edison" "table_row" should appear before "George David Gman Bradley" "table_row" And "George David Gman Bradley" "table_row" should appear before "Travis Peter Mr T Sutcliff" "table_row" And I follow "Middle name" And "George David Gman Bradley" "table_row" should appear before "Annie Faith Anne Edison" "table_row" And "Annie Faith Anne Edison" "table_row" should appear before "Travis Peter Mr T Sutcliff" "table_row" And I follow "Middle name" And "George David Gman Bradley" "table_row" should appear after "Annie Faith Anne Edison" "table_row" And "Annie Faith Anne Edison" "table_row" should appear after "Travis Peter Mr T Sutcliff" "table_row" And I follow "Alternate name" And "Annie Faith Anne Edison" "table_row" should appear before "George David Gman Bradley" "table_row" And "George David Gman Bradley" "table_row" should appear before "Travis Peter Mr T Sutcliff" "table_row" And I follow "Alternate name" And "Annie Faith Anne Edison" "table_row" should appear after "George David Gman Bradley" "table_row" And "George David Gman Bradley" "table_row" should appear after "Travis Peter Mr T Sutcliff" "table_row" And I follow "Last name" And "George David Gman Bradley" "table_row" should appear before "Annie Faith Anne Edison" "table_row" And "Annie Faith Anne Edison" "table_row" should appear before "Travis Peter Mr T Sutcliff" "table_row" And I follow "Last name" And "George David Gman Bradley" "table_row" should appear after "Annie Faith Anne Edison" "table_row" And "Annie Faith Anne Edison" "table_row" should appear after "Travis Peter Mr T Sutcliff" "table_row" tests/behat/participants_in_group_modes.feature 0000644 00000023631 15151162244 0016146 0 ustar 00 @core @core_course @javascript Feature: Viewing participants page in different group modes In order to view my students As a user I need to be able to browse participants who are grouped Background: Given the following "courses" exist: | fullname | shortname | summary | groupmode | category | | C1 nogroups | C1 | | 0 | 0 | | C2 visgroups | C2 | | 2 | 0 | | C3 sepgroups | C3 | | 1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | | teacher1 | C3 | editingteacher | | teacher2 | C1 | teacher | | teacher2 | C2 | teacher | | teacher2 | C3 | teacher | | student1 | C1 | student | | student1 | C2 | student | | student1 | C3 | student | | student2 | C1 | student | | student2 | C2 | student | | student2 | C3 | student | | student3 | C1 | student | | student3 | C2 | student | | student3 | C3 | student | And the following "groups" exist: | name | course | idnumber | | G1 | C2 | C2G1 | | G2 | C2 | C2G2 | | G1 | C3 | C3G1 | | G2 | C3 | C3G2 | And the following "group members" exist: | user | group | | student1 | C2G1 | | student1 | C3G1 | | student2 | C2G2 | | student2 | C3G2 | | teacher2 | C2G1 | | teacher2 | C3G1 | Scenario: Viewing participants page as an editing teacher in a course without group mode When I log in as "teacher1" And I am on "C1 nogroups" course homepage And I navigate to course participants Then "Student 1" row "Groups" column of "participants" table should contain "No groups" And "Student 2" row "Groups" column of "participants" table should contain "No groups" And "Student 3" row "Groups" column of "participants" table should contain "No groups" And "Teacher 1" row "Groups" column of "participants" table should contain "No groups" And "Teacher 2" row "Groups" column of "participants" table should contain "No groups" Scenario: Viewing participants page as an editing teacher in a course in visible groups mode When I log in as "teacher1" And I am on "C2 visgroups" course homepage And I navigate to course participants Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Student 2" row "Groups" column of "participants" table should contain "G2" And "Student 3" row "Groups" column of "participants" table should contain "No groups" And "Teacher 1" row "Groups" column of "participants" table should contain "No groups" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" Scenario: Viewing participants page as an editing teacher in a course in separate groups mode When I log in as "teacher1" And I am on "C3 sepgroups" course homepage And I navigate to course participants Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Student 2" row "Groups" column of "participants" table should contain "G2" And "Student 3" row "Groups" column of "participants" table should contain "No groups" And "Teacher 1" row "Groups" column of "participants" table should contain "No groups" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" Scenario: Viewing participants page as a non-editing teacher in a course without group mode When I log in as "teacher2" And I am on "C1 nogroups" course homepage And I navigate to course participants Then "Student 1" row "Groups" column of "participants" table should contain "No groups" And "Student 2" row "Groups" column of "participants" table should contain "No groups" And "Student 3" row "Groups" column of "participants" table should contain "No groups" And "Teacher 1" row "Groups" column of "participants" table should contain "No groups" And "Teacher 2" row "Groups" column of "participants" table should contain "No groups" Scenario: Viewing participants page as a non-editing teacher in a course in visible groups mode When I log in as "teacher2" And I am on "C2 visgroups" course homepage And I navigate to course participants Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" And I should not see "Teacher 1" And I should not see "Student 2" And I should not see "Student 3" When I click on "Clear filters" "button" Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Student 2" row "Groups" column of "participants" table should contain "G2" And "Student 3" row "Groups" column of "participants" table should contain "No groups" And "Teacher 1" row "Groups" column of "participants" table should contain "No groups" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" Scenario: Viewing participants page as a non-editing teacher in a course in separate groups mode When I log in as "teacher2" And I am on "C3 sepgroups" course homepage And I navigate to course participants Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" And I should not see "Teacher 1" And I should not see "Student 2" And I should not see "Student 3" When I click on "Clear filters" "button" Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" And I should not see "Teacher 1" And I should not see "Student 2" And I should not see "Student 3" Scenario: Viewing participants page as a student in a course without group mode When I log in as "student1" And I am on "C1 nogroups" course homepage And I navigate to course participants Then "Student 1" row "Groups" column of "participants" table should contain "No groups" And "Student 2" row "Groups" column of "participants" table should contain "No groups" And "Student 3" row "Groups" column of "participants" table should contain "No groups" Scenario: Viewing participants page as a student in a group in a course in visible groups mode When I log in as "student1" And I am on "C2 visgroups" course homepage And I navigate to course participants Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" And I should not see "Student 2" And I should not see "Student 3" And I should not see "Teacher 1" When I click on "Clear filters" "button" Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Student 2" row "Groups" column of "participants" table should contain "G2" And "Student 3" row "Groups" column of "participants" table should contain "No groups" And "Teacher 1" row "Groups" column of "participants" table should contain "No groups" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" Scenario: Viewing participants page as a student in a group in a course in separate groups mode When I log in as "student1" And I am on "C3 sepgroups" course homepage And I navigate to course participants Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" And I should not see "Student 2" And I should not see "Student 3" And I should not see "Teacher 1" When I click on "Clear filters" "button" Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" And I should not see "Student 2" And I should not see "Student 3" And I should not see "Teacher 1" Scenario: Viewing participants page as a student not in a group in a course in visible groups mode When I log in as "student3" And I am on "C2 visgroups" course homepage And I navigate to course participants Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" And I should not see "Student 2" And I should not see "Student 3" in the "participants" "table" And I should not see "Teacher 1" When I click on "Clear filters" "button" Then "Student 1" row "Groups" column of "participants" table should contain "G1" And "Student 2" row "Groups" column of "participants" table should contain "G2" And "Student 3" row "Groups" column of "participants" table should contain "No groups" And "Teacher 1" row "Groups" column of "participants" table should contain "No groups" And "Teacher 2" row "Groups" column of "participants" table should contain "G1" Scenario: Viewing participants page as a student not in a group in a course in separate groups mode When I log in as "student3" And I am on "C3 sepgroups" course homepage And I navigate to course participants Then I should see "Sorry, but you need to be part of a group to see this page." tests/behat/filter_timecreated.feature 0000644 00000004046 15151162244 0014206 0 ustar 00 @core @core_user Feature: Filter users by time created In order to find users created relative to a date As an admin I need to be able to filter users by their time created date Background: Given the following "users" exist: | username | firstname | lastname | email | | user01 | User | One | user01@example.com | @javascript Scenario Outline: Matching user filter by time created Given I log in as "admin" And I navigate to "Users > Accounts > Browse list of users" in site administration And I follow "Show more..." # Set the filter fields, excluding admin. When I set the following fields to these values: | <enablefield>[enabled] | 1 | | <enablefield>[year] | <year> | | Username field limiter | doesn't contain | | Username value | admin | And I press "Add filter" Then I should see "User One" in the "users" "table" And I should see "1 / 2 Users" Examples: | enablefield | year | # Time created, is after. | timecreated_sdt | ## -1 year ## %Y ## | # Time created, is before. | timecreated_edt | ## +1 year ## %Y ## | @javascript Scenario Outline: Non-matching user filter by time created Given I log in as "admin" And I navigate to "Users > Accounts > Browse list of users" in site administration And I follow "Show more..." # Set the filter fields, excluding admin. When I set the following fields to these values: | <enablefield>[enabled] | 1 | | <enablefield>[year] | <year> | | Username field limiter | doesn't contain | | Username value | admin | And I press "Add filter" Then "Users" "table" should not exist And I should see "0 / 2 Users" Examples: | enablefield | year | # Time created, is after. | timecreated_sdt | ## +1 year ## %Y ## | # Time created, is before. | timecreated_edt | ## -1 year ## %Y ## | tests/behat/view_participants_groups.feature 0000644 00000006566 15151162244 0015516 0 ustar 00 @core @core_user Feature: View course participants groups In order to know who is on a course As a teacher I need to be able to view the participants groups on a course Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1x | Teacher | 1x | teacher1x@example.com | | student1x | Student | 1x | student1x@example.com | | student2x | Student | 2x | student2x@example.com | | student3x | Student | 3x | student3x@example.com | | student4x | Student | 4x | student4x@example.com | And the following "courses" exist: | fullname | shortname | format | groupmode | | Course 1 | C1 | topics | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1x | C1 | editingteacher | | student1x | C1 | student | | student2x | C1 | student | | student3x | C1 | student | | student4x | C1 | student | And the following "groups" exist: | name | course | idnumber | | Group A | C1 | G1 | | Group B | C1 | G2 | And the following "group members" exist: | user | group | | student1x | G1 | | student2x | G1 | | student3x | G2 | | student4x | G2 | Scenario: User should not be able to see other groups in separated group mode Given I log in as "student1x" And I am on "Course 1" course homepage When I navigate to course participants Then I should see "Group A" And I should see "Student 1x" And I should see "Student 2x" And I should not see "Group B" And I should not see "Student 3x" And I should not see "Student 4x" @javascript Scenario: User should be able to see other groups in visible group mode Given I log in as "admin" And I am on "Course 1" course homepage And I navigate to "Settings" in current page administration And I expand all fieldsets And I set the field "Group mode" to "Visible groups" And I press "Save and display" And I log out And I log in as "student1x" And I am on "Course 1" course homepage When I navigate to course participants Then I should see "Group A" And I should see "Student 1x" And I should see "Student 2x" And I set the field "type" in the "Filter 1" "fieldset" to "Groups" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group B" And I click on "Apply filters" "button" And I should see "Student 3x" And I should see "Student 4x" Scenario: User should be able to see all users in no groups mode Given I log in as "admin" And I am on "Course 1" course homepage And I navigate to "Settings" in current page administration And I expand all fieldsets And I set the field "Group mode" to "No groups" And I press "Save and display" And I log out And I log in as "student1x" And I am on "Course 1" course homepage When I navigate to course participants Then I should see "Group A" And I should see "Student 1x" And I should see "Student 2x" And I should see "Group B" And I should see "Student 3x" And I should see "Student 4x" And I should see "Teacher 1x" And I should see "No groups" tests/behat/edituserpassword.feature 0000644 00000003761 15151162244 0013765 0 ustar 00 @core @core_user Feature: Edit a users password In order edit a user password properly As an admin I need to be able to edit their profile and change their password @javascript Scenario: Verify the password field is enabled/disabled based on authentication selected, in user edit advanced page. Given I log in as "admin" When I navigate to "Users > Accounts > Add a new user" in site administration Then the "New password" "field" should be enabled And I set the field "auth" to "Web services authentication" And the "New password" "field" should be disabled And I set the field "auth" to "Email-based self-registration" And the "New password" "field" should be enabled # We need to cancel/submit a form that has been modified. And I press "Create user" Scenario: Sign out everywhere field is not present if user doesn't have active token Given the following "users" exist: | username | firstname | lastname | email | | user01 | User | One | user01@example.com | When I am on the "user01" "user > editing" page logged in as "admin" Then "Sign out everywhere" "field" should not exist Scenario Outline: Sign out everywhere field is present based on expiry of active token Given the following "users" exist: | username | firstname | lastname | email | | user01 | User | One | user01@example.com | And the following "core_webservice > Service" exist: | shortname | name | | mytestservice | My test service | And the following "core_webservice > Tokens" exist: | user | service | validuntil | | user01 | mytestservice | <validuntil> | When I am on the "user01" "user > editing" page logged in as "admin" Then "Sign out everywhere" "field" <shouldornot> exist Examples: | validuntil | shouldornot | | ## -1 month ## | should not | | 0 | should | | ## +1 month ## | should | tests/behat/add_blocks.feature 0000644 00000001532 15151162244 0012435 0 ustar 00 @core @core_user Feature: Add blocks to my profile page In order to add more functionality to my profile page As a user I need to add blocks to my profile page Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | And I log in as "admin" And I follow "View profile" Scenario: Add blocks to page When I turn editing mode on And I add the "Latest announcements" block Then I should see "Latest announcements" tests/behat/custom_profile_fields_manual.feature 0000644 00000003522 15151162244 0016266 0 ustar 00 @core @core_user Feature: Custom profile fields creation using UI @javascript Scenario Outline: Manual creation of basic custom profile fields Given I log in as "admin" And I navigate to "Users > Accounts > User profile fields" in site administration And I click on "Create a new profile field" "link" And I click on "<name>" "link" And I set the following fields to these values: | Short name | <shortname> | | Name | <name> | When I click on "Save changes" "button" Then I should see "<name>" Examples: | shortname | name | | checkbox | Checkbox | | datetime | Date/Time | | textarea | Text area | | textinput | Text input | @javascript Scenario: Manual creation of drop-down menu custom profile field type Given I log in as "admin" And I navigate to "Users > Accounts > User profile fields" in site administration And I click on "Create a new profile field" "link" And I click on "Drop-down menu" "link" And I set the following fields to these values: | Short name | dropdownmenu | | Name | Drop-down menu field | And I set the field "Menu options (one per line)" to multiline: """ a b """ When I click on "Save changes" "button" Then I should see "Drop-down menu field" @javascript Scenario: Manual creation of social custom profile field type Given I log in as "admin" And I navigate to "Users > Accounts > User profile fields" in site administration And I click on "Create a new profile field" "link" And I click on "Social" "link" And I set the following fields to these values: | Network type | Web page | | Short name | social | When I click on "Save changes" "button" Then I should see "Web page" tests/behat/delete_users.feature 0000644 00000011175 15151162244 0013037 0 ustar 00 @core @core_user Feature: Deleting users In order to manage a Moodle site As an admin I need to be able to delete users Background: Given the following "users" exist: | username | firstname | lastname | email | | user1 | User | One | one@example.com | | user2 | User | Two | two@example.com | | user3 | User | Three | three@example.com | | user4 | User | Four | four@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | user1 | C1 | student | | user2 | C1 | student | | user3 | C1 | student | | user4 | C1 | student | And the following config values are set as admin: | messaging | 1 | @javascript Scenario: Deleting one user at a time When I log in as "admin" And I navigate to "Users > Accounts > Bulk user actions" in site administration And the "Available" select box should contain "User Four" And I set the field "Available" to "User Four" And I press "Add to selection" And I set the field "id_action" to "Delete" And I press "Go" And I should see "Are you absolutely sure you want to completely delete the user User Four, including their enrolments, activity and other user data?" And I press "Yes" And I should see "Changes saved" And I press "Continue" Then the "Available" select box should not contain "User Four" And the "Available" select box should contain "User One" @javascript Scenario: Deleting more than one user at a time When I log in as "admin" And I navigate to "Users > Accounts > Bulk user actions" in site administration And I set the field "Available" to "User Four" And I press "Add to selection" And I set the field "Available" to "User Three" And I press "Add to selection" And I set the field "id_action" to "Delete" And I press "Go" And I should see "Are you absolutely sure you want to completely delete the user User Four, User Three, including their enrolments, activity and other user data?" And I press "Yes" And I should see "Changes saved" And I press "Continue" Then the "Available" select box should not contain "User Four" And the "Available" select box should not contain "User Three" And the "Available" select box should contain "User One" @javascript @core_message Scenario: Deleting users who have unread messages sent or received When I log in as "user1" And I send "Message 1 from user1 to user2" message to "User Two" user And I log out And I log in as "user3" And I send "Message 2 from user3 to user4" message to "User Four" user And I log out And I log in as "admin" And I navigate to "Users > Accounts > Bulk user actions" in site administration And I set the field "Available" to "User One" And I press "Add to selection" And I set the field "Available" to "User Four" And I press "Add to selection" And I set the field "id_action" to "Delete" And I press "Go" And I press "Yes" Then I should see "Changes saved" And I navigate to "Users > Accounts > Bulk user actions" in site administration And I set the field "Available" to "User Two" And I press "Add to selection" And I set the field "Available" to "User Three" And I press "Add to selection" And I set the field "id_action" to "Delete" And I press "Go" And I press "Yes" And I should see "Changes saved" And I press "Continue" And the "Available" select box should not contain "User Four" And the "Available" select box should not contain "User Three" And the "Available" select box should not contain "User One" And the "Available" select box should not contain "User Two" @javascript Scenario: Deleting a bulked user When I log in as "admin" And I navigate to "Users > Accounts > Bulk user actions" in site administration And I set the field "Available" to "User Two" And I press "Add to selection" And I set the field "Available" to "User One" And I press "Add to selection" Then I should see "User One" And I navigate to "Users > Accounts > Browse list of users" in site administration And I set the following fields to these values: | username | user1 | And I press "Add filter" And I click on "Delete" "link" And I press "Delete" And I navigate to "Users > Accounts > Bulk user actions" in site administration Then I should not see "User One" tests/behat/name_fields.feature 0000644 00000005647 15151162244 0012631 0 ustar 00 @core @core_user Feature: Both first name and last name are always available for every user In order to easily identify and display users on Moodle pages As any user I need to rely on both first name and last name are always available Scenario: Attempting to self-register as a new user with empty names Given the following config values are set as admin: | registerauth | email | | passwordpolicy | 0 | And I am on site homepage And I follow "Log in" And I click on "Create new account" "link" When I set the following fields to these values: | Username | mrwhitespace | | Password | Gue$$m3ifY0uC&n | | Email address | mrwhitespace@nas.ty | | Email (again) | mrwhitespace@nas.ty | And I set the field "First name" to " " And I set the field "Last name" to " " And I press "Create my new account" Then I should see "Missing given name" And I should see "Missing last name" Scenario: Attempting to change own names to whitespace Given the following "users" exist: | username | firstname | lastname | email | | foobar | Foo | Bar | foo@bar.com | And I log in as "foobar" # UI test covering "I open my profile in edit mode" - # This should be one of the very few places where we directly call these 2 steps to open the current users profile # in edit mode, the rest of the time you should use "I open my profile in edit mode" as it is faster. And I follow "Profile" in the user menu And I click on "Edit profile" "link" in the "region-main" "region" # End UI test covering "I open my profile in edit mode" When I set the field "First name" to " " And I set the field "Last name" to " " And I click on "Cancel" "button" And I follow "Profile" in the user menu And I click on "Edit profile" "link" in the "region-main" "region" Then I should see "Foo" And I should see "Bar" When I set the field "First name" to " " And I set the field "Last name" to " " And I click on "Update profile" "button" Then I should see "Missing given name" And I should see "Missing last name" Scenario: Attempting to change someone else's names to whitespace Given the following "users" exist: | username | firstname | lastname | email | | foobar | Foo | Bar | foo@bar.com | And I am on the "foobar" "user > editing" page logged in as "admin" When I set the field "First name" to " " And I set the field "Last name" to " " And I click on "Cancel" "button" And I follow "Foo Bar" And I click on "Edit profile" "link" in the "region-main" "region" Then I should see "Foo" And I should see "Bar" When I set the field "First name" to " " And I set the field "Last name" to " " And I click on "Update profile" "button" Then I should see "Missing given name" And I should see "Missing last name" tests/behat/filter_idnumber.feature 0000644 00000006265 15151162244 0013532 0 ustar 00 @core @core_user Feature: Filter users by idnumber As a system administrator I need to be able to filter users by their ID number So that I can quickly find users based on an external key. Background: Given the following "users" exist: | username | firstname | lastname | email | idnumber | | teacher1 | Teacher | 1 | teacher@example.com | 0000002 | | student1 | Student1 | 1 | student1@example.com | 0000003 | | student2 | Student2 | 1 | student2@example.com | 2000000 | | student3 | Student3 | 1 | student3@example.com | 3000000 | And I log in as "admin" And I navigate to "Users > Accounts > Browse list of users" in site administration @javascript Scenario: Filtering id numbers - with case "is empty" # We should see see admin on the user list, the following e-mail is admin's e-mail. Then I should see "moodle@example.com" in the "users" "table" And I should see "Teacher" in the "users" "table" And I should see "Student1" in the "users" "table" And I should see "Student2" in the "users" "table" And I should see "Student3" in the "users" "table" And I follow "Show more..." And I set the field "id_idnumber_op" to "is empty" When I press "Add filter" # We should see admin on the user list, the following e-mail is admin's e-mail. Then I should see "moodle@example.com" in the "users" "table" And I should not see "Teacher" in the "users" "table" And I should not see "Student1" in the "users" "table" And I should not see "Student2" in the "users" "table" And I should not see "Student3" in the "users" "table" @javascript Scenario Outline: Filtering id numbers - with all other cases # We should see see admin on the user list, the following e-mail is admin's e-mail. Then I should see "moodle@example.com" in the "users" "table" And I should see "Teacher" in the "users" "table" And I should see "Student1" in the "users" "table" And I should see "Student2" in the "users" "table" And I should see "Student3" in the "users" "table" And I follow "Show more..." And I set the field "id_idnumber_op" to "<Category>" And I set the field "idnumber" to "<Argument>" When I press "Add filter" Then I should <Admin's Visibility> "moodle@example.com" in the "users" "table" And I should <Teacher's Vis> "Teacher" in the "users" "table" And I should <S1's Vis> "Student1" in the "users" "table" And I should <S2's Vis> "Student2" in the "users" "table" And I should <S3's Vis> "Student3" in the "users" "table" Examples: | Category | Argument | Admin's Visibility | Teacher's Vis | S1's Vis | S2's Vis | S3's Vis | | contains | 0 | not see | see | see | see | see | | doesn't contain | 2 | see | not see | see | not see | see | | is equal to | 2000000 | not see | not see | not see | see | not see | | starts with | 0 | not see | see | see | not see | not see | | ends with | 0 | not see | not see | not see | see | see | tests/behat/edit_profile_notification.feature 0000644 00000004037 15151162244 0015566 0 ustar 00 @core @core_user Feature: Notification shown when user edit profile or preferences In order to show notification As a user I press update profile button after make some changes in edit profile page Background: Given the following "users" exist: | username | firstname | lastname | email | | unicorn | Unicorn | 1 | unicorn@example.com | And the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | unicorn | C1 | student | @javascript Scenario: Change own profile and has notification shown Given I log in as "unicorn" And I open my profile in edit mode And I should see "Unicorn" And I should see "1" Then I set the field "Last name" to "Lil" And I click on "Update profile" "button" And I should see "Changes saved" And I press "Dismiss this notification" And I should not see "Changes saved" And I follow "Preferences" in the user menu And I follow "Preferred language" And I click on "Save changes" "button" And I should see "Changes saved" And I follow "Forum preferences" And I set the field "Use experimental nested discussion view" to "Yes" And I click on "Save changes" "button" And I should see "Changes saved" @javascript Scenario: Do not show notification when cancel profile change Given I log in as "unicorn" And I open my profile in edit mode And I should see "Unicorn" And I should see "1" Then I set the field "Last name" to "Lil" And I click on "Cancel" "button" And I should not see "Changes saved" @javascript Scenario: Show notification after admin edited profile of another user Given I am on the "unicorn" "user > editing" page logged in as "admin" And I expand all fieldsets Then I set the field "Last name" to "Lil" And I click on "Update profile" "button" And I should see "Changes saved" tests/behat/full_name_display.feature 0000644 00000011160 15151162244 0014035 0 ustar 00 @core @core_user Feature: Users' names are displayed across the site according to the user policy settings In order to control the way students and teachers see users' names As a teacher or admin I need to be able to configure the name display formats 'fullnamedisplay' and 'alternativefullnameformat' Background: Given the following "users" exist: | username | firstname | lastname | email | middlename | alternatename | firstnamephonetic | lastnamephonetic | | user1 | Grainne | Beauchamp | one@example.com | Ann | Jill | Gronya | Beecham | | user2 | Niamh | Cholmondely | two@example.com | Jane | Nina | Nee | Chumlee | | user3 | Siobhan | Desforges | three@example.com | Sarah | Sev | Shevon | De-forjay | | teacher1 | Teacher | 1 | teacher1@example.com | | | | | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | user1 | C1 | student | | user2 | C1 | student | And the following config values are set as admin: | fullnamedisplay | firstnamephonetic,lastnamephonetic | | alternativefullnameformat | middlename, alternatename, firstname, lastname | Scenario: As a student, 'fullnamedisplay' should be used in the participants list and when viewing my own course profile Given I log in as "user1" And I am on "Course 1" course homepage When I navigate to course participants And I click on "Gronya,Beecham" "link" in the "Gronya,Beecham" "table_row" Then I should see "Gronya,Beecham" in the "region-main" "region" And I log out Scenario: As a student, 'fullnamedisplay' should be used in the participants list and when viewing another user's course profile Given I log in as "user2" And I am on "Course 1" course homepage When I navigate to course participants And I click on "Gronya,Beecham" "link" in the "Gronya,Beecham" "table_row" Then I should see "Gronya,Beecham" in the "region-main" "region" And I log out Scenario: As a teacher, 'alternativefullnameformat' should be used in the participants list but 'fullnamedisplay' used on the course profile Given I log in as "teacher1" And I am on "Course 1" course homepage When I navigate to course participants Then I should see "Ann, Jill, Grainne, Beauchamp" in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I click on "Ann, Jill, Grainne, Beauchamp" "link" in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I should see "Gronya,Beecham" in the "region-main" "region" And I log out Scenario: As an authenticated user, 'fullnamedisplay' should be used in the navigation and when viewing my profile Given I log in as "user1" When I follow "Profile" in the user menu Then I should see "Gronya,Beecham" in the ".page-context-header" "css_element" And I should see "You are logged in as Gronya,Beecham" in the "page-footer" "region" And I log out Scenario: As an admin, 'fullnamedisplay' should be used when using the 'log in as' function Given I log in as "admin" When I navigate to "Users > Accounts > Browse list of users" in site administration And I follow "Jane, Nina, Niamh, Cholmondely" And I follow "Log in as" Then I should see "You are logged in as Nee,Chumlee" in the ".usermenu" "css_element" And I should see "You are logged in as Jane, Nina, Niamh, Cholmondely" in the "region-main" "region" And I should see "You are logged in as Nee,Chumlee" in the "page-footer" "region" And I log out Scenario: As an admin, 'fullnamedisplay' should be used when viewing another user's site profile Given I log in as "admin" When I navigate to "Users > Accounts > Browse list of users" in site administration And I follow "Ann, Jill, Grainne, Beauchamp" Then I should see "Gronya,Beecham" in the ".page-header-headings" "css_element" And I log out @javascript Scenario: As a teacher, the 'alternativefullnameformat' should be used when searching for and enrolling a user Given I log in as "teacher1" And I am on "Course 1" course homepage When I navigate to course participants And I press "Enrol users" And I click on "Select users" "field" And I type "three@example.com" Then I should see "Sarah, Sev, Siobhan, Desforges" tests/behat/bulk_message.feature 0000644 00000002761 15151162244 0013016 0 ustar 00 @core @core_user @javascript Feature: Bulk message In order to communicate with my students As a teacher I need to be able to send a message to all my students Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | Scenario: Send a message to students from participants list Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants And I click on "Select all" "checkbox" And I set the field "With selected users..." to "Send a message" And "Send message to 3 people" "dialogue" should exist # Try to send an empty message. When I press "Send message to 3 people" Then I should see "Please enter message text" And I set the following fields to these values: | bulk-message | "Hello world!" | And I press "Send message to 3 people" And I should see "Message sent to 3 people" tests/behat/input-purpose.feature 0000644 00000003503 15151162244 0013202 0 ustar 00 @core @core_user Feature: The purpose of each input field collecting information about the user can be determined Background: Given the following "users" exist: | username | firstname | lastname | email | | unicorn | unicorn | 1 | unicorn@example.com | And the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | unicorn | C1 | student | @javascript Scenario: Fields for other users are not auto filled When I am on the "unicorn@example.com" "user > editing" page logged in as "admin" And I expand all fieldsets Then the field "Username" should not have purpose "username" And the field "First name" should not have purpose "given-name" And the field "Last name" should not have purpose "family-name" And the field "Email" should not have purpose "email" And the field "Select a country" should not have purpose "country" And I press "Cancel" And I follow "Preferred language" And the field "Preferred language" should not have purpose "language" @javascript Scenario: My own user fields are auto filled Given I log in as "unicorn" When I open my profile in edit mode And I expand all fieldsets Then the field "First name" should have purpose "given-name" And the field "Last name" should have purpose "family-name" And the field "Email" should have purpose "email" And the field "Select a country" should have purpose "country" And I press "Cancel" And I follow "Preferences" in the user menu And I follow "Preferred language" And the field "Preferred language" should have purpose "language" tests/behat/table_column_visibility.feature 0000644 00000004705 15151162244 0015270 0 ustar 00 @core @core_user Feature: The visibility of table columns can be toggled In order to customise my view of participants data As a user I need to be able to hide and show columns in the participants table Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | t1 | Agatha | T | agatha@example.com | | s1 | Matilda | W | matilda@example.com | | s2 | Mick | H | mick@example.com | And the following "course enrolments" exist: | user | course | role | | t1 | C1 | editingteacher | | s1 | C1 | student | | s2 | C1 | student | @javascript Scenario: The visibility of columns can be individually toggled within the participants table Given I log in as "t1" And I am on "Course 1" course homepage And I navigate to course participants And I should see "Email address" in the "participants" "table" And I should see "matilda@example.com" in the "participants" "table" And I should see "Roles" in the "participants" "table" And I should see "Student" in the "participants" "table" When I follow "Hide Email address" Then I should not see "Email address" in the "participants" "table" And I should not see "matilda@example.com" in the "participants" "table" And I should see "Roles" in the "participants" "table" And I should see "Student" in the "participants" "table" And I follow "Hide Roles" And I should not see "Roles" in the "participants" "table" And I should not see "Student" in the "participants" "table" And I should not see "matilda@example.com" in the "participants" "table" And I follow "Show Email address" And I should see "Email address" in the "participants" "table" And I should see "matilda@example.com" in the "participants" "table" And I should not see "Roles" in the "participants" "table" And I should not see "Student" in the "participants" "table" And I follow "Show Roles" And I should see "Roles" in the "participants" "table" And I should see "Student" in the "participants" "table" And I should see "Email address" in the "participants" "table" And I should see "matilda@example.com" in the "participants" "table" tests/behat/bulk_editenrolment.feature 0000644 00000005724 15151162244 0014245 0 ustar 00 @core @core_user Feature: Bulk enrolments In order to manage a course site As a teacher I need to be able to bulk edit enrolments Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C1 | editingteacher | And the following "cohorts" exist: | name | idnumber | | Cohort | cohortid1 | @javascript Scenario: Bulk edit enrolments When I log in as "admin" And I am on "Course 1" course homepage And I navigate to course participants And I click on "Select all" "checkbox" And I set the field "With selected users..." to "Edit selected user enrolments" And I set the field "Alter status" to "Suspended" And I press "Save changes" Then I should see "Suspended" in the "Teacher 1" "table_row" And I should see "Suspended" in the "Student 1" "table_row" And I should see "Suspended" in the "Student 2" "table_row" @javascript Scenario: Bulk delete enrolments When I log in as "admin" And I am on "Course 1" course homepage And I navigate to course participants And I click on "Select all" "checkbox" And I set the field "With selected users..." to "Delete selected user enrolments" And I press "Unenrol users" Then I should not see "Student 1" And I should not see "Student 2" And I should not see "Teacher 1" And I should see "3 unenrolled users" @javascript Scenario: Bulk edit enrolment for deleted user When I log in as "admin" And I navigate to "Users > Accounts > Bulk user actions" in site administration And I set the field "Available" to "Student 1" And I press "Add to selection" And I set the field "Available" to "Student 2" And I press "Add to selection" And I navigate to "Users > Accounts > Browse list of users" in site administration And I set the following fields to these values: | username | student1 | And I press "Add filter" And I click on "Delete" "link" And I press "Delete" And I navigate to "Users > Accounts > Bulk user actions" in site administration And I set the field "id_action" to "Add to cohort" And I press "Go" And I set the field "id_cohort" to "Cohort [cohortid1]" And I press "Add to cohort" And I navigate to "Users > Accounts > Cohorts" in site administration And I press "Assign" action in the "cohortid1" report row Then the "removeselect" select box should contain "Student 2 (student2@example.com)" And the "removeselect" select box should not contain "Student 1 (student1@example.com)" tests/behat/set_default_homepage.feature 0000644 00000006241 15151162244 0014516 0 ustar 00 @core @core_user Feature: Set the site home page and dashboard as the default home page In order to set a page as my default home page As a user I need to choose which page I want and set it as my home page Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | user1 | User | One | user1@example.com | And the following "course enrolments" exist: | user | course | role | | user1 | C1 | student | Scenario: Admin sets the site page and then the dashboard as the default home page # This functionality does not work without the administration block. Given I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" And I add the "Administration" block if not present And I configure the "Administration" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" And I navigate to "Appearance > Navigation" in site administration And I set the field "Start page for users" to "User preference" And I press "Save changes" And I am on site homepage And I follow "Make this my home page" And I should not see "Make this my home page" And I am on "Course 1" course homepage And I should see "Home" in the "Navigation" "block" And I should not see "Site home" in the "Navigation" "block" And I am on site homepage And I follow "Dashboard" And I follow "Make this my home page" And I should not see "Make this my home page" And I am on "Course 1" course homepage Then I should not see "Home" in the "Navigation" "block" And I should see "Site home" in the "Navigation" "block" Scenario: User cannot configure their preferred default home page unless allowed by admin Given I log in as "user1" When I follow "Preferences" in the user menu Then I should not see "Home page" Scenario Outline: User can configure their preferred default home page when allowed by admin Given I log in as "admin" And I navigate to "Appearance > Navigation" in site administration And I set the field "Start page for users" to "User preference" And I press "Save changes" And I log out When I log in as "user1" And I follow "Preferences" in the user menu And I follow "Start page" And I set the field "Start page" to "<preference>" And I press "Save changes" And I log out And I log in as "user1" Then I should see "<breadcrumb>" is active in navigation Examples: | preference | breadcrumb | | Home | Home | | Dashboard | Dashboard | | My courses | My courses | tests/behat/view_participants.feature 0000644 00000036123 15151162244 0014107 0 ustar 00 @core @core_user Feature: View course participants In order to know who is on a course As a teacher I need to be able to view the participants on a course Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1x | Teacher | 1x | teacher1x@example.com | | student0x | Student | 0x | student0x@example.com | | student1x | Student | 1x | student1x@example.com | | student2x | Student | 2x | student2x@example.com | | student3x | Student | 3x | student3x@example.com | | student4x | Student | 4x | student4x@example.com | | student5x | Student | 5x | student5x@example.com | | student6x | Student | 6x | student6x@example.com | | student7x | Student | 7x | student7x@example.com | | student8x | Student | 8x | student8x@example.com | | student9x | Student | 9x | student9x@example.com | | student10x | Student | 10x | student10x@example.com | | student11x | Student | 11x | student11x@example.com | | student12x | Student | 12x | student12x@example.com | | student13x | Student | 13x | student13x@example.com | | student14x | Student | 14x | student14x@example.com | | student15x | Student | 15x | student15x@example.com | | student16x | Student | 16x | student16x@example.com | | student17x | Student | 17x | student17x@example.com | | student18x | Student | 18x | student18x@example.com | | student19x | Student | 19x | student19x@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | status | timeend | | teacher1x | C1 | editingteacher | 0 | 0 | | student0x | C1 | student | 0 | 0 | | student1x | C1 | student | 0 | 0 | | student2x | C1 | student | 0 | 0 | | student3x | C1 | student | 0 | 0 | | student4x | C1 | student | 0 | 0 | | student5x | C1 | student | 0 | 0 | | student6x | C1 | student | 0 | 0 | | student7x | C1 | student | 0 | 0 | | student8x | C1 | student | 0 | 0 | | student9x | C1 | student | 0 | 0 | | student10x | C1 | student | 1 | 0 | | student11x | C1 | student | 0 | 100 | | student12x | C1 | student | 0 | 0 | | student13x | C1 | student | 0 | 0 | | student14x | C1 | student | 0 | 0 | | student15x | C1 | student | 0 | 0 | | student16x | C1 | student | 0 | 0 | | student17x | C1 | student | 0 | 0 | | student18x | C1 | student | 0 | 0 | @javascript Scenario: Use select and deselect all buttons Given I log in as "teacher1x" And I am on "Course 1" course homepage And I navigate to course participants When I click on "Select all" "checkbox" Then the field "Select 'Teacher 1x'" matches value "1" And the field "Select 'Student 0x'" matches value "1" And the field "Select 'Student 1x'" matches value "1" And the field "Select 'Student 2x'" matches value "1" And the field "Select 'Student 3x'" matches value "1" And the field "Select 'Student 4x'" matches value "1" And the field "Select 'Student 5x'" matches value "1" And the field "Select 'Student 6x'" matches value "1" And the field "Select 'Student 7x'" matches value "1" And the field "Select 'Student 8x'" matches value "1" And the field "Select 'Student 9x'" matches value "1" And the field "Select 'Student 10x'" matches value "1" And the field "Select 'Student 11x'" matches value "1" And the field "Select 'Student 12x'" matches value "1" And the field "Select 'Student 13x'" matches value "1" And the field "Select 'Student 14x'" matches value "1" And the field "Select 'Student 14x'" matches value "1" And the field "Select 'Student 15x'" matches value "1" And the field "Select 'Student 16x'" matches value "1" And the field "Select 'Student 17x'" matches value "1" And the field "Select 'Student 18x'" matches value "1" And I click on "Deselect all" "checkbox" And the field "Select 'Teacher 1x'" matches value "0" And the field "Select 'Student 0x'" matches value "0" And the field "Select 'Student 1x'" matches value "0" And the field "Select 'Student 2x'" matches value "0" And the field "Select 'Student 3x'" matches value "0" And the field "Select 'Student 4x'" matches value "0" And the field "Select 'Student 5x'" matches value "0" And the field "Select 'Student 6x'" matches value "0" And the field "Select 'Student 7x'" matches value "0" And the field "Select 'Student 8x'" matches value "0" And the field "Select 'Student 9x'" matches value "0" And the field "Select 'Student 10x'" matches value "0" And the field "Select 'Student 11x'" matches value "0" And the field "Select 'Student 12x'" matches value "0" And the field "Select 'Student 13x'" matches value "0" And the field "Select 'Student 14x'" matches value "0" And the field "Select 'Student 14x'" matches value "0" And the field "Select 'Student 15x'" matches value "0" And the field "Select 'Student 16x'" matches value "0" And the field "Select 'Student 17x'" matches value "0" And the field "Select 'Student 18x'" matches value "0" @javascript Scenario: Sort and paginate the list of users Given I log in as "teacher1x" And the following "course enrolments" exist: | user | course | role | | student19x | C1 | student | And I am on "Course 1" course homepage And I navigate to course participants And I follow "Email address" When I click on "2" "link" in the "//nav[@aria-label='Page']" "xpath_element" Then I should not see "student0x@example.com" And I should not see "student19x@example.com" And I should see "teacher1x@example.com" And I follow "Email address" And I click on "2" "link" in the "//nav[@aria-label='Page']" "xpath_element" And I should not see "teacher1x@example.com" And I should not see "student19x@example.com" And I should not see "student1x@example.com" And I should see "student0x@example.com" @javascript Scenario: Use select all users on this page, select all users and deselect all Given the following "course enrolments" exist: | user | course | role | | student19x | C1 | student | When I log in as "teacher1x" And I am on "Course 1" course homepage And I navigate to course participants And I click on "Select all" "checkbox" Then I should not see "Student 9x" And the field "Select 'Teacher 1x'" matches value "1" And the field "Select 'Student 0x'" matches value "1" And the field "Select 'Student 1x'" matches value "1" And the field "Select 'Student 2x'" matches value "1" And the field "Select 'Student 3x'" matches value "1" And the field "Select 'Student 4x'" matches value "1" And the field "Select 'Student 5x'" matches value "1" And the field "Select 'Student 6x'" matches value "1" And the field "Select 'Student 7x'" matches value "1" And the field "Select 'Student 8x'" matches value "1" And the field "Select 'Student 10x'" matches value "1" And the field "Select 'Student 11x'" matches value "1" And the field "Select 'Student 12x'" matches value "1" And the field "Select 'Student 13x'" matches value "1" And the field "Select 'Student 14x'" matches value "1" And the field "Select 'Student 14x'" matches value "1" And the field "Select 'Student 15x'" matches value "1" And the field "Select 'Student 16x'" matches value "1" And the field "Select 'Student 17x'" matches value "1" And the field "Select 'Student 18x'" matches value "1" And the field "Select 'Student 19x'" matches value "1" And I click on "Deselect all" "checkbox" And the field "Select 'Teacher 1x'" matches value "0" And the field "Select 'Student 0x'" matches value "0" And the field "Select 'Student 1x'" matches value "0" And the field "Select 'Student 2x'" matches value "0" And the field "Select 'Student 3x'" matches value "0" And the field "Select 'Student 4x'" matches value "0" And the field "Select 'Student 5x'" matches value "0" And the field "Select 'Student 6x'" matches value "0" And the field "Select 'Student 7x'" matches value "0" And the field "Select 'Student 8x'" matches value "0" And the field "Select 'Student 10x'" matches value "0" And the field "Select 'Student 11x'" matches value "0" And the field "Select 'Student 12x'" matches value "0" And the field "Select 'Student 13x'" matches value "0" And the field "Select 'Student 14x'" matches value "0" And the field "Select 'Student 14x'" matches value "0" And the field "Select 'Student 15x'" matches value "0" And the field "Select 'Student 16x'" matches value "0" And the field "Select 'Student 17x'" matches value "0" And the field "Select 'Student 18x'" matches value "0" And the field "Select 'Student 19x'" matches value "0" # Pressing the "Select all X users" button should select all including the 21st user (Student 9x). And I press "Select all 21 users" And I should see "Student 9x" And the field "Select 'Teacher 1x'" matches value "1" And the field "Select 'Student 0x'" matches value "1" And the field "Select 'Student 1x'" matches value "1" And the field "Select 'Student 2x'" matches value "1" And the field "Select 'Student 3x'" matches value "1" And the field "Select 'Student 4x'" matches value "1" And the field "Select 'Student 5x'" matches value "1" And the field "Select 'Student 6x'" matches value "1" And the field "Select 'Student 7x'" matches value "1" And the field "Select 'Student 8x'" matches value "1" And the field "Select 'Student 9x'" matches value "1" And the field "Select 'Student 10x'" matches value "1" And the field "Select 'Student 11x'" matches value "1" And the field "Select 'Student 12x'" matches value "1" And the field "Select 'Student 13x'" matches value "1" And the field "Select 'Student 14x'" matches value "1" And the field "Select 'Student 14x'" matches value "1" And the field "Select 'Student 15x'" matches value "1" And the field "Select 'Student 16x'" matches value "1" And the field "Select 'Student 17x'" matches value "1" And the field "Select 'Student 18x'" matches value "1" And the field "Select 'Student 19x'" matches value "1" And the "With selected users..." "select" should be enabled And I click on "Deselect all" "checkbox" And the field "Select 'Teacher 1x'" matches value "0" And the field "Select 'Student 0x'" matches value "0" And the field "Select 'Student 1x'" matches value "0" And the field "Select 'Student 2x'" matches value "0" And the field "Select 'Student 3x'" matches value "0" And the field "Select 'Student 4x'" matches value "0" And the field "Select 'Student 5x'" matches value "0" And the field "Select 'Student 6x'" matches value "0" And the field "Select 'Student 7x'" matches value "0" And the field "Select 'Student 8x'" matches value "0" And the field "Select 'Student 9x'" matches value "0" And the field "Select 'Student 10x'" matches value "0" And the field "Select 'Student 11x'" matches value "0" And the field "Select 'Student 12x'" matches value "0" And the field "Select 'Student 13x'" matches value "0" And the field "Select 'Student 14x'" matches value "0" And the field "Select 'Student 14x'" matches value "0" And the field "Select 'Student 15x'" matches value "0" And the field "Select 'Student 16x'" matches value "0" And the field "Select 'Student 17x'" matches value "0" And the field "Select 'Student 18x'" matches value "0" And the field "Select 'Student 19x'" matches value "0" Scenario: View the participants page as a teacher Given I log in as "teacher1x" And I am on "Course 1" course homepage When I navigate to course participants Then I should see "Active" in the "student0x" "table_row" Then I should see "Active" in the "student1x" "table_row" And I should see "Active" in the "student2x" "table_row" And I should see "Active" in the "student3x" "table_row" And I should see "Active" in the "student4x" "table_row" And I should see "Active" in the "student5x" "table_row" And I should see "Active" in the "student6x" "table_row" And I should see "Active" in the "student7x" "table_row" And I should see "Active" in the "student8x" "table_row" And I should see "Active" in the "student9x" "table_row" And I should see "Suspended" in the "student10x" "table_row" And I should see "Not current" in the "student11x" "table_row" And I should see "Active" in the "student12x" "table_row" And I should see "Active" in the "student13x" "table_row" And I should see "Active" in the "student14x" "table_row" And I should see "Active" in the "student15x" "table_row" And I should see "Active" in the "student16x" "table_row" And I should see "Active" in the "student17x" "table_row" And I should see "Active" in the "student18x" "table_row" Scenario: View the participants page as a student Given I log in as "student1x" And I am on "Course 1" course homepage When I navigate to course participants # Student should not see the status column. Then I should not see "Status" in the "participants" "table" # Student should be able to see the other actively-enrolled students. And I should see "Student 1x" in the "participants" "table" And I should see "Student 2x" in the "participants" "table" And I should see "Student 3x" in the "participants" "table" And I should see "Student 4x" in the "participants" "table" And I should see "Student 5x" in the "participants" "table" And I should see "Student 6x" in the "participants" "table" And I should see "Student 7x" in the "participants" "table" And I should see "Student 8x" in the "participants" "table" # Suspended and non-current students should not be rendered. And I should not see "Student 10x" in the "participants" "table" And I should not see "Student 11x" in the "participants" "table" Scenario: Check status after disabling manual enrolment Given I log in as "admin" And I am on the "Course 1" "enrolment methods" page And I click on "Disable" "link" in the "Manual enrolments" "table_row" Then I navigate to course participants And I should see "Not current" in the "student0x" "table_row" tests/behat/hidden_user_fields.feature 0000644 00000004137 15151162244 0014173 0 ustar 00 @core @core_user Feature: Hidden user fields behavior In order to hide private information of users As an admin I can set Hide user fields setting Background: Given the following "users" exist: | username | firstname | lastname | email | description | city | | user | Profile | User | user@example.com | This is me | Donostia | | student | Student | User | student@example.com | | | | teacher | Teacher | User | teacher@example.com | | | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | user | C1 | student | | student | C1 | student | | teacher | C1 | editingteacher | And the following config values are set as admin: | hiddenuserfields | description,email | Scenario Outline: Hidden user fields on course context profile based on role permission Given I log in as "<user>" And I am on "Course 1" course homepage And I navigate to course participants And I should see "Profile User" When I click on "Profile User" "link" Then I <expected> "This is me" And I <expected> "user@example.com" And I should see "Donostia" Examples: | user | expected | | student | should not see | | teacher | should see | | admin | should see | Scenario Outline: Hidden user fields on system context profile based on role permission Given I log in as "<user>" And I am on "Course 1" course homepage And I navigate to course participants And I should see "Profile User" When I click on "Profile User" "link" And I click on "Full profile" "link" Then I <expected> "This is me" And I <expected> "user@example.com" And I should see "Donostia" Examples: | user | expected | | student | should not see | | teacher | should not see | | admin | should see | tests/behat/edit_user_enrolment.feature 0000644 00000015465 15151162244 0014430 0 ustar 00 @core @core_user Feature: Edit user enrolment In order to manage students' enrolments As a teacher I need to be able to view enrolment details and edit student enrolments in the course participants page Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | status | | teacher1 | C1 | editingteacher | 0 | | student1 | C1 | student | 0 | | student2 | C1 | student | 1 | @javascript Scenario: Edit a user's enrolment Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants When I click on "Edit enrolment" "icon" in the "student1" "table_row" And I should see "Edit Student 1's enrolment" And I set the field "Status" to "Suspended" And I click on "Save changes" "button" And I click on "Edit enrolment" "icon" in the "student2" "table_row" And I should see "Edit Student 2's enrolment" And I set the field "timeend[enabled]" to "1" And I set the field "timeend[day]" to "1" And I set the field "timeend[month]" to "January" And I set the field "timeend[year]" to "2017" And I set the field "Status" to "Active" And I click on "Save changes" "button" Then I should see "Suspended" in the "student1" "table_row" And I should see "Not current" in the "student2" "table_row" @javascript Scenario: Unenrol a student Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants When I click on "Unenrol" "icon" in the "student1" "table_row" And I click on "Unenrol" "button" in the "Unenrol" "dialogue" Then I should not see "Student 1" in the "participants" "table" @javascript Scenario: View a student's enrolment details Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants When I click on "Manual enrolments" "icon" in the "student1" "table_row" Then I should see "Enrolment details" And I should see "Student 1" in the "Full name" "table_row" And I should see "Active" in the "//td[@class='user-enrol-status']" "xpath_element" And I should see "Manual enrolments" in the "Enrolment method" "table_row" And I should see "Enrolment created" And I click on "Cancel" "button" in the "Enrolment details" "dialogue" And I click on "Manual enrolments" "icon" in the "student2" "table_row" And I should see "Enrolment details" And I should see "Student 2" in the "Full name" "table_row" And I should see "Suspended" in the "//td[@class='user-enrol-status']" "xpath_element" And I should see "Manual enrolments" in the "Enrolment method" "table_row" And I should see "Enrolment created" And "Edit enrolment" "icon" should exist in the "Enrolment method" "table_row" @javascript Scenario: View a student's enrolment details for a student enrolled via course meta link where editing can't be done Given the following "users" exist: | username | firstname | lastname | email | | student3 | Student | 3 | student3@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 2 | C2 | topics | And the following "course enrolments" exist: | user | course | role | status | | student3 | C2 | student | 0 | And I log in as "admin" And I navigate to "Plugins > Enrolments > Manage enrol plugins" in site administration And I click on "Enable" "link" in the "Course meta link" "table_row" And I add "Course meta link" enrolment method in "Course 1" with: | Link course | C2 | And I log out And I log in as "teacher1" And I am on "Course 1" course homepage When I navigate to course participants Then I should see "Student 3" in the "participants" "table" And "Edit enrolment" "icon" should not exist in the "student3" "table_row" And "Unenrol" "icon" should not exist in the "student3" "table_row" And I click on "Course meta link (Course 2)" "icon" in the "student3" "table_row" And I should see "Enrolment details" And I should see "Student 3" in the "Full name" "table_row" And I should see "Active" in the "//td[@class='user-enrol-status']" "xpath_element" And I should see "Course meta link (Course 2)" in the "Enrolment method" "table_row" And "Edit enrolment" "icon" should not exist in the "Enrolment method" "table_row" @javascript Scenario: Edit a student's enrolment details from the status dialogue Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants When I click on "Manual enrolments" "icon" in the "student2" "table_row" And I click on "Edit enrolment" "icon" in the "Enrolment method" "table_row" And I should see "Edit Student 2's enrolment" And I set the field "Status" to "Active" And I click on "Save changes" "button" Then I should see "Active" in the "student2" "table_row" # Without JS, the user should be redirected to the original edit enrolment form. Scenario: Edit a user's enrolment without JavaScript Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants When I click on "Edit enrolment" "link" in the "student1" "table_row" And I should see "Student 1" And I set the field "Status" to "Suspended" And I click on "Save changes" "button" And I click on "Edit enrolment" "link" in the "student2" "table_row" And I should see "Student 2" And I set the field "timeend[enabled]" to "1" And I set the field "timeend[day]" to "1" And I set the field "timeend[month]" to "January" And I set the field "timeend[year]" to "2017" And I set the field "Status" to "Active" And I click on "Save changes" "button" Then I should see "Suspended" in the "student1" "table_row" And I should see "Not current" in the "student2" "table_row" # Without JS, the user should be redirected to the original unenrol confirmation page. Scenario: Unenrol a student Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants When I click on "Unenrol" "link" in the "student1" "table_row" And I click on "Continue" "button" Then I should not see "Student 1" in the "participants" "table" tests/behat/edit_user_roles.feature 0000644 00000003174 15151162244 0013543 0 ustar 00 @core @core_user Feature: Edit user roles In order to administer users in course As a teacher I need to be able to assign and unassign roles in the course Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | @javascript Scenario: Assign roles on participants page Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants And I click on "Student 1's role assignments" "link" And I type "Non-editing teacher" And I press the enter key When I click on "Save changes" "link" Then I should see "Student, Non-editing teacher" in the "Student 1" "table_row" @javascript Scenario: Remove roles on participants page Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants And I click on "Student 1's role assignments" "link" And I click on "Student" "autocomplete_selection" When I click on "Save changes" "link" Then I should see "No roles" in the "Student 1" "table_row" tests/behat/siteadmin_user_breadcrumbs.feature 0000644 00000003130 15151162244 0015730 0 ustar 00 @core @core_user @javascript Feature: Verify the breadcrumbs in users account and cohort site administration pages Whenever I navigate to pages under users tab in site administration As an admin The breadcrumbs should be visible Background: Given I log in as "admin" @core @core_user Scenario: Verify the breadcrumbs in users tab as an admin Given I navigate to "Users > Accounts > Add a new user" in site administration And "Add a new user" "text" should exist in the ".breadcrumb" "css_element" And "Accounts" "link" should exist in the ".breadcrumb" "css_element" And I navigate to "Users > Accounts > Cohorts" in site administration And "Cohorts" "text" should exist in the ".breadcrumb" "css_element" And "Accounts" "link" should exist in the ".breadcrumb" "css_element" When I click on "All cohorts" "link" Then "All cohorts" "text" should exist in the ".breadcrumb" "css_element" And "Cohorts" "link" should exist in the ".breadcrumb" "css_element" And "Accounts" "link" should exist in the ".breadcrumb" "css_element" And I click on "Add new cohort" "link" And "Add new cohort" "text" should exist in the ".breadcrumb" "css_element" And "Cohorts" "link" should exist in the ".breadcrumb" "css_element" And "Accounts" "link" should exist in the ".breadcrumb" "css_element" And I click on "Upload cohorts" "link" And "Upload cohorts" "text" should exist in the ".breadcrumb" "css_element" And "Cohorts" "link" should exist in the ".breadcrumb" "css_element" And "Accounts" "link" should exist in the ".breadcrumb" "css_element" tests/behat/contact_site_support.feature 0000644 00000013044 15151162244 0014624 0 ustar 00 @core @core_user Feature: Contact site support method and availability can be customised In order to effectively support people using my Moodle site As an admin I need to be able to configure the site support method and who has access to it Background: Given the following "users" exist: | username | firstname | lastname | email | | user1 | User | One | user1@example.com | Scenario: Contact site support can be made available to all site visitors Given the following config values are set as admin: | supportavailability | 2 | # Confirm unauthenticated visitor has access to the contact form. When I am on site homepage Then I should see "Contact site support" in the "page-footer" "region" And I click on "Contact site support" "link" in the "page-footer" "region" And I should see "Contact site support" in the "page-header" "region" # Confirm someone logged in as guest has access to the contact form. And I log in as "guest" And I should see "Contact site support" in the "page-footer" "region" And I click on "Contact site support" "link" in the "page-footer" "region" And I should see "Contact site support" in the "page-header" "region" And I log out # Confirm logged in user has access to the contact form. And I log in as "user1" And I should see "Contact site support" in the "page-footer" "region" And I click on "Contact site support" "link" in the "page-footer" "region" And I should see "Contact site support" in the "page-header" "region" Scenario: Contact site support can be limited to authenticated users Given the following config values are set as admin: | supportavailability | 1 | # Confirm unauthenticated visitor cannot see the option or directly access the page. When I am on site homepage Then I should not see "Contact site support" in the "page-footer" "region" And I am on the "user > Contact Site Support" page And I should see "Acceptance test site" in the "page-header" "region" And I should not see "Contact site support" in the "page-header" "region" # Confirm someone logged in as guest cannot see the option or directly access the page. And I log in as "guest" And I should not see "Contact site support" in the "page-footer" "region" And I am on the "user > Contact Site Support" page And I should see "Acceptance test site" in the "page-header" "region" And I should not see "Contact site support" in the "page-header" "region" And I log out # Confirm logged in user has access to the contact form. And I log in as "user1" And I should see "Contact site support" in the "page-footer" "region" And I click on "Contact site support" "link" in the "page-footer" "region" And I should see "Contact site support" in the "page-header" "region" Scenario: Contact site support can be disabled Given the following config values are set as admin: | supportavailability | 0 | | defaulthomepage | home | # Confirm unauthenticated visitor cannot see the option. When I am on site homepage Then I should not see "Contact site support" in the "page-footer" "region" # Confirm someone logged in as guest cannot see the option. And I log in as "guest" And I should not see "Contact site support" in the "page-footer" "region" And I log out # Confirm logged in user cannot see the option. And I log in as "user1" And I should not see "Contact site support" in the "page-footer" "region" And I log out # Confirm admin cannot see the option. And I log in as "admin" And I should not see "Contact site support" in the "page-footer" "region" # Confirm visiting the contact form directly without permission redirects to the homepage. And I am on the "user > Contact Site Support" page And I should see "Acceptance test site" in the "page-header" "region" And I should not see "Contact site support" in the "page-header" "region" @javascript Scenario: Contact site support link opens a custom support page URL if set Given the following config values are set as admin: | supportavailability | 1 | | supportpage | user/profile.php | When I log in as "user1" And I am on site homepage And I click on "Contact site support" "link" in the "page-footer" "region" And I switch to a second window Then I should see "User One" in the "page-header" "region" And I should not see "Contact site support" in the "page-header" "region" And I close all opened windows Scenario: Visiting the contact site support page directly will redirect to the custom support page if set Given the following config values are set as admin: | supportavailability | 2 | | supportpage | profile.php | When I log in as "user1" And I am on the "user > Contact Site Support" page Then I should see "User One" in the "page-header" "region" And I should not see "Contact site support" in the "page-header" "region" Scenario: Visiting the contact site support page still redirects to homepage if access to support is disabled Given the following config values are set as admin: | supportavailability | 0 | | supportpage | profile.php | | defaulthomepage | home | When I log in as "user1" And I am on the "user > Contact Site Support" page Then I should see "Acceptance test site" in the "page-header" "region" And I should not see "Contact site support" in the "page-header" "region" tests/behat/custom_profile_fields.feature 0000644 00000033670 15151162244 0014740 0 ustar 00 @core @core_user Feature: Custom profile fields should be visible and editable by those with the correct permissions. Background: Given the following "users" exist: | username | firstname | lastname | email | | userwithinformation | userwithinformation | 1 | userwithinformation@example.com | And the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | userwithinformation | C1 | student | And the following config values are set as admin: | registerauth | email | And the following "custom profile fields" exist: | datatype | shortname | name | signup | visible | | text | notvisible_field | notvisible_field | 1 | 0 | | text | uservisible_field | uservisible_field | 1 | 1 | | text | everyonevisible_field | everyonevisible_field | 0 | 2 | | text | teachervisible_field | teachervisible_field | 1 | 3 | And I am on the "userwithinformation" "user > editing" page logged in as "admin" And I set the following fields to these values: | notvisible_field | notvisible_field_information | | uservisible_field | uservisible_field_information | | everyonevisible_field | everyonevisible_field_information | | teachervisible_field | teachervisible_field_information | And I click on "Update profile" "button" And I log out @javascript Scenario: Visible custom profile fields can be part of the sign up form for anonymous users. Given I am on site homepage And I follow "Log in" When I click on "Create new account" "link" And I expand all fieldsets Then I should not see "notvisible_field" And I should see "uservisible_field" And I should not see "everyonevisible_field" And I should see "teachervisible_field" @javascript Scenario: Visible custom profile fields can be part of the sign up form for guest users. Given I log in as "guest" And I am on site homepage And I follow "Log in" When I click on "Create new account" "link" And I expand all fieldsets Then I should not see "notvisible_field" And I should see "uservisible_field" And I should not see "everyonevisible_field" And I should see "teachervisible_field" @javascript Scenario: User with moodle/user:update but without moodle/user:viewalldetails or moodle/site:viewuseridentity can only update visible profile fields. Given the following "roles" exist: | name | shortname | description | archetype | | Update Users | updateusers | updateusers | | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/user:update | Allow | updateusers | System | | | moodle/site:viewuseridentity | Prohibit | updateusers | System | | And the following "users" exist: | username | firstname | lastname | email | | user_updateusers | updateusers | 1 | updateusers@example.com | And the following "role assigns" exist: | user | role | contextlevel | reference | | user_updateusers | updateusers | System | | And the following "course enrolments" exist: | user | course | role | | user_updateusers | C1 | editingteacher | And I log in as "user_updateusers" And I am on "Course 1" course homepage And I navigate to course participants And I follow "userwithinformation 1" Then I should see "everyonevisible_field" And I should see "everyonevisible_field_information" And I should not see "uservisible_field" And I should not see "uservisible_field_information" And I should not see "notvisible_field" And I should not see "notvisible_field_information" And I should not see "teachervisible_field" And I should not see "teachervisible_field_information" And I follow "Edit profile" And the following fields match these values: | everyonevisible_field | everyonevisible_field_information | And I should not see "uservisible_field" And I should not see "notvisible_field" And I should not see "teachervisible_field" @javascript Scenario: User with moodle/user:viewalldetails and moodle/site:viewuseridentity but without moodle/user:update can view all profile fields. Given the following "roles" exist: | name | shortname | description | archetype | | View All Details | viewalldetails | viewalldetails | | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/user:viewalldetails | Allow | viewalldetails | System | | And the following "users" exist: | username | firstname | lastname | email | | user_viewalldetails | viewalldetails | 1 | viewalldetails@example.com | And the following "role assigns" exist: | user | role | contextlevel | reference | | user_viewalldetails | viewalldetails | System | | And the following "course enrolments" exist: | user | course | role | | user_viewalldetails | C1 | editingteacher | And I log in as "user_viewalldetails" And I am on "Course 1" course homepage And I navigate to course participants And I follow "userwithinformation 1" Then I should see "everyonevisible_field" And I should see "everyonevisible_field_information" And I should see "uservisible_field" And I should see "uservisible_field_information" And I should see "notvisible_field" And I should see "notvisible_field_information" And I should see "teachervisible_field" And I should see "teachervisible_field_information" And I should not see "Edit profile" @javascript Scenario: User with moodle/user:viewalldetails and moodle/user:update and moodle/site:viewuseridentity capabilities can view and edit all profile fields. Given the following "roles" exist: | name | shortname | description | archetype | | View All Details and Update Users | viewalldetailsandupdateusers | viewalldetailsandupdateusers | | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/user:viewalldetails | Allow | viewalldetailsandupdateusers | System | | | moodle/user:update | Allow | viewalldetailsandupdateusers | System | | And the following "users" exist: | username | firstname | lastname | email | | user_viewalldetailsandupdateusers | viewalldetailsandupdateusers | 1 | viewalldetailsandupdateusers@example.com | And the following "role assigns" exist: | user | role | contextlevel | reference | | user_viewalldetailsandupdateusers | viewalldetailsandupdateusers | System | | And the following "course enrolments" exist: | user | course | role | | user_viewalldetailsandupdateusers | C1 | editingteacher | And I log in as "user_viewalldetailsandupdateusers" And I am on "Course 1" course homepage And I navigate to course participants And I follow "userwithinformation 1" Then I should see "everyonevisible_field" And I should see "everyonevisible_field_information" And I should see "uservisible_field" And I should see "uservisible_field_information" And I should see "notvisible_field" And I should see "notvisible_field_information" And I should see "teachervisible_field" And I should see "teachervisible_field_information" And I follow "Edit profile" And the following fields match these values: | everyonevisible_field | everyonevisible_field_information | | uservisible_field | uservisible_field_information | | notvisible_field | notvisible_field_information | | teachervisible_field | teachervisible_field_information | @javascript Scenario: Users can view and edit custom profile fields except those marked as not visible. Given I log in as "userwithinformation" And I follow "Profile" in the user menu Then I should see "everyonevisible_field" And I should see "everyonevisible_field_information" And I should see "uservisible_field" And I should see "uservisible_field_information" And I should see "teachervisible_field" And I should see "teachervisible_field_information" And I should not see "notvisible_field" And I should not see "notvisible_field_information" And I click on "Edit profile" "link" in the "region-main" "region" Then the following fields match these values: | everyonevisible_field | everyonevisible_field_information | | uservisible_field | uservisible_field_information | And I should not see "notvisible_field" And I should not see "notvisible_field_information" @javascript Scenario: Users can view but not edit custom profile fields when denied the edit own profile capability. Given the following "roles" exist: | name | shortname | description | archetype | | Deny editownprofile | denyeditownprofile | denyeditownprofile | | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/user:editownprofile | Prohibit | denyeditownprofile | System | | And the following "role assigns" exist: | user | role | contextlevel | reference | | userwithinformation | denyeditownprofile | System | | And I log in as "userwithinformation" And I follow "Profile" in the user menu Then I should see "everyonevisible_field" And I should see "everyonevisible_field_information" And I should see "uservisible_field" And I should see "uservisible_field_information" And I should see "teachervisible_field" And I should see "teachervisible_field_information" And I should not see "notvisible_field" And I should not see "notvisible_field_information" And I should not see "Edit profile" @javascript Scenario: User with parent permissions on other user context can view and edit all profile fields. Given the following "roles" exist: | name | shortname | description | archetype | | Parent | parent | parent | | And the following "users" exist: | username | firstname | lastname | email | | parent | Parent | user | parent@example.com | And the following "role assigns" exist: | user | role | contextlevel | reference | | parent | parent | User | userwithinformation | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/user:viewalldetails | Allow | parent | User | userwithinformation | | moodle/user:viewdetails | Allow | parent | User | userwithinformation | | moodle/user:editprofile | Allow | parent | User | userwithinformation | Given I log in as "admin" And I am on site homepage And I turn editing mode on And I add the "Mentees" block And I log out And I log in as "parent" And I am on site homepage When I follow "userwithinformation" Then I should see "everyonevisible_field" And I should see "everyonevisible_field_information" And I should see "uservisible_field" And I should see "uservisible_field_information" And I should see "teachervisible_field" And I should see "teachervisible_field_information" And I should not see "notvisible_field" And I should not see "notvisible_field_information" And I follow "Edit profile" And the following fields match these values: | everyonevisible_field | everyonevisible_field_information | | uservisible_field | uservisible_field_information | | teachervisible_field | teachervisible_field_information | @javascript Scenario: Menu profile field's default data works as expected when editing user profile Given the following "custom profile fields" exist: | datatype | shortname | name | visible | param1 | defaultdata | | menu | menufield | Menu field | 2 | OptA\nOptB\nOptC | OptB | And I log in as "userwithinformation" When I follow "Profile" in the user menu And I click on "Edit profile" "link" in the "region-main" "region" Then the following fields match these values: | Menu field | OptB | @javascript Scenario: Menu profile field successfully updated when editing user profile Given the following "custom profile fields" exist: | datatype | shortname | name | visible | param1 | | menu | menufield | Menu field | 2 | OptA\nOptB\nOptC | And I log in as "userwithinformation" When I follow "Profile" in the user menu And I click on "Edit profile" "link" in the "region-main" "region" And I set the following fields to these values: | Menu field | OptC | And I click on "Update profile" "button" Then I should see "OptC" tests/behat/enrol_cohort_list.feature 0000644 00000004354 15151162244 0014105 0 ustar 00 @core @core_user Feature: Viewing the list of cohorts to enrol in a course In order to ensure we only display the cohorts when applicable As a teacher I should only see the list of cohorts under some circumstances Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | @javascript @skip_chrome_zerosize Scenario: Check the teacher does not see the cohorts field without the proper capabilities Given the following "cohort" exists: | name | Test cohort name | | idnumber | 1337 | | description | Test cohort description | And I log in as "admin" And I set the following system permissions of "Teacher" role: | capability | permission | | moodle/cohort:manage | Prohibit | | moodle/cohort:view | Prohibit | And I log out And I am on the "Course 1" course page logged in as teacher1 And I navigate to course participants When I press "Enrol users" Then I should not see "Select cohorts" And I should not see "Enrol selected users and cohorts" @javascript Scenario: Check we show the cohorts field if there are some present Given the following "cohort" exists: | name | Test cohort name | | idnumber | 1337 | | description | Test cohort description | And I am on the "Course 1" course page logged in as teacher1 And I navigate to course participants When I press "Enrol users" Then I should see "Select cohorts" And I should see "Enrol selected users and cohorts" @javascript Scenario: Check we do not show the cohorts field if there are none present Given I am on the "Course 1" course page logged in as teacher1 And I navigate to course participants When I press "Enrol users" Then I should not see "Select cohorts" And I should not see "Enrol selected users and cohorts" tests/behat/view_preferences_page.feature 0000644 00000006421 15151162244 0014701 0 ustar 00 @core @core_user Feature: Access to preferences page In order to view the preferences page As a user I need global permissions to view the page. Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | manager1 | Manager | 1 | manager1@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | | parent | Parent | 1 | parent1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | | Course 2 | C2 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C1 | editingteacher | And the following "system role assigns" exist: | user | course | role | | manager1 | Acceptance test site | manager | Scenario: A student and teacher with normal permissions can not view another user's permissions page. Given I log in as "student1" And I am on "Course 1" course homepage And I navigate to course participants And I follow "Student 2" And I should not see "Preferences" in the "region-main" "region" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage When I navigate to course participants And I follow "Student 2" Then I should not see "Preferences" in the "region-main" "region" Scenario: Administrators and Managers can view another user's permissions page. Given I log in as "admin" And I am on "Course 1" course homepage And I navigate to course participants And I follow "Student 2" And I should see "Preferences" in the "region-main" "region" And I log out And I log in as "manager1" And I am on "Course 1" course homepage When I navigate to course participants And I follow "Student 2" Then I should see "Preferences" in the "region-main" "region" Scenario: A user with the appropriate permissions can view another user's permissions page. Given I log in as "admin" And I am on site homepage And I turn editing mode on And I add the "Mentees" block And I navigate to "Users > Permissions > Define roles" in site administration And I click on "Add a new role" "button" And I click on "Continue" "button" And I set the following fields to these values: | Short name | Parent | | Custom full name | Parent | | contextlevel30 | 1 | | moodle/user:editprofile | 1 | | moodle/user:viewalldetails | 1 | | moodle/user:viewuseractivitiesreport | 1 | | moodle/user:viewdetails | 1 | And I click on "Create this role" "button" And I am on the "student1" "user > profile" page And I click on "Preferences" "link" in the ".profile_tree" "css_element" And I follow "Assign roles relative to this user" And I follow "Parent" And I set the field "Potential users" to "Parent 1 (parent1@example.com)" And I click on "Add" "button" in the "#page-content" "css_element" And I log out And I log in as "parent" And I am on site homepage When I follow "Student 1" Then I should see "Preferences" in the "region-main" "region" tests/behat/set_email_display.feature 0000644 00000010123 15151162244 0014033 0 ustar 00 @core @core_user Feature: Set email display preference In order to control who can see my email address on my profile page As a student I need my email to be shown to only the user groups chosen Background: Given the following "users" exist: | username | firstname | lastname | email | maildisplay | | teacher1 | Teacher | 1 | teacher1@example.com | 2 | | studentp | Student | PEER | studentP@example.com | 2 | | studentn | Student | NONE | studentN@example.com | 0 | | studente | Student | EVERYONE | studentE@example.com | 1 | | studentm | Student | MEMBERS | studentM@example.com | 2 | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | status | timeend | | teacher1 | C1 | teacher | 0 | 0 | | studentp | C1 | student | 0 | 0 | | studentn | C1 | student | 0 | 0 | | studente | C1 | student | 0 | 0 | | studentm | C1 | student | 0 | 0 | @javascript Scenario: Student viewing own profile Given I log in as "studentp" When I follow "Profile" in the user menu Then I should see "studentP@example.com" And I should see "(Visible to other course participants)" @javascript Scenario: Student peer on the same course viewing profiles Given I log in as "studentp" And I am on "Course 1" course homepage And I navigate to course participants When I follow "Student NONE" Then I should not see "studentN@example.com" And I navigate to course participants When I follow "Student EVERYONE" Then I should see "studentE@example.com" And I navigate to course participants When I follow "Student MEMBERS" Then I should see "studentM@example.com" @javascript Scenario: Student viewing teacher email (whose maildisplay = MEMBERS) Given I log in as "studentp" And I am on "Course 1" course homepage And I navigate to course participants When I follow "Teacher 1" Then I should see "teacher1@example.com" @javascript Scenario: Teacher viewing student email, whilst site:showuseridentity = “email” Given the following config values are set as admin: | showuseridentity | email | Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants When I follow "Student NONE" Then I should see "studentN@example.com" And I navigate to course participants When I follow "Student MEMBERS" Then I should see "studentM@example.com" @javascript Scenario: Teacher viewing student email, whilst site:showuseridentity = “” Given I log in as "teacher1" And the following config values are set as admin: | showuseridentity | | And I am on "Course 1" course homepage And I navigate to course participants When I follow "Student NONE" Then I should not see "studentN@example.com" And I navigate to course participants When I follow "Student MEMBERS" Then I should see "studentM@example.com" @javascript Scenario: User can see user's email address settings on own profile Given I log in as "studentp" And I follow "Profile" in the user menu Then I should see "studentP@example.com" And I should see "(Visible to other course participants)" When I click on "Edit profile" "link" in the "region-main" "region" And I set the following fields to these values: | maildisplay | 0 | And I click on "Update profile" "button" Then I should see "(Hidden from everyone except users with appropriate permissions)" When I click on "Edit profile" "link" in the "region-main" "region" And I set the following fields to these values: | maildisplay | 1 | And I click on "Update profile" "button" Then I should see "(Visible to everyone)" tests/behat/view_full_profile.feature 0000644 00000016615 15151162244 0014074 0 ustar 00 @core @core_user Feature: Access to full profiles of users In order to allow visibility of full profiles As an admin I need to set global permission or disable forceloginforprofiles Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | format | groupmode | | Course 1 | C1 | topics | 0 | | Course 2 | C2 | topics | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | | student1 | C2 | student | | student3 | C2 | student | And the following config values are set as admin: | messaging | 1 | Scenario: Viewing full profiles with default settings When I log in as "student1" # Another student's full profile is visible And I am on "Course 1" course homepage And I navigate to course participants And I follow "Student 2" Then I should see "Full profile" # Teacher's full profile is visible And I am on "Course 1" course homepage And I navigate to course participants And I follow "Teacher 1" And I follow "Full profile" And I should see "First access to site" # Own full profile is visible And I am on "Course 1" course homepage And I navigate to course participants And I click on "Student 1" "link" in the "#participants" "css_element" And I follow "Full profile" And I should see "First access to site" Scenario: Viewing full profiles with forceloginforprofiles off Given the following config values are set as admin: | forceloginforprofiles | 0 | When I log in as "student1" And I am on "Course 1" course homepage And I navigate to course participants And I follow "Student 2" And I follow "Full profile" Then I should see "First access to site" Scenario: Viewing full profiles with global permission Given I log in as "admin" And I set the following system permissions of "Authenticated user" role: | moodle/user:viewdetails | Allow | And I log out When I log in as "student1" And I am on "Course 1" course homepage And I navigate to course participants And I follow "Student 2" And I follow "Full profile" Then I should see "First access to site" Scenario: Viewing full profiles of students as a teacher When I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants And I follow "Student 1" And I follow "Full profile" Then I should see "First access to site" Scenario: Viewing own full profile Given I log in as "student1" When I follow "Profile" in the user menu Then I should see "First access to site" Scenario: View only shared groups in a course with separate groups forced Given the following "groups" exist: | name | course | idnumber | | Group 1 | C2 | G1 | | Group 2 | C2 | G2 | And the following "group members" exist: | user | group | | student1 | G1 | | student3 | G2 | | teacher1 | G1 | | teacher1 | G2 | When I log in as "student3" And I am on "Course 2" course homepage And I navigate to course participants And I follow "Teacher 1" Then I should see "Group 2" And I should not see "Group 1" Scenario: View all groups in a course with visible groups Given the following "groups" exist: | name | course | idnumber | | Group 1 | C1 | G1 | | Group 2 | C1 | G2 | And the following "group members" exist: | user | group | | student1 | G1 | | student2 | G2 | | teacher1 | G1 | | teacher1 | G2 | When I log in as "student1" And I am on "Course 1" course homepage And I navigate to course participants And I follow "Teacher 1" Then I should see "Group 1" And I should see "Group 2" @javascript Scenario: Viewing full profiles of someone with the course contact role Given I log in as "admin" And I navigate to "Appearance > Courses" in site administration And I set the following fields to these values: | Course creator | 1 | And I press "Save changes" And I navigate to "Users > Permissions > Assign system roles" in site administration And I follow "Course creator" And I click on "//div[@class='userselector']/descendant::option[contains(., 'Student 3')]" "xpath_element" And I press "Add" And I log out # Message search will not return a course contact unless the searcher shares a course with them, # or site-wide messaging is enabled ($CFG->messagingallusers). When I log in as "student1" And I open messaging And I search for "Student 3" in messaging Then I should see "Student 3" And I log out When I log in as "student2" And I open messaging And I search for "Student 3" in messaging Then I should see "No results" @javascript Scenario: View full profiles of someone in the same group in a course with separate groups. Given I log in as "admin" And I am on "Course 1" course homepage And I navigate to "Settings" in current page administration And I set the following fields to these values: | Group mode | Separate groups | | Force group mode | Yes | And I press "Save and display" And I log out And the following "message contacts" exist: | user | contact | | student1 | student2 | When I log in as "student1" And I view the "Student 2" contact in the message area And I should not see "First access to site" And I should see "The details of this user are not available to you" And I log out And I log in as "admin" And I am on the "Course 1" "groups" page And I press "Create group" And I set the following fields to these values: | Group name | Group 1 | And I press "Save changes" And I add "Student 1 (student1@example.com)" user to "Group 1" group members And I add "Student 2 (student2@example.com)" user to "Group 1" group members And I log out And I log in as "student1" And I view the "Student 2" contact in the message area Then I should see "First access to site" @javascript Scenario: Accessibility, users can not click on profile image when on user's profile page. Given I log in as "admin" And I am on "Course 1" course homepage When I navigate to course participants Then "//img[contains(@class, 'userpicture')]" "xpath_element" should exist And "//a/child::img[contains(@class, 'userpicture')]" "xpath_element" should exist When I follow "Teacher 1" Then I should see "Teacher 1" And "//img[contains(@class, 'userpicture')]" "xpath_element" should exist And "//a/child::img[contains(@class, 'userpicture')]" "xpath_element" should not exist When I follow "Full profile" And I should see "Teacher 1" Then "//img[contains(@class, 'userpicture')]" "xpath_element" should exist And "//a/child::img[contains(@class, 'userpicture')]" "xpath_element" should not exist tests/behat/filter_participants_showall.feature 0000644 00000015355 15151162244 0016157 0 ustar 00 @core @core_user Feature: Course participants can be filtered to display all the users In order to filter the list of course participants As a user I need to visit the course participants page, apply the appropriate filters and show all users per page Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | | Course 2 | C2 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | | student4 | Student | 4 | student4@example.com | | student5 | Student | 5 | student5@example.com | | student6 | Student | 6 | student6@example.com | | student7 | Student | 7 | student7@example.com | | student8 | Student | 8 | student8@example.com | | student9 | Student | 9 | student9@example.com | | student10 | Student | 10 | student10@example.com | | student11 | Student | 11 | student11@example.com | | student12 | Student | 12 | student12@example.com | | student13 | Student | 13 | student13@example.com | | student14 | Student | 14 | student14@example.com | | student15 | Student | 15 | student15@example.com | | student16 | Student | 16 | student16@example.com | | student17 | Student | 17 | student17@example.com | | student18 | Student | 18 | student18@example.com | | student19 | Student | 19 | student19@example.com | | student20 | Student | 20 | student20@example.com | | student21 | Student | 21 | student21@example.com | | student22 | Student | 22 | student22@example.com | | student23 | Student | 23 | student23@example.com | | student24 | Student | 24 | student24@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | status | timeend | | student1 | C1 | student | 0 | | | student2 | C1 | student | 0 | | | student3 | C1 | student | 0 | | | student4 | C1 | student | 0 | | | student5 | C1 | student | 0 | | | student6 | C1 | student | 0 | | | student7 | C1 | student | 0 | | | student8 | C1 | student | 0 | | | student9 | C1 | student | 0 | | | student10 | C1 | student | 0 | | | student11 | C1 | student | 0 | | | student12 | C1 | student | 0 | | | student13 | C1 | student | 0 | | | student14 | C1 | student | 0 | | | student15 | C1 | student | 0 | | | student16 | C1 | student | 0 | | | student17 | C1 | student | 0 | | | student18 | C1 | student | 0 | | | student19 | C1 | student | 0 | | | student20 | C1 | student | 0 | | | student21 | C1 | student | 0 | | | student22 | C1 | student | 0 | | | student23 | C1 | student | 0 | | | student24 | C1 | student | 1 | | | student1 | C2 | student | 0 | | | student2 | C2 | student | 0 | | | student3 | C2 | student | 0 | | | teacher1 | C1 | editingteacher | 0 | | | teacher1 | C2 | editingteacher | 0 | | @javascript Scenario: Show all users in a course that match a single filter value Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants And I set the field "Match" in the "Filter 1" "fieldset" to "All" And I set the field "type" in the "Filter 1" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student" When I click on "Apply filters" "button" Then I should see "24 participants found" And I should see "Show all 24" And I should not see "Show 20 per page" And I should not see "of the following" And I click on "Show all 24" "link" And I should see "Show 20 per page" And I should not see "Show all 24" @javascript Scenario: Show all users as a student Given I log in as "student1" And I am on "Course 1" course homepage And I navigate to course participants And I set the field "Match" in the "Filter 1" "fieldset" to "All" And I set the field "type" in the "Filter 1" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student" When I click on "Apply filters" "button" Then I should see "23 participants found" And I should see "Show all 23" And I should not see "Show 20 per page" And I click on "Show all 23" "link" And I should see "Show 20 per page" And I should not see "Show all 23" @javascript Scenario: Apply one value for more than one filter and show all matching users Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants And I click on "Add condition" "button" And I set the field "Match" to "All" And I set the field "Match" in the "Filter 1" "fieldset" to "Any" And I set the field "type" in the "Filter 1" "fieldset" to "Roles" And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Student" And I set the field "Match" in the "Filter 2" "fieldset" to "Any" And I set the field "type" in the "Filter 2" "fieldset" to "Status" And I set the field "Type or select..." in the "Filter 2" "fieldset" to "Active" When I click on "Apply filters" "button" And I click on "Show all 23" "link" Then I should see "23 participants found" And I should see "Show 20 per page" And I should see "of the following" And I should see "Student 1" And I should not see "Student 24" And I should not see "Show all 23" tests/behat/addnewuser.feature 0000644 00000001325 15151162244 0012511 0 ustar 00 @core @core_user Feature: Manually create a user In order create a user properly As an admin I need to be able to add new users and edit their fields. Scenario: Change default language for a new user Given I log in as "admin" When I navigate to "Users > Accounts > Add a new user" in site administration Then I should see "Preferred language" Scenario: Language not displayed when editing an existing user Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | When I am on the "student1" "user > editing" page logged in as "admin" Then I should not see "Preferred language" tests/behat/reset_page.feature 0000644 00000001614 15151162244 0012467 0 ustar 00 @core @core_user Feature: Reset my profile page to default In order to remove customisations from my profile page As a user I need to reset my profile page Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | And I log in as "admin" And I follow "View profile" Scenario: Add blocks to page and reset When I turn editing mode on And I add the "Latest announcements" block And I press "Reset page to default" Then I should not see "Latest announcements" tests/search/search_test.php 0000644 00000027046 15151162244 0012202 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Course global search unit tests. * * @package core_user * @copyright 2016 Devang Gaur {@link http://www.devanggaur.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\search; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); /** * Provides the unit tests for course global search. * * @package core * @copyright 2016 Devang Gaur {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class search_test extends \advanced_testcase { /** * @var string Area id */ protected $userareaid = null; public function setUp(): void { $this->resetAfterTest(true); set_config('enableglobalsearch', true); $this->userareaid = \core_search\manager::generate_areaid('core_user', 'user'); // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this. $search = \testable_core_search::instance(); } /** * Indexing users contents. * * @return void */ public function test_users_indexing() { global $SITE; // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->userareaid); $this->assertInstanceOf('\core_user\search\user', $searcharea); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); // All records. // Recordset will produce 4 user records: // Guest User, Admin User and two above generated users. $recordset = $searcharea->get_recordset_by_timestamp(0); $this->assertTrue($recordset->valid()); $nrecords = 0; foreach ($recordset as $record) { $this->assertInstanceOf('stdClass', $record); $doc = $searcharea->get_document($record); $this->assertInstanceOf('\core_search\document', $doc); $nrecords++; } // If there would be an error/failure in the foreach above the recordset would be closed on shutdown. $recordset->close(); $this->assertEquals(4, $nrecords); // The +2 is to prevent race conditions. $recordset = $searcharea->get_recordset_by_timestamp(time() + 2); // No new records. $this->assertFalse($recordset->valid()); $recordset->close(); // Context support; first, try an unsupported context type. $coursecontext = \context_course::instance($SITE->id); $this->assertNull($searcharea->get_document_recordset(0, $coursecontext)); // Try a specific user, will only return 1 record (that user). $rs = $searcharea->get_document_recordset(0, \context_user::instance($user1->id)); $this->assertEquals(1, iterator_count($rs)); $rs->close(); } /** * Document contents. * * @return void */ public function test_users_document() { // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->userareaid); $this->assertInstanceOf('\core_user\search\user', $searcharea); $user = self::getDataGenerator()->create_user(); $doc = $searcharea->get_document($user); $this->assertInstanceOf('\core_search\document', $doc); $this->assertEquals($user->id, $doc->get('itemid')); $this->assertEquals($this->userareaid . '-' . $user->id, $doc->get('id')); $this->assertEquals(SITEID, $doc->get('courseid')); $this->assertFalse($doc->is_set('userid')); $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid')); $this->assertEquals(content_to_text(fullname($user), false), $searcharea->get_document_display_title($doc)); $this->assertEquals(content_to_text($user->description, $user->descriptionformat), $doc->get('content')); } /** * Document accesses. * * @return void */ public function test_users_access() { global $CFG; // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->userareaid); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $user4 = self::getDataGenerator()->create_user(); $user5 = self::getDataGenerator()->create_user(); $user5->id = 0; // Visitor (not guest). $deleteduser = self::getDataGenerator()->create_user(array('deleted' => 1)); $unconfirmeduser = self::getDataGenerator()->create_user(array('confirmed' => 0)); $suspendeduser = self::getDataGenerator()->create_user(array('suspended' => 1)); $course1 = self::getDataGenerator()->create_course(); $course2 = self::getDataGenerator()->create_course(); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student'); $this->getDataGenerator()->enrol_user($user3->id, $course2->id, 'student'); $this->getDataGenerator()->enrol_user($user4->id, $course2->id, 'student'); $this->getDataGenerator()->enrol_user($suspendeduser->id, $course1->id, 'student'); $this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $group1->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user3->id, 'groupid' => $group1->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user4->id, 'groupid' => $group2->id)); $this->setAdminUser(); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user1->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user3->id)); $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access($deleteduser->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($unconfirmeduser->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($suspendeduser->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access(2)); $this->setUser($user1); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user1->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user3->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user4->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access(1));// Guest user can't be accessed. $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access(2));// Admin user can't be accessed. $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-123)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($unconfirmeduser->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($suspendeduser->id)); $this->setUser($user2); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user1->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user3->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user4->id)); $this->setUser($user3); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user1->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user3->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($suspendeduser->id)); $this->setGuestUser(); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user1->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user2->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user3->id)); $CFG->forceloginforprofiles = 0; $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user1->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user3->id)); $this->setUser($user5); $CFG->forceloginforprofiles = 1; $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user1->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user2->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($user3->id)); $CFG->forceloginforprofiles = 0; $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user1->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user2->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($user3->id)); } /** * Test document icon. */ public function test_get_doc_icon() { $searcharea = \core_search\manager::get_search_area($this->userareaid); $user = self::getDataGenerator()->create_user(); $doc = $searcharea->get_document($user); $result = $searcharea->get_doc_icon($doc); $this->assertEquals('i/user', $result->get_name()); $this->assertEquals('moodle', $result->get_component()); } /** * Test assigned search categories. */ public function test_get_category_names() { $searcharea = \core_search\manager::get_search_area($this->userareaid); $expected = ['core-users']; $this->assertEquals($expected, $searcharea->get_category_names()); } } tests/coverage.php 0000644 00000002117 15151162244 0010214 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Coverage information for the core_user subsystem. * * @copyright 2021 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ return new class extends phpunit_coverage_info { /** @var array The list of files relative to the plugin root to include in coverage generation. */ protected $includelistfiles = [ 'editlib.php', ]; }; tests/editlib_test.php 0000644 00000012146 15151162244 0011077 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/user/editlib.php'); /** * Unit tests for user editlib api. * * @package core_user * @category test * @copyright 2013 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class editlib_test extends \advanced_testcase { /** * Test that the required fields are returned in the correct order. */ function test_useredit_get_required_name_fields() { global $CFG; // Back up config settings for restore later. $originalcfg = new \stdClass(); $originalcfg->fullnamedisplay = $CFG->fullnamedisplay; $CFG->fullnamedisplay = 'language'; $expectedresult = array(5 => 'firstname', 21 => 'lastname'); $this->assertEquals(useredit_get_required_name_fields(), $expectedresult); $CFG->fullnamedisplay = 'firstname'; $expectedresult = array(5 => 'firstname', 21 => 'lastname'); $this->assertEquals(useredit_get_required_name_fields(), $expectedresult); $CFG->fullnamedisplay = 'lastname firstname'; $expectedresult = array('lastname', 9 => 'firstname'); $this->assertEquals(useredit_get_required_name_fields(), $expectedresult); $CFG->fullnamedisplay = 'firstnamephonetic lastnamephonetic'; $expectedresult = array(5 => 'firstname', 21 => 'lastname'); $this->assertEquals(useredit_get_required_name_fields(), $expectedresult); // Tidy up after we finish testing. $CFG->fullnamedisplay = $originalcfg->fullnamedisplay; } /** * Test that the enabled fields are returned in the correct order. */ function test_useredit_get_enabled_name_fields() { global $CFG; // Back up config settings for restore later. $originalcfg = new \stdClass(); $originalcfg->fullnamedisplay = $CFG->fullnamedisplay; $CFG->fullnamedisplay = 'language'; $expectedresult = array(); $this->assertEquals(useredit_get_enabled_name_fields(), $expectedresult); $CFG->fullnamedisplay = 'firstname lastname firstnamephonetic'; $expectedresult = array(19 => 'firstnamephonetic'); $this->assertEquals(useredit_get_enabled_name_fields(), $expectedresult); $CFG->fullnamedisplay = 'firstnamephonetic, lastname lastnamephonetic (alternatename)'; $expectedresult = array('firstnamephonetic', 28 => 'lastnamephonetic', 46 => 'alternatename'); $this->assertEquals(useredit_get_enabled_name_fields(), $expectedresult); $CFG->fullnamedisplay = 'firstnamephonetic lastnamephonetic alternatename middlename'; $expectedresult = array('firstnamephonetic', 18 => 'lastnamephonetic', 35 => 'alternatename', 49 => 'middlename'); $this->assertEquals(useredit_get_enabled_name_fields(), $expectedresult); // Tidy up after we finish testing. $CFG->fullnamedisplay = $originalcfg->fullnamedisplay; } /** * Test that the disabled fields are returned. */ function test_useredit_get_disabled_name_fields() { global $CFG; // Back up config settings for restore later. $originalcfg = new \stdClass(); $originalcfg->fullnamedisplay = $CFG->fullnamedisplay; $CFG->fullnamedisplay = 'language'; $expectedresult = array('firstnamephonetic' => 'firstnamephonetic', 'lastnamephonetic' => 'lastnamephonetic', 'middlename' => 'middlename', 'alternatename' => 'alternatename'); $this->assertEquals(useredit_get_disabled_name_fields(), $expectedresult); $CFG->fullnamedisplay = 'firstname lastname firstnamephonetic'; $expectedresult = array('lastnamephonetic' => 'lastnamephonetic', 'middlename' => 'middlename', 'alternatename' => 'alternatename'); $this->assertEquals(useredit_get_disabled_name_fields(), $expectedresult); $CFG->fullnamedisplay = 'firstnamephonetic, lastname lastnamephonetic (alternatename)'; $expectedresult = array('middlename' => 'middlename'); $this->assertEquals(useredit_get_disabled_name_fields(), $expectedresult); $CFG->fullnamedisplay = 'firstnamephonetic lastnamephonetic alternatename middlename'; $expectedresult = array(); $this->assertEquals(useredit_get_disabled_name_fields(), $expectedresult); // Tidy up after we finish testing. $CFG->fullnamedisplay = $originalcfg->fullnamedisplay; } } tests/userlib_test.php 0000644 00000134352 15151162244 0011134 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/user/lib.php'); /** * Unit tests for user lib api. * * @package core_user * @category test * @copyright 2013 Rajesh Taneja <rajesh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class userlib_test extends \advanced_testcase { /** * Test user_get_user_details_courses */ public function test_user_get_user_details_courses() { global $DB; $this->resetAfterTest(); // Create user and modify user profile. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course1->id); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); role_assign($teacherrole->id, $user1->id, $coursecontext->id); role_assign($teacherrole->id, $user2->id, $coursecontext->id); accesslib_clear_all_caches_for_unit_testing(); // Get user2 details as a user with super system capabilities. $result = user_get_user_details_courses($user2); $this->assertEquals($user2->id, $result['id']); $this->assertEquals(fullname($user2), $result['fullname']); $this->assertEquals($course1->id, $result['enrolledcourses'][0]['id']); $this->setUser($user1); // Get user2 details as a user who can only see this user in a course. $result = user_get_user_details_courses($user2); $this->assertEquals($user2->id, $result['id']); $this->assertEquals(fullname($user2), $result['fullname']); $this->assertEquals($course1->id, $result['enrolledcourses'][0]['id']); // Get user2 details as a user who doesn't share any course with user2. $this->setUser($user3); $result = user_get_user_details_courses($user2); $this->assertNull($result); } /** * Verify return when course groupmode set to 'no groups'. */ public function test_user_get_user_details_courses_groupmode_nogroups() { $this->resetAfterTest(); // Enrol 2 users into a course with groupmode set to 'no groups'. // Profiles should be visible. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course((object) ['groupmode' => 0]); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $this->setUser($user1); $userdetails = user_get_user_details_courses($user2); $this->assertIsArray($userdetails); $this->assertEquals($user2->id, $userdetails['id']); } /** * Verify return when course groupmode set to 'separate groups'. */ public function test_user_get_user_details_courses_groupmode_separate() { $this->resetAfterTest(); // Enrol 2 users into a course with groupmode set to 'separate groups'. // The users are not in any groups, so profiles should be hidden (same as if they were in separate groups). $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course((object) ['groupmode' => 1]); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $this->setUser($user1); $this->assertNull(user_get_user_details_courses($user2)); } /** * Verify return when course groupmode set to 'visible groups'. */ public function test_user_get_user_details_courses_groupmode_visible() { $this->resetAfterTest(); // Enrol 2 users into a course with groupmode set to 'visible groups'. // The users are not in any groups, and profiles should be visible because of the groupmode. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course((object) ['groupmode' => 2]); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); $this->setUser($user1); $userdetails = user_get_user_details_courses($user2); $this->assertIsArray($userdetails); $this->assertEquals($user2->id, $userdetails['id']); } /** * Tests that the user fields returned by the method can be limited. * * @covers ::user_get_user_details_courses */ public function test_user_get_user_details_courses_limit_return() { $this->resetAfterTest(); // Setup some data. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id); // Calculate the minimum fields that can be returned. $namefields = \core_user\fields::for_name()->get_required_fields(); $fields = array_intersect($namefields, user_get_default_fields()); $minimaluser = (object) [ 'id' => $user2->id, 'deleted' => $user2->deleted, ]; foreach ($namefields as $field) { $minimaluser->$field = $user2->$field; } $this->setUser($user1); $fulldetails = user_get_user_details_courses($user2); $limiteddetails = user_get_user_details_courses($minimaluser, $fields); $this->assertIsArray($fulldetails); $this->assertIsArray($limiteddetails); $this->assertEquals($user2->id, $fulldetails['id']); $this->assertEquals($user2->id, $limiteddetails['id']); // Test that less data was returned when using a filter. $fullcount = count($fulldetails); $limitedcount = count($limiteddetails); $this->assertLessThan($fullcount, $limitedcount); $this->assertNotEquals($fulldetails, $limiteddetails); } /** * Test user_update_user. */ public function test_user_update_user() { global $DB; $this->resetAfterTest(); // Create user and modify user profile. $user = $this->getDataGenerator()->create_user(); $user->firstname = 'Test'; $user->password = 'M00dLe@T'; // Update user and capture event. $sink = $this->redirectEvents(); user_update_user($user); $events = $sink->get_events(); $sink->close(); $event = array_pop($events); // Test updated value. $dbuser = $DB->get_record('user', array('id' => $user->id)); $this->assertSame($user->firstname, $dbuser->firstname); $this->assertNotSame('M00dLe@T', $dbuser->password); // Test event. $this->assertInstanceOf('\core\event\user_updated', $event); $this->assertSame($user->id, $event->objectid); $this->assertSame('user_updated', $event->get_legacy_eventname()); $this->assertEventLegacyData($dbuser, $event); $this->assertEquals(\context_user::instance($user->id), $event->get_context()); $expectedlogdata = array(SITEID, 'user', 'update', 'view.php?id='.$user->id, ''); $this->assertEventLegacyLogData($expectedlogdata, $event); // Update user with no password update. $password = $user->password = hash_internal_user_password('M00dLe@T'); user_update_user($user, false); $dbuser = $DB->get_record('user', array('id' => $user->id)); $this->assertSame($password, $dbuser->password); // Verify event is not triggred by user_update_user when needed. $sink = $this->redirectEvents(); user_update_user($user, false, false); $events = $sink->get_events(); $sink->close(); $this->assertCount(0, $events); // With password, there should be 1 event. $sink = $this->redirectEvents(); user_update_user($user, true, false); $events = $sink->get_events(); $sink->close(); $this->assertCount(1, $events); $event = array_pop($events); $this->assertInstanceOf('\core\event\user_password_updated', $event); // Test user data validation. $user->username = 'johndoe123'; $user->auth = 'shibolth'; $user->country = 'WW'; $user->lang = 'xy'; $user->theme = 'somewrongthemename'; $user->timezone = '30.5'; $debugmessages = $this->getDebuggingMessages(); user_update_user($user, true, false); $this->assertDebuggingCalledCount(5, $debugmessages); // Now, with valid user data. $user->username = 'johndoe321'; $user->auth = 'shibboleth'; $user->country = 'AU'; $user->lang = 'en'; $user->theme = 'classic'; $user->timezone = 'Australia/Perth'; user_update_user($user, true, false); $this->assertDebuggingNotCalled(); } /** * Test create_users. */ public function test_create_users() { global $DB; $this->resetAfterTest(); $user = array( 'username' => 'usernametest1', 'password' => 'Moodle2012!', 'idnumber' => 'idnumbertest1', 'firstname' => 'First Name User Test 1', 'lastname' => 'Last Name User Test 1', 'middlename' => 'Middle Name User Test 1', 'lastnamephonetic' => '最後のお名前のテスト一号', 'firstnamephonetic' => 'お名前のテスト一号', 'alternatename' => 'Alternate Name User Test 1', 'email' => 'usertest1@example.com', 'description' => 'This is a description for user 1', 'city' => 'Perth', 'country' => 'AU' ); // Create user and capture event. $sink = $this->redirectEvents(); $user['id'] = user_create_user($user); $events = $sink->get_events(); $sink->close(); $event = array_pop($events); // Test user info in DB. $dbuser = $DB->get_record('user', array('id' => $user['id'])); $this->assertEquals($dbuser->username, $user['username']); $this->assertEquals($dbuser->idnumber, $user['idnumber']); $this->assertEquals($dbuser->firstname, $user['firstname']); $this->assertEquals($dbuser->lastname, $user['lastname']); $this->assertEquals($dbuser->email, $user['email']); $this->assertEquals($dbuser->description, $user['description']); $this->assertEquals($dbuser->city, $user['city']); $this->assertEquals($dbuser->country, $user['country']); // Test event. $this->assertInstanceOf('\core\event\user_created', $event); $this->assertEquals($user['id'], $event->objectid); $this->assertEquals('user_created', $event->get_legacy_eventname()); $this->assertEquals(\context_user::instance($user['id']), $event->get_context()); $this->assertEventLegacyData($dbuser, $event); $expectedlogdata = array(SITEID, 'user', 'add', '/view.php?id='.$event->objectid, fullname($dbuser)); $this->assertEventLegacyLogData($expectedlogdata, $event); // Verify event is not triggred by user_create_user when needed. $user = array('username' => 'usernametest2'); // Create another user. $sink = $this->redirectEvents(); user_create_user($user, true, false); $events = $sink->get_events(); $sink->close(); $this->assertCount(0, $events); // Test user data validation, first some invalid data. $user['username'] = 'johndoe123'; $user['auth'] = 'shibolth'; $user['country'] = 'WW'; $user['lang'] = 'xy'; $user['theme'] = 'somewrongthemename'; $user['timezone'] = '-30.5'; $debugmessages = $this->getDebuggingMessages(); $user['id'] = user_create_user($user, true, false); $this->assertDebuggingCalledCount(5, $debugmessages); $dbuser = $DB->get_record('user', array('id' => $user['id'])); $this->assertEquals($dbuser->country, 0); $this->assertEquals($dbuser->lang, 'en'); $this->assertEquals($dbuser->timezone, ''); // Now, with valid user data. $user['username'] = 'johndoe321'; $user['auth'] = 'shibboleth'; $user['country'] = 'AU'; $user['lang'] = 'en'; $user['theme'] = 'classic'; $user['timezone'] = 'Australia/Perth'; user_create_user($user, true, false); $this->assertDebuggingNotCalled(); } /** * Test that creating users populates default values * * @covers ::user_create_user */ public function test_user_create_user_default_values(): void { global $CFG; $this->resetAfterTest(); // Update default values for city/country (both initially empty). set_config('defaultcity', 'Nadi'); set_config('country', 'FJ'); $userid = user_create_user((object) [ 'username' => 'newuser', ], false, false); $user = \core_user::get_user($userid); $this->assertEquals($CFG->calendartype, $user->calendartype); $this->assertEquals($CFG->defaultpreference_maildisplay, $user->maildisplay); $this->assertEquals($CFG->defaultpreference_mailformat, $user->mailformat); $this->assertEquals($CFG->defaultpreference_maildigest, $user->maildigest); $this->assertEquals($CFG->defaultpreference_autosubscribe, $user->autosubscribe); $this->assertEquals($CFG->defaultpreference_trackforums, $user->trackforums); $this->assertEquals($CFG->lang, $user->lang); $this->assertEquals($CFG->defaultcity, $user->city); $this->assertEquals($CFG->country, $user->country); } /** * Test that {@link user_create_user()} throws exception when invalid username is provided. * * @dataProvider data_create_user_invalid_username * @param string $username Invalid username * @param string $expectmessage Expected exception message */ public function test_create_user_invalid_username($username, $expectmessage) { global $CFG; $this->resetAfterTest(); $CFG->extendedusernamechars = false; $user = [ 'username' => $username, ]; $this->expectException('moodle_exception'); $this->expectExceptionMessage($expectmessage); user_create_user($user); } /** * Data provider for {@link self::test_create_user_invalid_username()}. * * @return array */ public function data_create_user_invalid_username() { return [ 'empty_string' => [ '', 'The username cannot be blank', ], 'only_whitespace' => [ "\t\t \t\n ", 'The username cannot be blank', ], 'lower_case' => [ 'Mudrd8mz', 'The username must be in lower case', ], 'extended_chars' => [ 'dmudrák', 'The given username contains invalid characters', ], ]; } /** * Test function user_count_login_failures(). */ public function test_user_count_login_failures() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->assertEquals(0, get_user_preferences('login_failed_count_since_success', 0, $user)); for ($i = 0; $i < 10; $i++) { login_attempt_failed($user); } $this->assertEquals(10, get_user_preferences('login_failed_count_since_success', 0, $user)); $count = user_count_login_failures($user); // Reset count. $this->assertEquals(10, $count); $this->assertEquals(0, get_user_preferences('login_failed_count_since_success', 0, $user)); for ($i = 0; $i < 10; $i++) { login_attempt_failed($user); } $this->assertEquals(10, get_user_preferences('login_failed_count_since_success', 0, $user)); $count = user_count_login_failures($user, false); // Do not reset count. $this->assertEquals(10, $count); $this->assertEquals(10, get_user_preferences('login_failed_count_since_success', 0, $user)); } /** * Test function user_add_password_history(). */ public function test_user_add_password_history() { global $DB; $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $DB->delete_records('user_password_history', array()); set_config('passwordreuselimit', 0); user_add_password_history($user1->id, 'pokus'); $this->assertEquals(0, $DB->count_records('user_password_history')); // Test adding and discarding of old. set_config('passwordreuselimit', 3); user_add_password_history($user1->id, 'pokus'); $this->assertEquals(1, $DB->count_records('user_password_history')); $this->assertEquals(1, $DB->count_records('user_password_history', array('userid' => $user1->id))); user_add_password_history($user1->id, 'pokus2'); user_add_password_history($user1->id, 'pokus3'); user_add_password_history($user1->id, 'pokus4'); $this->assertEquals(3, $DB->count_records('user_password_history')); $this->assertEquals(3, $DB->count_records('user_password_history', array('userid' => $user1->id))); user_add_password_history($user2->id, 'pokus1'); $this->assertEquals(4, $DB->count_records('user_password_history')); $this->assertEquals(3, $DB->count_records('user_password_history', array('userid' => $user1->id))); $this->assertEquals(1, $DB->count_records('user_password_history', array('userid' => $user2->id))); user_add_password_history($user2->id, 'pokus2'); user_add_password_history($user2->id, 'pokus3'); $this->assertEquals(3, $DB->count_records('user_password_history', array('userid' => $user2->id))); $ids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC')); user_add_password_history($user2->id, 'pokus4'); $this->assertEquals(3, $DB->count_records('user_password_history', array('userid' => $user2->id))); $newids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC')); $removed = array_shift($ids); $added = array_pop($newids); $this->assertSame($ids, $newids); $this->assertGreaterThan($removed, $added); // Test disabling prevents changes. set_config('passwordreuselimit', 0); $this->assertEquals(6, $DB->count_records('user_password_history')); $ids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC')); user_add_password_history($user2->id, 'pokus5'); user_add_password_history($user3->id, 'pokus1'); $newids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC')); $this->assertSame($ids, $newids); $this->assertEquals(6, $DB->count_records('user_password_history')); set_config('passwordreuselimit', -1); $ids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC')); user_add_password_history($user2->id, 'pokus6'); user_add_password_history($user3->id, 'pokus6'); $newids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC')); $this->assertSame($ids, $newids); $this->assertEquals(6, $DB->count_records('user_password_history')); } /** * Test function user_add_password_history(). */ public function test_user_is_previously_used_password() { global $DB; $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $DB->delete_records('user_password_history', array()); set_config('passwordreuselimit', 0); user_add_password_history($user1->id, 'pokus'); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus')); set_config('passwordreuselimit', 3); user_add_password_history($user2->id, 'pokus1'); user_add_password_history($user2->id, 'pokus2'); user_add_password_history($user1->id, 'pokus1'); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus1')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus2')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus3')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus4')); user_add_password_history($user1->id, 'pokus2'); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus1')); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus2')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus3')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus4')); user_add_password_history($user1->id, 'pokus3'); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus1')); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus2')); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus3')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus4')); user_add_password_history($user1->id, 'pokus4'); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus1')); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus2')); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus3')); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus4')); set_config('passwordreuselimit', 2); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus1')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus2')); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus3')); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus4')); set_config('passwordreuselimit', 3); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus1')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus2')); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus3')); $this->assertTrue(user_is_previously_used_password($user1->id, 'pokus4')); set_config('passwordreuselimit', 0); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus1')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus2')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus3')); $this->assertFalse(user_is_previously_used_password($user1->id, 'pokus4')); } /** * Test that password history is deleted together with user. */ public function test_delete_of_hashes_on_user_delete() { global $DB; $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $DB->delete_records('user_password_history', array()); set_config('passwordreuselimit', 3); user_add_password_history($user1->id, 'pokus'); user_add_password_history($user2->id, 'pokus1'); user_add_password_history($user2->id, 'pokus2'); $this->assertEquals(3, $DB->count_records('user_password_history')); $this->assertEquals(1, $DB->count_records('user_password_history', array('userid' => $user1->id))); $this->assertEquals(2, $DB->count_records('user_password_history', array('userid' => $user2->id))); delete_user($user2); $this->assertEquals(1, $DB->count_records('user_password_history')); $this->assertEquals(1, $DB->count_records('user_password_history', array('userid' => $user1->id))); $this->assertEquals(0, $DB->count_records('user_password_history', array('userid' => $user2->id))); } /** * Test user_list_view function */ public function test_user_list_view() { $this->resetAfterTest(); // Course without sections. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $this->setAdminUser(); // Redirect events to the sink, so we can recover them later. $sink = $this->redirectEvents(); user_list_view($course, $context); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Check the event details are correct. $this->assertInstanceOf('\core\event\user_list_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEquals($course->shortname, $event->other['courseshortname']); $this->assertEquals($course->fullname, $event->other['coursefullname']); } /** * Test setting the user menu avatar size. */ public function test_user_menu_custom_avatar_size() { global $PAGE; $this->resetAfterTest(true); $testsize = 100; $PAGE->set_url('/'); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $opts = user_get_user_navigation_info($user, $PAGE, array('avatarsize' => $testsize)); $avatarhtml = $opts->metadata['useravatar']; $matches = []; preg_match('/size-100/', $avatarhtml, $matches); $this->assertCount(1, $matches); } /** * Test user_can_view_profile */ public function test_user_can_view_profile() { global $DB, $CFG; $this->resetAfterTest(); // Create five users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $user4 = $this->getDataGenerator()->create_user(); $user5 = $this->getDataGenerator()->create_user(); $user6 = $this->getDataGenerator()->create_user(array('deleted' => 1)); $user7 = $this->getDataGenerator()->create_user(); $user8 = $this->getDataGenerator()->create_user(); $user8->id = 0; // Visitor. $studentrole = $DB->get_record('role', array('shortname' => 'student')); // Add the course creator role to the course contact and assign a user to that role. $CFG->coursecontact = '2'; $coursecreatorrole = $DB->get_record('role', array('shortname' => 'coursecreator')); $this->getDataGenerator()->role_assign($coursecreatorrole->id, $user7->id); // Create two courses. $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course2->id); // Prepare another course with separate groups and groupmodeforce set to true. $record = new \stdClass(); $record->groupmode = 1; $record->groupmodeforce = 1; $course3 = $this->getDataGenerator()->create_course($record); // Enrol users 1 and 2 in first course. $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->getDataGenerator()->enrol_user($user2->id, $course1->id); // Enrol users 2 and 3 in second course. $this->getDataGenerator()->enrol_user($user2->id, $course2->id); $this->getDataGenerator()->enrol_user($user3->id, $course2->id); // Enrol users 1, 4, and 5 into course 3. $this->getDataGenerator()->enrol_user($user1->id, $course3->id); $this->getDataGenerator()->enrol_user($user4->id, $course3->id); $this->getDataGenerator()->enrol_user($user5->id, $course3->id); // User 3 should not be able to see user 1, either by passing their own course (course 2) or user 1's course (course 1). $this->setUser($user3); $this->assertFalse(user_can_view_profile($user1, $course2)); $this->assertFalse(user_can_view_profile($user1, $course1)); // Remove capability moodle/user:viewdetails in course 2. assign_capability('moodle/user:viewdetails', CAP_PROHIBIT, $studentrole->id, $coursecontext); // Set current user to user 1. $this->setUser($user1); // User 1 can see User 1's profile. $this->assertTrue(user_can_view_profile($user1)); $tempcfg = $CFG->forceloginforprofiles; $CFG->forceloginforprofiles = 0; // Not forced to log in to view profiles, should be able to see all profiles besides user 6. $users = array($user1, $user2, $user3, $user4, $user5, $user7); foreach ($users as $user) { $this->assertTrue(user_can_view_profile($user)); } // Restore setting. $CFG->forceloginforprofiles = $tempcfg; // User 1 can not see user 6 as they have been deleted. $this->assertFalse(user_can_view_profile($user6)); // User 1 can see User 7 as they are a course contact. $this->assertTrue(user_can_view_profile($user7)); // User 1 is in a course with user 2 and has the right capability - return true. $this->assertTrue(user_can_view_profile($user2)); // User 1 is not in a course with user 3 - return false. $this->assertFalse(user_can_view_profile($user3)); // Set current user to user 2. $this->setUser($user2); // User 2 is in a course with user 3 but does not have the right capability - return false. $this->assertFalse(user_can_view_profile($user3)); // Set user 1 in one group and users 4 and 5 in another group. $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); groups_add_member($group1->id, $user1->id); groups_add_member($group2->id, $user4->id); groups_add_member($group2->id, $user5->id); $this->setUser($user1); // Check that user 1 can not see user 4. $this->assertFalse(user_can_view_profile($user4)); // Check that user 5 can see user 4. $this->setUser($user5); $this->assertTrue(user_can_view_profile($user4)); // Test the user:viewalldetails cap check using the course creator role which, by default, can't see student profiles. $this->setUser($user7); $this->assertFalse(user_can_view_profile($user4)); assign_capability('moodle/user:viewalldetails', CAP_ALLOW, $coursecreatorrole->id, \context_system::instance()->id, true); reload_all_capabilities(); $this->assertTrue(user_can_view_profile($user4)); unassign_capability('moodle/user:viewalldetails', $coursecreatorrole->id, $coursecontext->id); reload_all_capabilities(); $CFG->coursecontact = null; // Visitor (Not a guest user, userid=0). $CFG->forceloginforprofiles = 1; $this->setUser($user8); $this->assertFalse(user_can_view_profile($user1)); // Let us test with guest user. $this->setGuestUser(); $CFG->forceloginforprofiles = 1; foreach ($users as $user) { $this->assertFalse(user_can_view_profile($user)); } // Even with cap, still guests should not be allowed in. $guestrole = $DB->get_records_menu('role', array('shortname' => 'guest'), 'id', 'archetype, id'); assign_capability('moodle/user:viewdetails', CAP_ALLOW, $guestrole['guest'], \context_system::instance()->id, true); reload_all_capabilities(); foreach ($users as $user) { $this->assertFalse(user_can_view_profile($user)); } $CFG->forceloginforprofiles = 0; foreach ($users as $user) { $this->assertTrue(user_can_view_profile($user)); } // Let us test with Visitor user. $this->setUser($user8); $CFG->forceloginforprofiles = 1; foreach ($users as $user) { $this->assertFalse(user_can_view_profile($user)); } $CFG->forceloginforprofiles = 0; foreach ($users as $user) { $this->assertTrue(user_can_view_profile($user)); } // Testing non-shared courses where capabilities are met, using system role overrides. $CFG->forceloginforprofiles = $tempcfg; $course4 = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course4->id); // Assign a manager role at the system context. $managerrole = $DB->get_record('role', array('shortname' => 'manager')); $user9 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->role_assign($managerrole->id, $user9->id); // Make sure viewalldetails and viewdetails are overridden to 'prevent' (i.e. can be overridden at a lower context). $systemcontext = \context_system::instance(); assign_capability('moodle/user:viewdetails', CAP_PREVENT, $managerrole->id, $systemcontext, true); assign_capability('moodle/user:viewalldetails', CAP_PREVENT, $managerrole->id, $systemcontext, true); // And override these to 'Allow' in a specific course. $course4context = \context_course::instance($course4->id); assign_capability('moodle/user:viewalldetails', CAP_ALLOW, $managerrole->id, $course4context, true); assign_capability('moodle/user:viewdetails', CAP_ALLOW, $managerrole->id, $course4context, true); // The manager now shouldn't have viewdetails in the system or user context. $this->setUser($user9); $user1context = \context_user::instance($user1->id); $this->assertFalse(has_capability('moodle/user:viewdetails', $systemcontext)); $this->assertFalse(has_capability('moodle/user:viewdetails', $user1context)); // Confirm that user_can_view_profile() returns true for $user1 when called without $course param. It should find $course1. $this->assertTrue(user_can_view_profile($user1)); // Confirm this also works when restricting scope to just that course. $this->assertTrue(user_can_view_profile($user1, $course4)); } /** * Test user_get_user_details */ public function test_user_get_user_details() { global $DB; $this->resetAfterTest(); // Create user and modify user profile. $teacher = $this->getDataGenerator()->create_user(); $student = $this->getDataGenerator()->create_user(); $studentfullname = fullname($student); $course1 = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course1->id); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($teacher->id, $course1->id); $this->getDataGenerator()->enrol_user($student->id, $course1->id); role_assign($teacherrole->id, $teacher->id, $coursecontext->id); role_assign($studentrole->id, $student->id, $coursecontext->id); accesslib_clear_all_caches_for_unit_testing(); // Get student details as a user with super system capabilities. $result = user_get_user_details($student, $course1); $this->assertEquals($student->id, $result['id']); $this->assertEquals($studentfullname, $result['fullname']); $this->assertEquals($course1->id, $result['enrolledcourses'][0]['id']); $this->setUser($teacher); // Get student details as a user who can only see this user in a course. $result = user_get_user_details($student, $course1); $this->assertEquals($student->id, $result['id']); $this->assertEquals($studentfullname, $result['fullname']); $this->assertEquals($course1->id, $result['enrolledcourses'][0]['id']); // Get student details with required fields. $result = user_get_user_details($student, $course1, array('id', 'fullname')); $this->assertCount(2, $result); $this->assertEquals($student->id, $result['id']); $this->assertEquals($studentfullname, $result['fullname']); // Get exception for invalid required fields. $this->expectException('moodle_exception'); $result = user_get_user_details($student, $course1, array('wrongrequiredfield')); } /** * Regression test for MDL-57840. * * Ensure the fields "auth, confirmed, idnumber, lang, theme, timezone and mailformat" are present when * calling user_get_user_details() function. */ public function test_user_get_user_details_missing_fields() { global $CFG; $this->resetAfterTest(true); $this->setAdminUser(); // We need capabilities to view the data. $user = self::getDataGenerator()->create_user([ 'auth' => 'email', 'confirmed' => '0', 'idnumber' => 'someidnumber', 'lang' => 'en', 'theme' => $CFG->theme, 'timezone' => '5', 'mailformat' => '0', ]); // Fields that should get by default. $got = user_get_user_details($user); self::assertSame('email', $got['auth']); self::assertSame('0', $got['confirmed']); self::assertSame('someidnumber', $got['idnumber']); self::assertSame('en', $got['lang']); self::assertSame($CFG->theme, $got['theme']); self::assertSame('5', $got['timezone']); self::assertSame('0', $got['mailformat']); } /** * Test user_get_user_details_permissions. * @covers ::user_get_user_details */ public function test_user_get_user_details_permissions() { global $CFG; $this->resetAfterTest(); // Create user and modify user profile. $teacher = $this->getDataGenerator()->create_user(); $student1 = $this->getDataGenerator()->create_user(['idnumber' => 'user1id', 'city' => 'Barcelona', 'address' => 'BCN 1B']); $student2 = $this->getDataGenerator()->create_user(); $student1fullname = fullname($student1); $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); $this->getDataGenerator()->enrol_user($teacher->id, $course->id); $this->getDataGenerator()->enrol_user($student1->id, $course->id); $this->getDataGenerator()->enrol_user($student2->id, $course->id); $this->getDataGenerator()->role_assign('teacher', $teacher->id, $coursecontext->id); $this->getDataGenerator()->role_assign('student', $student1->id, $coursecontext->id); $this->getDataGenerator()->role_assign('student', $student2->id, $coursecontext->id); accesslib_clear_all_caches_for_unit_testing(); // Get student details as a user with super system capabilities. $result = user_get_user_details($student1, $course); $this->assertEquals($student1->id, $result['id']); $this->assertEquals($student1fullname, $result['fullname']); $this->assertEquals($course->id, $result['enrolledcourses'][0]['id']); $this->setUser($student2); // Get student details with required fields. $result = user_get_user_details($student1, $course, array('id', 'fullname', 'timezone', 'city', 'address', 'idnumber')); $this->assertCount(4, $result); // Ensure address (never returned), idnumber (identity field) are not returned here. $this->assertEquals($student1->id, $result['id']); $this->assertEquals($student1fullname, $result['fullname']); $this->assertEquals($student1->timezone, $result['timezone']); $this->assertEquals($student1->city, $result['city']); // Set new identity fields and hidden fields and try to retrieve them without permission. $CFG->showuseridentity = $CFG->showuseridentity . ',idnumber'; $CFG->hiddenuserfields = 'city'; $result = user_get_user_details($student1, $course, array('id', 'fullname', 'timezone', 'city', 'address', 'idnumber')); $this->assertCount(3, $result); // Ensure address, city and idnumber are not returned here. $this->assertEquals($student1->id, $result['id']); $this->assertEquals($student1fullname, $result['fullname']); $this->assertEquals($student1->timezone, $result['timezone']); // Now, teacher should have permission to see the idnumber and city fields. $this->setUser($teacher); $result = user_get_user_details($student1, $course, array('id', 'fullname', 'timezone', 'city', 'address', 'idnumber')); $this->assertCount(5, $result); // Ensure address is not returned here. $this->assertEquals($student1->id, $result['id']); $this->assertEquals($student1fullname, $result['fullname']); $this->assertEquals($student1->timezone, $result['timezone']); $this->assertEquals($student1->idnumber, $result['idnumber']); $this->assertEquals($student1->city, $result['city']); // And admins can see anything. $this->setAdminUser(); $result = user_get_user_details($student1, $course, array('id', 'fullname', 'timezone', 'city', 'address', 'idnumber')); $this->assertCount(6, $result); $this->assertEquals($student1->id, $result['id']); $this->assertEquals($student1fullname, $result['fullname']); $this->assertEquals($student1->timezone, $result['timezone']); $this->assertEquals($student1->idnumber, $result['idnumber']); $this->assertEquals($student1->city, $result['city']); $this->assertEquals($student1->address, $result['address']); } /** * Test user_get_user_details_groups. * @covers ::user_get_user_details */ public function test_user_get_user_details_groups() { $this->resetAfterTest(); // Create user and modify user profile. $teacher = $this->getDataGenerator()->create_user(); $student1 = $this->getDataGenerator()->create_user(['idnumber' => 'user1id', 'city' => 'Barcelona', 'address' => 'BCN 1B']); $student2 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); $this->getDataGenerator()->enrol_user($teacher->id, $course->id); $this->getDataGenerator()->enrol_user($student1->id, $course->id); $this->getDataGenerator()->enrol_user($student2->id, $course->id); $this->getDataGenerator()->role_assign('teacher', $teacher->id, $coursecontext->id); $this->getDataGenerator()->role_assign('student', $student1->id, $coursecontext->id); $this->getDataGenerator()->role_assign('student', $student2->id, $coursecontext->id); $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'G1']); $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'G2']); // Each student in one group but teacher in two. groups_add_member($group1->id, $student1->id); groups_add_member($group1->id, $teacher->id); groups_add_member($group2->id, $student2->id); groups_add_member($group2->id, $teacher->id); accesslib_clear_all_caches_for_unit_testing(); // A student can see other users groups when separate groups are not forced. $this->setUser($student2); // Get student details with groups. $result = user_get_user_details($student1, $course, array('id', 'fullname', 'groups')); $this->assertCount(3, $result); $this->assertEquals($group1->id, $result['groups'][0]['id']); // Teacher is in two different groups. $result = user_get_user_details($teacher, $course, array('id', 'fullname', 'groups')); // Order by group id. usort($result['groups'], function($a, $b) { return $a['id'] - $b['id']; }); $this->assertCount(3, $result); $this->assertCount(2, $result['groups']); $this->assertEquals($group1->id, $result['groups'][0]['id']); $this->assertEquals($group2->id, $result['groups'][1]['id']); // Change to separate groups. $course->groupmode = SEPARATEGROUPS; $course->groupmodeforce = true; update_course($course); // Teacher is in two groups but I can only see the one shared with me. $result = user_get_user_details($teacher, $course, array('id', 'fullname', 'groups')); $this->assertCount(3, $result); $this->assertCount(1, $result['groups']); $this->assertEquals($group2->id, $result['groups'][0]['id']); } } editlib.php 0000644 00000046667 15151162244 0006715 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains function used when editing a users profile and preferences. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once($CFG->dirroot . '/user/lib.php'); /** * Cancels the requirement for a user to update their email address. * * @param int $userid */ function cancel_email_update($userid) { unset_user_preference('newemail', $userid); unset_user_preference('newemailkey', $userid); unset_user_preference('newemailattemptsleft', $userid); } /** * Performs the common access checks and page setup for all * user preference pages. * * @param int $userid The user id to edit taken from the page params. * @param int $courseid The optional course id if we came from a course context. * @return array containing the user and course records. */ function useredit_setup_preference_page($userid, $courseid) { global $PAGE, $SESSION, $DB, $CFG, $OUTPUT, $USER; // Guest can not edit. if (isguestuser()) { throw new \moodle_exception('guestnoeditprofile'); } if (!$course = $DB->get_record('course', array('id' => $courseid))) { throw new \moodle_exception('invalidcourseid'); } if ($course->id != SITEID) { require_login($course); } else if (!isloggedin()) { if (empty($SESSION->wantsurl)) { $SESSION->wantsurl = $CFG->wwwroot.'/user/preferences.php'; } redirect(get_login_url()); } else { $PAGE->set_context(context_system::instance()); } // The user profile we are editing. if (!$user = $DB->get_record('user', array('id' => $userid))) { throw new \moodle_exception('invaliduserid'); } // Guest can not be edited. if (isguestuser($user)) { throw new \moodle_exception('guestnoeditprofile'); } // Remote users cannot be edited. if (is_mnet_remote_user($user)) { if (user_not_fully_set_up($user, false)) { $hostwwwroot = $DB->get_field('mnet_host', 'wwwroot', array('id' => $user->mnethostid)); throw new \moodle_exception('usernotfullysetup', 'mnet', '', $hostwwwroot); } redirect($CFG->wwwroot . "/user/view.php?course={$course->id}"); } $systemcontext = context_system::instance(); $personalcontext = context_user::instance($user->id); // Check access control. if ($user->id == $USER->id) { // Editing own profile - require_login() MUST NOT be used here, it would result in infinite loop! if (!has_capability('moodle/user:editownprofile', $systemcontext)) { throw new \moodle_exception('cannotedityourprofile'); } } else { // Teachers, parents, etc. require_capability('moodle/user:editprofile', $personalcontext); // No editing of primary admin! if (is_siteadmin($user) and !is_siteadmin($USER)) { // Only admins may edit other admins. throw new \moodle_exception('useradmineditadmin'); } } if ($user->deleted) { echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('userdeleted')); echo $OUTPUT->footer(); die; } $PAGE->set_pagelayout('admin'); $PAGE->add_body_class('limitedwidth'); $PAGE->set_context($personalcontext); if ($USER->id != $user->id) { $PAGE->navigation->extend_for_user($user); } else { if ($node = $PAGE->navigation->find('myprofile', navigation_node::TYPE_ROOTNODE)) { $node->force_open(); } } return array($user, $course); } /** * Loads the given users preferences into the given user object. * * @param stdClass $user The user object, modified by reference. * @param bool $reload */ function useredit_load_preferences(&$user, $reload=true) { global $USER; if (!empty($user->id)) { if ($reload and $USER->id == $user->id) { // Reload preferences in case it was changed in other session. unset($USER->preference); } if ($preferences = get_user_preferences(null, null, $user->id)) { foreach ($preferences as $name => $value) { $user->{'preference_'.$name} = $value; } } } } /** * Updates the user preferences for the given user * * 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 list 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_')); if (core_user::can_edit_preference($name, $user)) { $value = core_user::clean_preference($value, $name); set_user_preference($name, $value, $user->id); } } } } /** * @deprecated since Moodle 3.2 * @see core_user::update_picture() */ function useredit_update_picture() { throw new coding_exception('useredit_update_picture() can not be used anymore. Please use ' . 'core_user::update_picture() instead.'); } /** * Updates the user email bounce + send counts when the user is edited. * * @param stdClass $user The current user object. * @param stdClass $usernew The updated user object. */ function useredit_update_bounces($user, $usernew) { if (!isset($usernew->email)) { // Locked field. return; } if (!isset($user->email) || $user->email !== $usernew->email) { set_bounce_count($usernew, true); set_send_count($usernew, true); } } /** * Updates the forums a user is tracking when the user is edited. * * @param stdClass $user The original user object. * @param stdClass $usernew The updated user object. */ function useredit_update_trackforums($user, $usernew) { global $CFG; if (!isset($usernew->trackforums)) { // Locked field. return; } if ((!isset($user->trackforums) || ($usernew->trackforums != $user->trackforums)) and !$usernew->trackforums) { require_once($CFG->dirroot.'/mod/forum/lib.php'); forum_tp_delete_read_records($usernew->id); } } /** * Updates a users interests. * * @param stdClass $user * @param array $interests */ function useredit_update_interests($user, $interests) { core_tag_tag::set_item_tags('core', 'user', $user->id, context_user::instance($user->id), $interests); } /** * Powerful function that is used by edit and editadvanced to add common form elements/rules/etc. * * @param moodleform $mform * @param array $editoroptions * @param array $filemanageroptions * @param stdClass $user */ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions, $user) { global $CFG, $USER, $DB; if ($user->id > 0) { useredit_load_preferences($user, false); } $strrequired = get_string('required'); $stringman = get_string_manager(); // Add the necessary names. foreach (useredit_get_required_name_fields() as $fullname) { $purpose = user_edit_map_field_purpose($user->id, $fullname); $mform->addElement('text', $fullname, get_string($fullname), 'maxlength="100" size="30"' . $purpose); if ($stringman->string_exists('missing'.$fullname, 'core')) { $strmissingfield = get_string('missing'.$fullname, 'core'); } else { $strmissingfield = $strrequired; } $mform->addRule($fullname, $strmissingfield, 'required', null, 'client'); $mform->setType($fullname, PARAM_NOTAGS); } $enabledusernamefields = useredit_get_enabled_name_fields(); // Add the enabled additional name fields. foreach ($enabledusernamefields as $addname) { $purpose = user_edit_map_field_purpose($user->id, $addname); $mform->addElement('text', $addname, get_string($addname), 'maxlength="100" size="30"' . $purpose); $mform->setType($addname, PARAM_NOTAGS); } // Do not show email field if change confirmation is pending. if ($user->id > 0 and !empty($CFG->emailchangeconfirmation) and !empty($user->preference_newemail)) { $notice = get_string('emailchangepending', 'auth', $user); $notice .= '<br /><a href="edit.php?cancelemailchange=1&id='.$user->id.'">' . get_string('emailchangecancel', 'auth') . '</a>'; $mform->addElement('static', 'emailpending', get_string('email'), $notice); } else { $purpose = user_edit_map_field_purpose($user->id, 'email'); $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"' . $purpose); $mform->addRule('email', $strrequired, 'required', null, 'client'); $mform->setType('email', PARAM_RAW_TRIMMED); } $choices = array(); $choices['0'] = get_string('emaildisplayno'); $choices['1'] = get_string('emaildisplayyes'); $choices['2'] = get_string('emaildisplaycourse'); $mform->addElement('select', 'maildisplay', get_string('emaildisplay'), $choices); $mform->setDefault('maildisplay', core_user::get_property_default('maildisplay')); $mform->addHelpButton('maildisplay', 'emaildisplay'); if (get_config('tool_moodlenet', 'enablemoodlenet')) { $mform->addElement('text', 'moodlenetprofile', get_string('moodlenetprofile', 'user'), 'maxlength="255" size="30"'); $mform->setType('moodlenetprofile', PARAM_NOTAGS); $mform->addHelpButton('moodlenetprofile', 'moodlenetprofile', 'user'); } $mform->addElement('text', 'city', get_string('city'), 'maxlength="120" size="21"'); $mform->setType('city', PARAM_TEXT); if (!empty($CFG->defaultcity)) { $mform->setDefault('city', $CFG->defaultcity); } $purpose = user_edit_map_field_purpose($user->id, 'country'); $choices = get_string_manager()->get_list_of_countries(); $choices = array('' => get_string('selectacountry') . '...') + $choices; $mform->addElement('select', 'country', get_string('selectacountry'), $choices, $purpose); if (!empty($CFG->country)) { $mform->setDefault('country', core_user::get_property_default('country')); } if (isset($CFG->forcetimezone) and $CFG->forcetimezone != 99) { $choices = core_date::get_list_of_timezones($CFG->forcetimezone); $mform->addElement('static', 'forcedtimezone', get_string('timezone'), $choices[$CFG->forcetimezone]); $mform->addElement('hidden', 'timezone'); $mform->setType('timezone', core_user::get_property_type('timezone')); } else { $choices = core_date::get_list_of_timezones($user->timezone, true); $mform->addElement('select', 'timezone', get_string('timezone'), $choices); } if ($user->id < 0) { $purpose = user_edit_map_field_purpose($user->id, 'lang'); $translations = get_string_manager()->get_list_of_translations(); $mform->addElement('select', 'lang', get_string('preferredlanguage'), $translations, $purpose); $lang = empty($user->lang) ? $CFG->lang : $user->lang; $mform->setDefault('lang', $lang); } if (!empty($CFG->allowuserthemes)) { $choices = array(); $choices[''] = get_string('default'); $themes = get_list_of_themes(); foreach ($themes as $key => $theme) { if (empty($theme->hidefromselector)) { $choices[$key] = get_string('pluginname', 'theme_'.$theme->name); } } $mform->addElement('select', 'theme', get_string('preferredtheme'), $choices); } $mform->addElement('editor', 'description_editor', get_string('userdescription'), null, $editoroptions); $mform->setType('description_editor', PARAM_RAW); $mform->addHelpButton('description_editor', 'userdescription'); if (empty($USER->newadminuser)) { $mform->addElement('header', 'moodle_picture', get_string('pictureofuser')); $mform->setExpanded('moodle_picture', true); if (!empty($CFG->enablegravatar)) { $mform->addElement('html', html_writer::tag('p', get_string('gravatarenabled'))); } $mform->addElement('static', 'currentpicture', get_string('currentpicture')); $mform->addElement('checkbox', 'deletepicture', get_string('deletepicture')); $mform->setDefault('deletepicture', 0); $mform->addElement('filemanager', 'imagefile', get_string('newpicture'), '', $filemanageroptions); $mform->addHelpButton('imagefile', 'newpicture'); $mform->addElement('text', 'imagealt', get_string('imagealt'), 'maxlength="100" size="30"'); $mform->setType('imagealt', PARAM_TEXT); } // Display user name fields that are not currenlty enabled here if there are any. $disabledusernamefields = useredit_get_disabled_name_fields($enabledusernamefields); if (count($disabledusernamefields) > 0) { $mform->addElement('header', 'moodle_additional_names', get_string('additionalnames')); foreach ($disabledusernamefields as $allname) { $purpose = user_edit_map_field_purpose($user->id, $allname); $mform->addElement('text', $allname, get_string($allname), 'maxlength="100" size="30"' . $purpose); $mform->setType($allname, PARAM_NOTAGS); } } if (core_tag_tag::is_enabled('core', 'user') and empty($USER->newadminuser)) { $mform->addElement('header', 'moodle_interests', get_string('interests')); $mform->addElement('tags', 'interests', get_string('interestslist'), array('itemtype' => 'user', 'component' => 'core')); $mform->addHelpButton('interests', 'interestslist'); } // Moodle optional fields. $mform->addElement('header', 'moodle_optional', get_string('optional', 'form')); $mform->addElement('text', 'idnumber', get_string('idnumber'), 'maxlength="255" size="25"'); $mform->setType('idnumber', core_user::get_property_type('idnumber')); $mform->addElement('text', 'institution', get_string('institution'), 'maxlength="255" size="25"'); $mform->setType('institution', core_user::get_property_type('institution')); $mform->addElement('text', 'department', get_string('department'), 'maxlength="255" size="25"'); $mform->setType('department', core_user::get_property_type('department')); $mform->addElement('text', 'phone1', get_string('phone1'), 'maxlength="20" size="25"'); $mform->setType('phone1', core_user::get_property_type('phone1')); $mform->setForceLtr('phone1'); $mform->addElement('text', 'phone2', get_string('phone2'), 'maxlength="20" size="25"'); $mform->setType('phone2', core_user::get_property_type('phone2')); $mform->setForceLtr('phone2'); $mform->addElement('text', 'address', get_string('address'), 'maxlength="255" size="25"'); $mform->setType('address', core_user::get_property_type('address')); } /** * Return required user name fields for forms. * * @return array required user name fields in order according to settings. */ function useredit_get_required_name_fields() { global $CFG; // Get the name display format. $nameformat = $CFG->fullnamedisplay; // Names that are required fields on user forms. $necessarynames = array('firstname', 'lastname'); $languageformat = get_string('fullnamedisplay'); // Check that the language string and the $nameformat contain the necessary names. foreach ($necessarynames as $necessaryname) { $pattern = "/$necessaryname\b/"; if (!preg_match($pattern, $languageformat)) { // If the language string has been altered then fall back on the below order. $languageformat = 'firstname lastname'; } if (!preg_match($pattern, $nameformat)) { // If the nameformat doesn't contain the necessary name fields then use the languageformat. $nameformat = $languageformat; } } // Order all of the name fields in the postion they are written in the fullnamedisplay setting. $necessarynames = order_in_string($necessarynames, $nameformat); return $necessarynames; } /** * Gets enabled (from fullnameformate setting) user name fields in appropriate order. * * @return array Enabled user name fields. */ function useredit_get_enabled_name_fields() { global $CFG; // Get all of the other name fields which are not ranked as necessary. $additionalusernamefields = array_diff(\core_user\fields::get_name_fields(), array('firstname', 'lastname')); // Find out which additional name fields are actually being used from the fullnamedisplay setting. $enabledadditionalusernames = array(); foreach ($additionalusernamefields as $enabledname) { if (strpos($CFG->fullnamedisplay, $enabledname) !== false) { $enabledadditionalusernames[] = $enabledname; } } // Order all of the name fields in the postion they are written in the fullnamedisplay setting. $enabledadditionalusernames = order_in_string($enabledadditionalusernames, $CFG->fullnamedisplay); return $enabledadditionalusernames; } /** * Gets user name fields not enabled from the setting fullnamedisplay. * * @param array $enabledadditionalusernames Current enabled additional user name fields. * @return array Disabled user name fields. */ function useredit_get_disabled_name_fields($enabledadditionalusernames = null) { // If we don't have enabled additional user name information then go and fetch it (try to avoid). if (!isset($enabledadditionalusernames)) { $enabledadditionalusernames = useredit_get_enabled_name_fields(); } // These are the additional fields that are not currently enabled. $nonusednamefields = array_diff(\core_user\fields::get_name_fields(), array_merge(array('firstname', 'lastname'), $enabledadditionalusernames)); // It may not be significant anywhere, but for compatibility, this used to return an array // with keys and values the same. $result = []; foreach ($nonusednamefields as $field) { $result[$field] = $field; } return $result; } portfolio.php 0000644 00000011417 15151162244 0007277 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file is part of the User section Moodle * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once(__DIR__ . '/../config.php'); if (empty($CFG->enableportfolios)) { throw new \moodle_exception('disabled', 'portfolio'); } require_once($CFG->libdir . '/portfoliolib.php'); require_once($CFG->libdir . '/portfolio/forms.php'); $config = optional_param('config', 0, PARAM_INT); $hide = optional_param('hide', 0, PARAM_INT); $courseid = optional_param('courseid', SITEID, PARAM_INT); $url = new moodle_url('/user/portfolio.php', array('courseid' => $courseid)); if ($config !== 0) { $url->param('config', $config); } if (! $course = $DB->get_record("course", array("id" => $courseid))) { throw new \moodle_exception('invalidcourseid'); } $user = $USER; $fullname = fullname($user); $strportfolios = get_string('portfolios', 'portfolio'); $configstr = get_string('manageyourportfolios', 'portfolio'); $namestr = get_string('name'); $pluginstr = get_string('plugin', 'portfolio'); $baseurl = $CFG->wwwroot . '/user/portfolio.php'; $introstr = get_string('intro', 'portfolio'); $showhide = get_string('showhide', 'portfolio'); $display = true; // Set this to false in the conditions to stop processing. require_login($course, false); $PAGE->set_url($url); $PAGE->set_context(context_user::instance($user->id)); $PAGE->set_title($configstr); $PAGE->set_heading($fullname); $PAGE->set_pagelayout('admin'); echo $OUTPUT->header(); $showroles = 1; if (!empty($config)) { navigation_node::override_active_url(new moodle_url('/user/portfolio.php', array('courseid' => $courseid))); $instance = portfolio_instance($config); $mform = new portfolio_user_form('', array('instance' => $instance, 'userid' => $user->id)); if ($mform->is_cancelled()) { redirect($baseurl); exit; } else if ($fromform = $mform->get_data()) { if (!confirm_sesskey()) { throw new \moodle_exception('confirmsesskeybad', '', $baseurl); } // This branch is where you process validated data. $instance->set_user_config($fromform, $USER->id); core_plugin_manager::reset_caches(); redirect($baseurl, get_string('instancesaved', 'portfolio'), 3); exit; } else { echo $OUTPUT->heading(get_string('configplugin', 'portfolio')); echo $OUTPUT->box_start(); $mform->display(); echo $OUTPUT->box_end(); $display = false; } } else if (!empty($hide)) { $instance = portfolio_instance($hide); $instance->set_user_config(array('visible' => !$instance->get_user_config('visible', $USER->id)), $USER->id); core_plugin_manager::reset_caches(); } if ($display) { echo $OUTPUT->heading($configstr); echo $OUTPUT->box_start(); echo html_writer::tag('p', $introstr); if (!$instances = portfolio_instances(true, false)) { throw new \moodle_exception('noinstances', 'portfolio', $CFG->wwwroot . '/user/view.php'); } $table = new html_table(); $table->head = array($namestr, $pluginstr, $showhide); $table->data = array(); foreach ($instances as $i) { // Contents of the actions (Show / hide) column. $actions = ''; // Configure icon. if ($i->has_user_config()) { $configurl = new moodle_url($baseurl); $configurl->param('config', $i->get('id')); $actions .= html_writer::link($configurl, $OUTPUT->pix_icon('t/edit', get_string('configure', 'portfolio'))); } // Hide/show icon. $visible = $i->get_user_config('visible', $USER->id); $visibilityaction = $visible ? 'hide' : 'show'; $showhideurl = new moodle_url($baseurl); $showhideurl->param('hide', $i->get('id')); $actions .= html_writer::link($showhideurl, $OUTPUT->pix_icon('t/' . $visibilityaction, get_string($visibilityaction))); $table->data[] = array($i->get('name'), $i->get('plugin'), $actions); } echo html_writer::table($table); echo $OUTPUT->box_end(); } echo $OUTPUT->footer(); contentbank.php 0000644 00000004370 15151162244 0007570 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Allows you to edit a users profile * * @copyright 2020 François Moreau * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once('../config.php'); require_once($CFG->dirroot.'/user/editlib.php'); require_once($CFG->dirroot.'/user/lib.php'); require_login(); $userid = optional_param('id', $USER->id, PARAM_INT); // User id. $PAGE->set_url('/user/contentbank.php', ['id' => $userid]); list($user, $course) = useredit_setup_preference_page($userid, SITEID); $form = new \core_user\form\contentbank_user_preferences_form(null, ['userid' => $user->id]); $user->contentvisibility = get_user_preferences('core_contentbank_visibility', $CFG->defaultpreference_core_contentbank_visibility, $user->id); $form->set_data($user); $redirect = new moodle_url("/user/preferences.php", ['userid' => $user->id]); if ($form->is_cancelled()) { redirect($redirect); } else if ($data = $form->get_data()) { $data = $form->get_data(); $usernew = [ 'id' => $user->id, 'preference_core_contentbank_visibility' => $data->contentvisibility ]; useredit_update_user_preference($usernew); \core\event\user_updated::create_from_userid($user->id)->trigger(); redirect($redirect); } $title = get_string('contentbankpreferences', 'core_contentbank'); $userfullname = fullname($user, true); $PAGE->navbar->includesettingsbase = true; $PAGE->set_title("$course->shortname: $title"); $PAGE->set_heading($userfullname); echo $OUTPUT->header(); echo $OUTPUT->heading($title); $form->display(); echo $OUTPUT->footer(); templates/add_bulk_note.mustache 0000644 00000003451 15151162244 0013073 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle 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 the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_user/add_bulk_note Template for the add bulk note modal. Context variables required for this template: * stateNames array - List of value / label pairs of valid publish states for notes. * stateHelpIcon string - Rendered help icon for the publish state. Example context (json): { "stateNames": [ { "value": 0, "label": "State 1"}, { "value": 1, "label": "State 2"} ], "stateHelpIcon": "(help me)" } }} <form> <p> <label for="bulk-state" class="mr-2"> {{#str}}publishstate, core_notes{{/str}} </label> <select name="state" id="bulk-state" class="custom-select"> {{#stateNames}} <option value="{{value}}" {{#selected}}selected{{/selected}}>{{label}}</option> {{/stateNames}} </select> {{{stateHelpIcon}}} </p> <p> <label for="bulk-note"> <span class="sr-only">{{#str}}note, core_notes{{/str}}</span> </label> <textarea id="bulk-note" rows="3" data-max-rows="10" data-auto-rows="true" cols="30" class="form-control"></textarea> </p> </form> {{#js}} require(['core/auto_rows'], function(AutoRows) { AutoRows.init(document.getElementById('bulk-note')); }); {{/js}} templates/upcoming_activities_due_insight_body.mustache 0000644 00000006126 15151162244 0017747 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle 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 the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_user/upcoming_activities_due_insight_body Template for the upcoming activity due insight Context variables required for this template: * activitiesdue array - Data for each activity due. * userfirstname string - The user firstname. Example context (json): { "userfirstname": "John", "activitiesdue": [ { "name": "Introduction to ASP is due", "formattedtime": "31 January 2018", "coursename": "Programming I", "url": "https://www.google.com" } ] } }} {{! The styles defined here will be included in the Moodle web UI and in emails. Emails do not include Moodle stylesheets so we want these styles to be applied to emails. However, they will also be included in the Moodle web UI. The styles defined in the class .table have precedence over general styles at tag level, so these styles are only applied to emails.}} <head><style> table { text-align: justify; margin-bottom: 1rem; margin-top: 1rem; } table tr.when { background-color: #e9ecef; } table th { padding: 1rem .75rem 1rem .75rem; font-weight: 400; font-size: larger; border-top: 1px solid #dee2e6; } table td { padding: .75rem; } table td.link { border-top: 1px solid #dee2e6; border-bottom: 1px solid #dee2e6; } </style></head> <div> {{#str}} youhaveupcomingactivitiesdueinfo, moodle, {{userfirstname}} {{/str}} <br/><br/> {{#activitiesdue}} <table class="table upcoming-activity-due"> <thead> <tr> <th scope="col" class="h5"> {{#icon}} {{#pix}} {{key}}, {{component}}, {{title}} {{alttext}} {{/pix}} {{/icon}} {{name}} </th> </tr> </thead> <tbody> <tr class="when"> <td><strong>{{#str}} whendate, calendar, {{formattedtime}} {{/str}}</strong></td> </tr> <tr> <td>{{#str}} coursetitle, moodle, {"course": "{{coursename}}" } {{/str}}</td> </tr> <tr> <td class="link"><a href="{{url}}">{{#str}} gotoactivity, calendar{{/str}}</a></td> </tr> </tbody> </table> {{/activitiesdue}} </div> templates/status_field.mustache 0000644 00000006217 15151162244 0012772 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle 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 the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_user/status_field Template for the enrolment status field. Context variables required for this template: * fullname string - The user's full name. * coursename string - The course name. * enrolinstancename string - The enrolment instance name. * status string - The enrolment status. * active boolean - Flag to indicate whether the user has an active enrolment. * suspended boolean - Flag to indicate whether the user is suspended. * notcurrent boolean - Flag to indicate whether the user's enrolment is active, but not current. * timestart string - Optional. The enrolment time start. * timeend string - Optional. The enrolment time end. * timeenrolled string - Optional. The time when the user was enrolled. * enrolactions array - Optional. Array of enrol actions consisting of: * url string - The URL of the enrol action link. * attributes array - The array of attributes for the enrol action link. * icon string - The HTML output for the enrol action icon. Example context (json): { "fullname": "Student John", "coursename": "TC 1", "enrolinstancename": "Manual enrolment", "status": "Active", "active": true, "timestart": "1 January 2017", "timeend": "31 January 2018", "timeenrolled": "31 December 2016", "enrolactions": [ { "url": "#", "attributes": [ { "name": "class", "value": "action-class" } ], "icon": "<i class=\"icon fa fa-cog fa-fw\" aria-hidden=\"true\" title=\"Edit enrolment\" aria-label=\"\"></i>" } ] } }} <div data-fullname="{{fullname}}" data-coursename="{{coursename}}" data-enrolinstancename="{{enrolinstancename}}" data-status="{{status}}" data-timestart="{{timestart}}" data-timeend="{{timeend}}" data-timeenrolled="{{timeenrolled}}"> <span class="badge {{#active}}badge-success{{/active}}{{#suspended}}badge-warning{{/suspended}}{{#notcurrent}}badge-secondary{{/notcurrent}}">{{status}}</span> <a data-action="showdetails" href="#" role="button" tabindex="0">{{! }}{{#pix}}docs, core, {{enrolinstancename}}{{/pix}}{{! }}</a> {{#enrolactions}} <a href="{{url}}" role="button" {{#attributes}}{{name}}="{{value}}" {{/attributes}}>{{! }}{{{icon}}}{{! }}</a> {{/enrolactions}} </div> templates/contact_site_support_not_available.mustache 0000644 00000003435 15151162244 0017436 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle 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 the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_user/contact_site_support_not_available Content displayed on the contact site support page if attempting to submit the support form fails. Example context (json): { "supportemail": "<a href=\"mailto:support@mail.com\">support@mail.com</a>", "supportform": "<input type='text' value='test'>" } }} <div class="card"> <div class="card-body bg-light d-flex flex-row"> <div class="p-3 mr-3 align-self-center supporticon">{{#pix}}t/life-ring, core{{/pix}}</div> <div class = "p-1 align-self-center text-muted"> <div class="h4 font-weight-normal mb-0">{{#str}}supportmessagenotsent, user{{/str}}</div> {{#supportemail}} <div class="py-3 mt-2"> <span class="h5 font-weight-normal w-75 rounded border p-3"> {{#str}}supportmessagealternative, user, {{{supportemail}}}{{/str}} </span> </div> {{/supportemail}} </div> </div> </div> <div class="py-1 pt-3"> {{{supportform}}} </div> templates/form_user_selector_suggestion.mustache 0000644 00000003047 15151162244 0016452 0 ustar 00 {{! This file is part of Moodle - https://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle 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 the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_user/form_user_selector_suggestion Moodle template for the list of valid options in an user selector autocomplate form element. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * fullname - string Users full name * extrafields - list Example context (json): { "fullname": "Admin User", "extrafields": [ { "name": "email", "value": "admin@example.com" }, { "name": "phone1", "value": "0123456789" } ] } }} <span> <span data-field="fullname">{{fullname}}</span> <small> {{#extrafields}} <span data-field="{{name}}">{{{value}}}</span> {{/extrafields}} </small> </span> templates/contact_site_support_email_body.mustache 0000644 00000006117 15151162244 0016742 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle 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 the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_user/contact_site_support_email_body Support email email body. Example context (json): { "subject": "email subject", "notloggedinuser": true, "name": "Sender name", "email": "sender@mail.com", "description": "Support request message" } }} <head> <style> .table { font-size: 18px; margin-bottom: 20px; width: 40%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; } .alert { position: relative; padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 0 solid transparent; border-radius: 0.5rem; } .alert-warning { color: #7d5a29; background-color: #fcefdc; border-color: #fbe8cd; } .subject { background-color: #f8f9fa; } </style> </head> <body> <table class="table"> <tr> <td class="subject"><br /><b>{{#str}}subject{{/str}}:</b> {{subject}}<br /><br /></td> </tr> <tr> <td> <table style="width: 100%;"> <thead> <tr> {{#notloggedinuser}} <td class="alert alert-warning" colspan="2"> {{#str}}supportmessagesentforloggedoutuser, user{{/str}} </td> {{/notloggedinuser}} </tr> </thead> <tbody> <tr> <td colspan="2"><br /></td> </tr> <tr> <td> <b>{{#str}}name{{/str}}</b></td> <td>{{name}}</td> </tr> <tr> <td colspan="2"><hr></td> </tr> <tr style="border-top: 10px" > <td > <b>{{#str}}email{{/str}}</b></td> <td>{{email}}</td> </tr> <tr> <td colspan="2"><hr></td> </tr> <tr> <td> <b>{{#str}}description{{/str}}</b></td> <td>{{message}}</td> </tr> </tbody> </table> </td> </tr> </table> </body> templates/edit_profile_fields.mustache 0000644 00000012706 15151162244 0014277 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle 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 the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_user/edit_profile_fields UI for editing profile fields Example context (json): { "baseurl": "index.php", "sesskey": "12345", "categories": [ { "id": 1, "name": "Cat1", "fields": [ {"id": 1, "name": "Field1", "isfirst": true, "islast": false}, {"id": 2, "name": "Field2", "isfirst": false, "islast": false}, {"id": 3, "name": "Field3", "isfirst": false, "islast": true} ], "hasfields": true, "isfirst": true, "candelete": true }, { "id": 2, "name": "Cat2", "candelete": true }, { "id": 3, "name": "Cat3", "islast": true, "candelete": true } ] } }} <div class="row profileeditor"> <div class="col align-self-end"> <a tabindex="0" role="button" class="btn btn-secondary float-right" data-action="editcategory">{{#str}}profilecreatecategory, admin{{/str}}</a> </div> </div> <div class="categorieslist"> {{#categories}} <div data-category-id="{{id}}" id="category-{{id}}" class="mt-2"> <div class="row justify-content-between align-items-end"> <div class="col-6 categoryinstance"> <h3> {{{name}}} <a href="#" data-action="editcategory" data-id="{{id}}" data-name="{{name}}"> {{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}}</a> {{#candelete}} <a href="{{baseurl}}?action=deletecategory&id={{id}}&sesskey={{sesskey}}"> {{#pix}}t/delete, core, {{#str}}delete{{/str}}{{/pix}}</a> {{/candelete}} {{^isfirst}} <a href="{{baseurl}}?id={{id}}&action=movecategory&dir=up&sesskey={{sesskey}}"> {{#pix}}t/up, core, {{#str}}moveup{{/str}}{{/pix}}</a> {{/isfirst}} {{#isfirst}}{{#pix}}spacer, moodle{{/pix}}{{/isfirst}} {{^islast}} <a href="{{baseurl}}?id={{id}}&action=movecategory&dir=down&sesskey={{sesskey}}"> {{#pix}}t/down, core, {{#str}}movedown{{/str}}{{/pix}}</a> {{/islast}} </h3> </div> <div class="col-auto text-right"> {{#addfieldmenu}}{{> core/action_menu}}{{/addfieldmenu}} </div> </div> <table class="generaltable fullwidth profilefield"> {{#hasfields}} <thead> <tr> <th scope="col" class="col-8">{{#str}}profilefield, admin{{/str}}</th> <th scope="col" class="col-3 text-right">{{#str}}edit{{/str}}</th> </tr> </thead> <tbody> {{#fields}} <tr> <td class="col-8"> {{{name}}} </td> <td class="col-3 text-right"> <a href="#" data-action="editfield" data-id="{{id}}" data-name="{{name}}"> {{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}}</a> <a href="{{baseurl}}?action=deletefield&id={{id}}&sesskey={{sesskey}}"> {{#pix}}t/delete, core, {{#str}}delete{{/str}}{{/pix}}</a> {{^isfirst}} <a href="{{baseurl}}?id={{id}}&action=movefield&dir=up&sesskey={{sesskey}}"> {{#pix}}t/up, core, {{#str}}moveup{{/str}}{{/pix}}</a> {{/isfirst}} {{#isfirst}}{{#pix}}spacer, moodle{{/pix}}{{/isfirst}} {{^islast}} <a href="{{baseurl}}?id={{id}}&action=movefield&dir=down&sesskey={{sesskey}}"> {{#pix}}t/down, core, {{#str}}movedown{{/str}}{{/pix}}</a> {{/islast}} {{#islast}}{{#pix}}spacer, moodle{{/pix}}{{/islast}} </td> </tr> {{/fields}} </tbody> {{/hasfields}} {{^hasfields}} <thead> <tr class="nofields alert alert-danger alert-block fade in"> <td> {{#str}}profilenofieldsdefined, admin{{/str}} </td> </tr> </thead> {{/hasfields}} </table> </div> {{/categories}} </div> {{#js}} require(['core_user/edit_profile_fields'], function(s) { s.init(); }); {{/js}} templates/participantsfilter.mustache 0000644 00000002673 15151162244 0014215 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle 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 the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_user/participantsfilter Template for the form containing one or more filter rows. Example context (json): { "filtertypes": [ { "name": "status", "title": "Status", "values": [ { "value": 1, "title": "Active" }, { "value": 0, "title": "Suspended" } ] } ] } }} {{> core/datafilter/filter }} {{#js}} require(['core_user/participants_filter'], function(ParticipantsFilter) { ParticipantsFilter.init('core-filter-{{uniqid}}'); }); {{/js}} templates/send_bulk_message.mustache 0000644 00000002567 15151162244 0013762 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle 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 the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_user/send_bulk_message Template for the send bulk message modal. Context variables required for this template: None Example context (json): { } }} <form> <p> <label for="bulk-message"> <span class="sr-only">{{#str}}message, core_message{{/str}}</span> </label> <textarea id="bulk-message" rows="3" data-max-rows="10" data-auto-rows="true" cols="30" class="form-control"></textarea> </p> <div class="text-danger" data-role="messagetextrequired" hidden> {{#str}} messagetextrequired, core_message {{/str}} </div> </form> {{#js}} require(['core/auto_rows'], function(AutoRows) { AutoRows.init(document.getElementById('bulk-message')); }); {{/js}} templates/status_details.mustache 0000644 00000006053 15151162244 0013332 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle 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 the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template core_user/status_details Template for the user enrolment status details. Context variables required for this template: * fullname string - The user's full name. * coursename string - The course name. * enrolinstancename string - The enrolment instance name. * status string - The enrolment status. * statusclass string - The CSS class for the enrolment status. * timestart string - Optional. The enrolment time start. * timeend string - Optional. The enrolment time end. * timeenrolled string - Optional. The time when the user was enrolled. Example context (json): { "fullname": "Student John", "coursename": "TC 1", "enrolinstancename": "Manual enrolment", "status": "Active", "statusclass": "badge badge-success", "timestart": "1 January 2017", "timeend": "31 January 2018", "timeenrolled": "31 December 2016" } }} <table class="table user-enrol-details"> <tr> <th> {{#str}}fullname{{/str}} </th> <td class="user-fullname"> {{fullname}} </td> </tr> <tr> <th> {{#str}}course{{/str}} </th> <td class="user-course"> {{coursename}} </td> </tr> <tr> <th> {{#str}}enrolmentmethod, enrol{{/str}} </th> <td class="user-enrol-instance"> {{enrolinstancename}} {{{editenrollink}}} </td> </tr> <tr> <th> {{#str}}participationstatus, enrol{{/str}} </th> <td class="user-enrol-status"> <span class="{{statusclass}}"> {{status}} </span> </td> </tr> {{#timestart}} <tr> <th> {{#str}}enroltimestart, enrol{{/str}} </th> <td class="user-enrol-timestart"> {{timestart}} </td> </tr> {{/timestart}} {{#timeend}} <tr> <th> {{#str}}enroltimeend, enrol{{/str}} </th> <td class="user-enrol-timeend"> {{timeend}} </td> </tr> {{/timeend}} {{#timeenrolled}} <tr> <th> {{#str}}enroltimecreated, enrol{{/str}} </th> <td class="user-enrol-timeenrolled"> {{timeenrolled}} </td> </tr> {{/timeenrolled}} </table> managetoken.php 0000644 00000010230 15151162244 0007543 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Web service test client. * * @package core_webservice * @copyright 2009 Moodle Pty Ltd (http://moodle.com) * @author Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../config.php'); require_login(); $usercontext = context_user::instance($USER->id); $PAGE->set_context($usercontext); $PAGE->set_url('/user/managetoken.php'); $PAGE->set_title(get_string('securitykeys', 'webservice')); $PAGE->set_heading(get_string('securitykeys', 'webservice')); $PAGE->set_pagelayout('admin'); $rsstokenboxhtml = $webservicetokenboxhtml = ''; // Manage user web service tokens. if ( !is_siteadmin($USER->id) && !empty($CFG->enablewebservices) && has_capability('moodle/webservice:createtoken', $usercontext )) { require_once($CFG->dirroot.'/webservice/lib.php'); $action = optional_param('action', '', PARAM_ALPHANUMEXT); $tokenid = optional_param('tokenid', '', PARAM_SAFEDIR); $confirm = optional_param('confirm', 0, PARAM_BOOL); $webservice = new webservice(); // Load the webservice library. $wsrenderer = $PAGE->get_renderer('core', 'webservice'); if ($action == 'resetwstoken') { $token = $webservice->get_created_by_user_ws_token($USER->id, $tokenid); // Display confirmation page to Reset the token. if (!$confirm) { $resetconfirmation = $wsrenderer->user_reset_token_confirmation($token); } else { // Delete the token that need to be regenerated. require_sesskey(); $webservice->delete_user_ws_token($tokenid); redirect($PAGE->url, get_string('resettokencomplete', 'core_webservice')); } } // No point creating the table is we're just displaying a confirmation screen. if (empty($resetconfirmation)) { $webservice->generate_user_ws_tokens($USER->id); // Generate all token that need to be generated. $tokens = $webservice->get_user_ws_tokens($USER->id); foreach ($tokens as $token) { if ($token->restrictedusers) { $authlist = $webservice->get_ws_authorised_user($token->wsid, $USER->id); if (empty($authlist)) { $token->enabled = false; } } } $webservicetokenboxhtml = $wsrenderer->user_webservice_tokens_box($tokens, $USER->id, $CFG->enablewsdocumentation); // Display the box for web service token. } } // RSS keys. if (!empty($CFG->enablerssfeeds)) { require_once($CFG->dirroot.'/lib/rsslib.php'); $action = optional_param('action', '', PARAM_ALPHANUMEXT); $confirm = optional_param('confirm', 0, PARAM_BOOL); $rssrenderer = $PAGE->get_renderer('core', 'rss'); if ($action == 'resetrsstoken') { // Display confirmation page to Reset the token. if (!$confirm) { $resetconfirmation = $rssrenderer->user_reset_rss_token_confirmation(); } else { require_sesskey(); rss_delete_token($USER->id); redirect($PAGE->url, get_string('resettokencomplete', 'core_webservice')); } } if (empty($resetconfirmation)) { $token = rss_get_token($USER->id); $rsstokenboxhtml = $rssrenderer->user_rss_token_box($token); // Display the box for the user's RSS token. } } // PAGE OUTPUT. echo $OUTPUT->header(); if (!empty($resetconfirmation)) { echo $resetconfirmation; } else { echo $webservicetokenboxhtml; echo $rsstokenboxhtml; } echo $OUTPUT->footer(); forum_form.php 0000644 00000007643 15151162244 0007443 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form to edit a users forum preferences. * * These are stored as columns in the user table, which * is why they are in /user and not /mod/forum. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. } require_once($CFG->dirroot.'/lib/formslib.php'); /** * Class user_edit_forum_form. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_edit_forum_form extends moodleform { /** * Define the form. */ public function definition () { global $CFG, $COURSE; $mform = $this->_form; $choices = array(); $choices['0'] = get_string('emaildigestoff'); $choices['1'] = get_string('emaildigestcomplete'); $choices['2'] = get_string('emaildigestsubjects'); $mform->addElement('select', 'maildigest', get_string('emaildigest'), $choices); $mform->setDefault('maildigest', core_user::get_property_default('maildigest')); $mform->addHelpButton('maildigest', 'emaildigest'); $choices = array(); $choices['1'] = get_string('autosubscribeyes'); $choices['0'] = get_string('autosubscribeno'); $mform->addElement('select', 'autosubscribe', get_string('autosubscribe'), $choices); $mform->setDefault('autosubscribe', core_user::get_property_default('autosubscribe')); $choices = array(); $choices['1'] = get_string('yes'); $choices['0'] = get_string('no'); $mform->addElement('select', 'useexperimentalui', get_string('useexperimentalui', 'mod_forum'), $choices); $mform->setDefault('useexperimentalui', '0'); if (!empty($CFG->forum_trackreadposts)) { $mform->addElement('header', 'trackreadposts', get_string('trackreadposts_header', 'mod_forum')); $choices = array(); $choices['0'] = get_string('trackforumsno'); $choices['1'] = get_string('trackforumsyes'); $mform->addElement('select', 'trackforums', get_string('trackforums'), $choices); $mform->setDefault('trackforums', core_user::get_property_default('trackforums')); $choices = [ 1 => get_string('markasreadonnotificationyes', 'mod_forum'), 0 => get_string('markasreadonnotificationno', 'mod_forum'), ]; $mform->addElement('select', 'markasreadonnotification', get_string('markasreadonnotification', 'mod_forum'), $choices); $mform->addHelpButton('markasreadonnotification', 'markasreadonnotification', 'mod_forum'); $mform->disabledIf('markasreadonnotification', 'trackforums', 'eq', "0"); $mform->setDefault('markasreadonnotification', 1); } // Add some extra hidden fields. $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'course', $COURSE->id); $mform->setType('course', PARAM_INT); $this->add_action_buttons(true, get_string('savechanges')); } } language.php 0000644 00000005353 15151162244 0007047 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Allows you to edit a users profile * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once('../config.php'); require_once($CFG->libdir.'/gdlib.php'); require_once($CFG->dirroot.'/user/language_form.php'); require_once($CFG->dirroot.'/user/editlib.php'); require_once($CFG->dirroot.'/user/lib.php'); $userid = optional_param('id', $USER->id, PARAM_INT); // User id. $courseid = optional_param('course', SITEID, PARAM_INT); // Course id (defaults to Site). $PAGE->set_url('/user/language.php', array('id' => $userid, 'course' => $courseid)); list($user, $course) = useredit_setup_preference_page($userid, $courseid); // Create form. $languageform = new user_edit_language_form(null, array('userid' => $user->id)); $languageform->set_data($user); $redirect = new moodle_url("/user/preferences.php", array('userid' => $user->id)); if ($languageform->is_cancelled()) { redirect($redirect); } else if ($data = $languageform->get_data()) { $lang = $data->lang; // If the specified language does not exist, use the site default. if (!get_string_manager()->translation_exists($lang, false)) { $lang = core_user::get_property_default('lang'); } $user->lang = $lang; // Update user with new language. user_update_user($user, false, false); // Trigger event. \core\event\user_updated::create_from_userid($user->id)->trigger(); if ($USER->id == $user->id) { $USER->lang = $lang; } redirect($redirect, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } // Display page header. $streditmylanguage = get_string('preferredlanguage'); $userfullname = fullname($user, true); $PAGE->navbar->includesettingsbase = true; $PAGE->set_title("$course->shortname: $streditmylanguage"); $PAGE->set_heading($userfullname); echo $OUTPUT->header(); echo $OUTPUT->heading($streditmylanguage); // Finally display THE form. $languageform->display(); // And proper footer. echo $OUTPUT->footer(); selector/search.php 0000644 00000005476 15151162244 0010357 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Code to search for users in response to an ajax call from a user selector. * * @package core_user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('AJAX_SCRIPT', true); require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/user/selector/lib.php'); $PAGE->set_context(context_system::instance()); $PAGE->set_url('/user/selector/search.php'); echo $OUTPUT->header(); // Check access. require_login(); require_sesskey(); // Get the search parameter. $search = required_param('search', PARAM_RAW); // Get and validate the selectorid parameter. $selectorhash = required_param('selectorid', PARAM_ALPHANUM); if (!isset($USER->userselectors[$selectorhash])) { throw new \moodle_exception('unknownuserselector'); } // Get the options. $options = $USER->userselectors[$selectorhash]; // Create the appropriate userselector. $classname = $options['class']; unset($options['class']); $name = $options['name']; unset($options['name']); if (isset($options['file'])) { require_once($CFG->dirroot . '/' . $options['file']); unset($options['file']); } $userselector = new $classname($name, $options); // Do the search and output the results. $results = $userselector->find_users($search); $jsonresults = array(); foreach ($results as $groupname => $users) { $groupdata = array('name' => $groupname, 'users' => array()); foreach ($users as $user) { $output = new stdClass; $output->id = $user->id; $output->name = $userselector->output_user($user); if (!empty($user->disabled)) { $output->disabled = true; } if (!empty($user->infobelow)) { $output->infobelow = $user->infobelow; } $groupdata['users'][] = $output; } $jsonresults[] = $groupdata; } $json = array('results' => $jsonresults); // Also add users' group membership summaries, if possible. if (is_callable(array($userselector, 'get_user_summaries')) && isset($options['courseid'])) { $json['userSummaries'] = $userselector->get_user_summaries($options['courseid']); } echo json_encode($json); selector/module.js 0000644 00000037303 15151162244 0010216 0 ustar 00 /** * JavaScript for the user selectors. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package userselector */ // Define the core_user namespace if it has not already been defined M.core_user = M.core_user || {}; // Define a user selectors array for against the cure_user namespace M.core_user.user_selectors = []; /** * Retrieves an instantiated user selector or null if there isn't one by the requested name * @param {string} name The name of the selector to retrieve * @return bool */ M.core_user.get_user_selector = function (name) { return this.user_selectors[name] || null; }; /** * Initialise a new user selector. * * @param {YUI} Y The YUI3 instance * @param {string} name the control name/id. * @param {string} hash the hash that identifies this selector in the user's session. * @param {array} extrafields extra fields we are displaying for each user in addition to fullname. * @param {string} lastsearch The last search that took place */ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearch) { // Creates a new user_selector object var user_selector = { /** This id/name used for this control in the HTML. */ name : name, /** Array of fields to display for each user, in addition to fullname. */ extrafields: extrafields, /** Number of seconds to delay before submitting a query request */ querydelay : 0.5, /** The input element that contains the search term. */ searchfield : Y.one('#' + name + '_searchtext'), /** The clear button. */ clearbutton : null, /** The select element that contains the list of users. */ listbox : Y.one('#' + name), /** Used to hold the timeout id of the timeout that waits before doing a search. */ timeoutid : null, /** Stores any in-progress remote requests. */ iotransactions : {}, /** The last string that we searched for, so we can avoid unnecessary repeat searches. */ lastsearch : lastsearch, /** Whether any options where selected last time we checked. Used by * handle_selection_change to track when this status changes. */ selectionempty : true, /** * Initialises the user selector object * @constructor */ init : function() { // Hide the search button and replace it with a label. var searchbutton = Y.one('#' + this.name + '_searchbutton'); this.searchfield.insert(Y.Node.create('<label for="' + this.name + '_searchtext">' + searchbutton.get('value') + '</label>'), this.searchfield); searchbutton.remove(); // Hook up the event handler for when the search text changes. this.searchfield.on('keyup', this.handle_keyup, this); // Hook up the event handler for when the selection changes. this.listbox.on('keyup', this.handle_selection_change, this); this.listbox.on('click', this.handle_selection_change, this); this.listbox.on('change', this.handle_selection_change, this); // And when the search any substring preference changes. Do an immediate re-search. Y.one('#userselector_searchanywhereid').on('click', this.handle_searchanywhere_change, this); // Define our custom event. //this.createEvent('selectionchanged'); this.selectionempty = this.is_selection_empty(); // Replace the Clear submit button with a clone that is not a submit button. var clearbtn = Y.one('#' + this.name + '_clearbutton'); this.clearbutton = Y.Node.create('<input type="button" value="' + clearbtn.get('value') + '" class="btn btn-secondary mx-1"/>'); clearbtn.replace(Y.Node.getDOMNode(this.clearbutton)); this.clearbutton.set('id', this.name + "_clearbutton"); this.clearbutton.on('click', this.handle_clear, this); this.clearbutton.set('disabled', (this.get_search_text() == '')); this.send_query(false); }, /** * Key up hander for the search text box. * @param {Y.Event} e the keyup event. */ handle_keyup : function(e) { // Trigger an ajax search after a delay. this.cancel_timeout(); this.timeoutid = Y.later(this.querydelay * 1000, e, function(obj){obj.send_query(false)}, this); // Enable or diable the clear button. this.clearbutton.set('disabled', (this.get_search_text() == '')); // If enter was pressed, prevent a form submission from happening. if (e.keyCode == 13) { e.halt(); } }, /** * Handles when the selection has changed. If the selection has changed from * empty to not-empty, or vice versa, then fire the event handlers. */ handle_selection_change : function() { var isselectionempty = this.is_selection_empty(); if (isselectionempty !== this.selectionempty) { this.fire('user_selector:selectionchanged', isselectionempty); } this.selectionempty = isselectionempty; }, /** * Trigger a re-search when the 'search any substring' option is changed. */ handle_searchanywhere_change : function() { if (this.lastsearch != '' && this.get_search_text() != '') { this.send_query(true); } }, /** * Click handler for the clear button.. */ handle_clear : function() { this.searchfield.set('value', ''); this.clearbutton.set('disabled',true); this.send_query(false); }, /** * Fires off the ajax search request. */ send_query : function(forceresearch) { // Cancel any pending timeout. this.cancel_timeout(); var value = this.get_search_text(); this.searchfield.set('class', ''); if (this.lastsearch == value && !forceresearch) { return; } // Try to cancel existing transactions. Y.Object.each(this.iotransactions, function(trans) { trans.abort(); }); var iotrans = Y.io(M.cfg.wwwroot + '/user/selector/search.php', { method: 'POST', data: 'selectorid=' + hash + '&sesskey=' + M.cfg.sesskey + '&search=' + value + '&userselector_searchanywhere=' + this.get_option('searchanywhere'), on: { complete: this.handle_response }, context:this }); this.iotransactions[iotrans.id] = iotrans; this.lastsearch = value; this.listbox.setStyle('background','url(' + M.util.image_url('i/loading', 'moodle') + ') no-repeat center center'); }, /** * Handle what happens when we get some data back from the search. * @param {int} requestid not used. * @param {object} response the list of users that was returned. */ handle_response : function(requestid, response) { try { delete this.iotransactions[requestid]; if (!Y.Object.isEmpty(this.iotransactions)) { // More searches pending. Wait until they are all done. return; } this.listbox.setStyle('background',''); var data = Y.JSON.parse(response.responseText); if (data.error) { this.searchfield.addClass('error'); return new M.core.ajaxException(data); } this.output_options(data); // If updated userSummaries are present, overwrite the global variable // that's output by group_non_members_selector::print_user_summaries() in user/selector/lib.php if (typeof data.userSummaries !== "undefined") { /* global userSummaries:true */ /* exported userSummaries */ userSummaries = data.userSummaries; } } catch (e) { this.listbox.setStyle('background',''); this.searchfield.addClass('error'); return new M.core.exception(e); } }, /** * This method should do the same sort of thing as the PHP method * user_selector_base::output_options. * @param {object} data the list of users to populate the list box with. */ output_options : function(data) { // Clear out the existing options, keeping any ones that are already selected. var selectedusers = {}; this.listbox.all('optgroup').each(function(optgroup){ optgroup.all('option').each(function(option){ if (option.get('selected')) { selectedusers[option.get('value')] = { id : option.get('value'), name : option.get('innerText') || option.get('textContent'), disabled: option.get('disabled') } } option.remove(); }, this); optgroup.remove(); }, this); // Output each optgroup. var count = 0; for (var key in data.results) { var groupdata = data.results[key]; this.output_group(groupdata.name, groupdata.users, selectedusers, true); count ++; } if (!count) { var searchstr = (this.lastsearch != '') ? this.insert_search_into_str(M.util.get_string('nomatchingusers', 'moodle'), this.lastsearch) : M.util.get_string('none', 'moodle'); this.output_group(searchstr, {}, selectedusers, true) } // If there were previously selected users who do not match the search, show them too. if (this.get_option('preserveselected') && selectedusers) { this.output_group(this.insert_search_into_str(M.util.get_string('previouslyselectedusers', 'moodle'), this.lastsearch), selectedusers, true, false); } this.handle_selection_change(); }, /** * This method should do the same sort of thing as the PHP method * user_selector_base::output_optgroup. * * @param {string} groupname the label for this optgroup.v * @param {object} users the users to put in this optgroup. * @param {boolean|object} selectedusers if true, select the users in this group. * @param {boolean} processsingle */ output_group : function(groupname, users, selectedusers, processsingle) { var optgroup = Y.Node.create('<optgroup></optgroup>'); this.listbox.append(optgroup); var count = 0; for (var key in users) { var user = users[key]; var option = Y.Node.create('<option value="' + user.id + '">' + user.name + '</option>'); if (user.disabled) { option.setAttribute('disabled', 'disabled'); } else if (selectedusers === true || selectedusers[user.id]) { option.setAttribute('selected', 'selected'); delete selectedusers[user.id]; } optgroup.append(option); if (user.infobelow) { extraoption = Y.Node.create('<option disabled="disabled" class="userselector-infobelow"/>'); extraoption.appendChild(document.createTextNode(user.infobelow)); optgroup.append(extraoption); } count ++; } if (count > 0) { optgroup.set('label', groupname + ' (' + count + ')'); if (processsingle && count === 1 && this.get_option('autoselectunique') && option.get('disabled') == false) { option.setAttribute('selected', 'selected'); } } else { optgroup.set('label', groupname); optgroup.append(Y.Node.create('<option disabled="disabled">\u00A0</option>')); } }, /** * Replace * @param {string} str * @param {string} search The search term * @return string */ insert_search_into_str : function(str, search) { return str.replace("%%SEARCHTERM%%", search); }, /** * Gets the search text * @return String the value to search for, with leading and trailing whitespace trimmed. */ get_search_text : function() { return this.searchfield.get('value').toString().replace(/^ +| +$/, ''); }, /** * Returns true if the selection is empty (nothing is selected) * @return Boolean check all the options and return whether any are selected. */ is_selection_empty : function() { var selection = false; this.listbox.all('option').each(function(){ if (this.get('selected')) { selection = true; } }); return !(selection); }, /** * Cancel the search delay timeout, if there is one. */ cancel_timeout : function() { if (this.timeoutid) { clearTimeout(this.timeoutid); this.timeoutid = null; } }, /** * @param {string} name The name of the option to retrieve * @return the value of one of the option checkboxes. */ get_option : function(name) { var checkbox = Y.one('#userselector_' + name + 'id'); if (checkbox) { return (checkbox.get('checked')); } else { return false; } } }; // Augment the user selector with the EventTarget class so that we can use // custom events Y.augment(user_selector, Y.EventTarget, null, null, {}); // Initialise the user selector user_selector.init(); // Store the user selector so that it can be retrieved this.user_selectors[name] = user_selector; // Return the user selector return user_selector; }; /** * Initialise a class that updates the user's preferences when they change one of * the options checkboxes. * @constructor * @param {YUI} Y * @return Tracker object */ M.core_user.init_user_selector_options_tracker = function(Y) { // Create a user selector options tracker var user_selector_options_tracker = { /** * Initlises the option tracker and gets everything going. * @constructor */ init : function() { var settings = [ 'userselector_preserveselected', 'userselector_autoselectunique', 'userselector_searchanywhere' ]; for (var s in settings) { var setting = settings[s]; Y.one('#' + setting + 'id').on('click', this.set_user_preference, this, setting); } }, /** * Sets a user preference for the options tracker * @param {Y.Event|null} e * @param {string} name The name of the preference to set */ set_user_preference : function(e, name) { M.util.set_user_preference(name, Y.one('#' + name + 'id').get('checked')); } }; // Initialise the options tracker user_selector_options_tracker.init(); // Return it just incase it is ever wanted return user_selector_options_tracker; }; selector/lib.php 0000644 00000113502 15151162244 0007646 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Code for ajax user selectors. * * @package core_user * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * The default size of a user selector. */ define('USER_SELECTOR_DEFAULT_ROWS', 20); /** * Base class for user selectors. * * In your theme, you must give each user-selector a defined width. If the * user selector has name="myid", then the div myid_wrapper must have a width * specified. * * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class user_selector_base { /** @var string The control name (and id) in the HTML. */ protected $name; /** @var array Extra fields to search on and return in addition to firstname and lastname. */ protected $extrafields; /** @var object Context used for capability checks regarding this selector (does * not necessarily restrict user list) */ protected $accesscontext; /** @var boolean Whether the conrol should allow selection of many users, or just one. */ protected $multiselect = true; /** @var int The height this control should have, in rows. */ protected $rows = USER_SELECTOR_DEFAULT_ROWS; /** @var array A list of userids that should not be returned by this control. */ protected $exclude = array(); /** @var array|null A list of the users who are selected. */ protected $selected = null; /** @var boolean When the search changes, do we keep previously selected options that do * not match the new search term? */ protected $preserveselected = false; /** @var boolean If only one user matches the search, should we select them automatically. */ protected $autoselectunique = false; /** @var boolean When searching, do we only match the starts of fields (better performance) * or do we match occurrences anywhere? */ protected $searchanywhere = false; /** @var mixed This is used by get selected users */ protected $validatinguserids = null; /** @var boolean Used to ensure we only output the search options for one user selector on * each page. */ private static $searchoptionsoutput = false; /** @var array JavaScript YUI3 Module definition */ protected static $jsmodule = array( 'name' => 'user_selector', 'fullpath' => '/user/selector/module.js', 'requires' => array('node', 'event-custom', 'datasource', 'json', 'moodle-core-notification'), 'strings' => array( array('previouslyselectedusers', 'moodle', '%%SEARCHTERM%%'), array('nomatchingusers', 'moodle', '%%SEARCHTERM%%'), array('none', 'moodle') )); /** @var int this is used to define maximum number of users visible in list */ public $maxusersperpage = 100; /** @var boolean Whether to override fullname() */ public $viewfullnames = false; /** @var boolean Whether to include custom user profile fields */ protected $includecustomfields = false; /** @var string User fields selects for custom fields. */ protected $userfieldsselects = ''; /** @var string User fields join for custom fields. */ protected $userfieldsjoin = ''; /** @var array User fields params for custom fields. */ protected $userfieldsparams = []; /** @var array User fields mappings for custom fields. */ protected $userfieldsmappings = []; /** * Constructor. Each subclass must have a constructor with this signature. * * @param string $name the control name/id for use in the HTML. * @param array $options other options needed to construct this selector. * You must be able to clone a userselector by doing new get_class($us)($us->get_name(), $us->get_options()); */ public function __construct($name, $options = array()) { global $CFG, $PAGE; // Initialise member variables from constructor arguments. $this->name = $name; // Use specified context for permission checks, system context if not specified. if (isset($options['accesscontext'])) { $this->accesscontext = $options['accesscontext']; } else { $this->accesscontext = context_system::instance(); } $this->viewfullnames = has_capability('moodle/site:viewfullnames', $this->accesscontext); // Check if some legacy code tries to override $CFG->showuseridentity. if (isset($options['extrafields'])) { debugging('The user_selector classes do not support custom list of extra identity fields any more. '. 'Instead, the user identity fields defined by the site administrator will be used to respect '. 'the configured privacy setting.', DEBUG_DEVELOPER); unset($options['extrafields']); } if (isset($options['includecustomfields'])) { $this->includecustomfields = $options['includecustomfields']; } else { $this->includecustomfields = false; } // Populate the list of additional user identifiers to display. if ($this->includecustomfields) { $userfieldsapi = \core_user\fields::for_identity($this->accesscontext)->with_name(); $this->extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); [ 'selects' => $this->userfieldsselects, 'joins' => $this->userfieldsjoin, 'params' => $this->userfieldsparams, 'mappings' => $this->userfieldsmappings ] = (array) $userfieldsapi->get_sql('u', true, '', '', false); } else { $this->extrafields = \core_user\fields::get_identity_fields($this->accesscontext, false); } if (isset($options['exclude']) && is_array($options['exclude'])) { $this->exclude = $options['exclude']; } if (isset($options['multiselect'])) { $this->multiselect = $options['multiselect']; } // Read the user prefs / optional_params that we use. $this->preserveselected = $this->initialise_option('userselector_preserveselected', $this->preserveselected); $this->autoselectunique = $this->initialise_option('userselector_autoselectunique', $this->autoselectunique); $this->searchanywhere = $this->initialise_option('userselector_searchanywhere', $this->searchanywhere); if (!empty($CFG->maxusersperpage)) { $this->maxusersperpage = $CFG->maxusersperpage; } } /** * All to the list of user ids that this control will not select. * * For example, on the role assign page, we do not list the users who already have the role in question. * * @param array $arrayofuserids the user ids to exclude. */ public function exclude($arrayofuserids) { $this->exclude = array_unique(array_merge($this->exclude, $arrayofuserids)); } /** * Clear the list of excluded user ids. */ public function clear_exclusions() { $this->exclude = array(); } /** * Returns the list of user ids that this control will not select. * * @return array the list of user ids that this control will not select. */ public function get_exclusions() { return clone($this->exclude); } /** * The users that were selected. * * This is a more sophisticated version of optional_param($this->name, array(), PARAM_INT) that validates the * returned list of ids against the rules for this user selector. * * @return array of user objects. */ public function get_selected_users() { // Do a lazy load. if (is_null($this->selected)) { $this->selected = $this->load_selected_users(); } return $this->selected; } /** * Convenience method for when multiselect is false (throws an exception if not). * * @throws moodle_exception * @return object the selected user object, or null if none. */ public function get_selected_user() { if ($this->multiselect) { throw new moodle_exception('cannotcallusgetselecteduser'); } $users = $this->get_selected_users(); if (count($users) == 1) { return reset($users); } else if (count($users) == 0) { return null; } else { throw new moodle_exception('userselectortoomany'); } } /** * Invalidates the list of selected users. * * If you update the database in such a way that it is likely to change the * list of users that this component is allowed to select from, then you * must call this method. For example, on the role assign page, after you have * assigned some roles to some users, you should call this. */ public function invalidate_selected_users() { $this->selected = null; } /** * Output this user_selector as HTML. * * @param boolean $return if true, return the HTML as a string instead of outputting it. * @return mixed if $return is true, returns the HTML as a string, otherwise returns nothing. */ public function display($return = false) { global $PAGE; // Get the list of requested users. $search = optional_param($this->name . '_searchtext', '', PARAM_RAW); if (optional_param($this->name . '_clearbutton', false, PARAM_BOOL)) { $search = ''; } $groupedusers = $this->find_users($search); // Output the select. $name = $this->name; $multiselect = ''; if ($this->multiselect) { $name .= '[]'; $multiselect = 'multiple="multiple" '; } $output = '<div class="userselector" id="' . $this->name . '_wrapper">' . "\n" . '<select name="' . $name . '" id="' . $this->name . '" ' . $multiselect . 'size="' . $this->rows . '" class="form-control no-overflow">' . "\n"; // Populate the select. $output .= $this->output_options($groupedusers, $search); // Output the search controls. $output .= "</select>\n<div class=\"form-inline\">\n"; $output .= '<input type="text" name="' . $this->name . '_searchtext" id="' . $this->name . '_searchtext" size="15" value="' . s($search) . '" class="form-control"/>'; $output .= '<input type="submit" name="' . $this->name . '_searchbutton" id="' . $this->name . '_searchbutton" value="' . $this->search_button_caption() . '" class="btn btn-secondary"/>'; $output .= '<input type="submit" name="' . $this->name . '_clearbutton" id="' . $this->name . '_clearbutton" value="' . get_string('clear') . '" class="btn btn-secondary"/>'; // And the search options. $optionsoutput = false; if (!user_selector_base::$searchoptionsoutput) { $output .= print_collapsible_region_start('', 'userselector_options', get_string('searchoptions'), 'userselector_optionscollapsed', true, true); $output .= $this->option_checkbox('preserveselected', $this->preserveselected, get_string('userselectorpreserveselected')); $output .= $this->option_checkbox('autoselectunique', $this->autoselectunique, get_string('userselectorautoselectunique')); $output .= $this->option_checkbox('searchanywhere', $this->searchanywhere, get_string('userselectorsearchanywhere')); $output .= print_collapsible_region_end(true); $PAGE->requires->js_init_call('M.core_user.init_user_selector_options_tracker', array(), false, self::$jsmodule); user_selector_base::$searchoptionsoutput = true; } $output .= "</div>\n</div>\n\n"; // Initialise the ajax functionality. $output .= $this->initialise_javascript($search); // Return or output it. if ($return) { return $output; } else { echo $output; } } /** * The height this control will be displayed, in rows. * * @param integer $numrows the desired height. */ public function set_rows($numrows) { $this->rows = $numrows; } /** * Returns the number of rows to display in this control. * * @return integer the height this control will be displayed, in rows. */ public function get_rows() { return $this->rows; } /** * Whether this control will allow selection of many, or just one user. * * @param boolean $multiselect true = allow multiple selection. */ public function set_multiselect($multiselect) { $this->multiselect = $multiselect; } /** * Returns true is multiselect should be allowed. * * @return boolean whether this control will allow selection of more than one user. */ public function is_multiselect() { return $this->multiselect; } /** * Returns the id/name of this control. * * @return string the id/name that this control will have in the HTML. */ public function get_name() { return $this->name; } /** * Set the user fields that are displayed in the selector in addition to the user's name. * * @param array $fields a list of field names that exist in the user table. */ public function set_extra_fields($fields) { debugging('The user_selector classes do not support custom list of extra identity fields any more. '. 'Instead, the user identity fields defined by the site administrator will be used to respect '. 'the configured privacy setting.', DEBUG_DEVELOPER); } /** * Search the database for users matching the $search string, and any other * conditions that apply. The SQL for testing whether a user matches the * search string should be obtained by calling the search_sql method. * * This method is used both when getting the list of choices to display to * the user, and also when validating a list of users that was selected. * * When preparing a list of users to choose from ($this->is_validating() * return false) you should probably have an maximum number of users you will * return, and if more users than this match your search, you should instead * return a message generated by the too_many_results() method. However, you * should not do this when validating. * * If you are writing a new user_selector subclass, I strongly recommend you * look at some of the subclasses later in this file and in admin/roles/lib.php. * They should help you see exactly what you have to do. * * @param string $search the search string. * @return array An array of arrays of users. The array keys of the outer * array should be the string names of optgroups. The keys of the inner * arrays should be userids, and the values should be user objects * containing at least the list of fields returned by the method * required_fields_sql(). If a user object has a ->disabled property * that is true, then that option will be displayed greyed out, and * will not be returned by get_selected_users. */ public abstract function find_users($search); /** * * Note: this function must be implemented if you use the search ajax field * (e.g. set $options['file'] = '/admin/filecontainingyourclass.php';) * @return array the options needed to recreate this user_selector. */ protected function get_options() { return array( 'class' => get_class($this), 'name' => $this->name, 'exclude' => $this->exclude, 'multiselect' => $this->multiselect, 'accesscontext' => $this->accesscontext, ); } /** * Returns true if this control is validating a list of users. * * @return boolean if true, we are validating a list of selected users, * rather than preparing a list of uesrs to choose from. */ protected function is_validating() { return !is_null($this->validatinguserids); } /** * Get the list of users that were selected by doing optional_param then validating the result. * * @return array of user objects. */ protected function load_selected_users() { // See if we got anything. if ($this->multiselect) { $userids = optional_param_array($this->name, array(), PARAM_INT); } else if ($userid = optional_param($this->name, 0, PARAM_INT)) { $userids = array($userid); } // If there are no users there is nobody to load. if (empty($userids)) { return array(); } // If we did, use the find_users method to validate the ids. $this->validatinguserids = $userids; $groupedusers = $this->find_users(''); $this->validatinguserids = null; // Aggregate the resulting list back into a single one. $users = array(); foreach ($groupedusers as $group) { foreach ($group as $user) { if (!isset($users[$user->id]) && empty($user->disabled) && in_array($user->id, $userids)) { $users[$user->id] = $user; } } } // If we are only supposed to be selecting a single user, make sure we do. if (!$this->multiselect && count($users) > 1) { $users = array_slice($users, 0, 1); } return $users; } /** * Returns SQL to select required fields. * * @param string $u the table alias for the user table in the query being * built. May be ''. * @return string fragment of SQL to go in the select list of the query. * @throws coding_exception if used when includecustomfields is true */ protected function required_fields_sql(string $u) { if ($this->includecustomfields) { throw new coding_exception('required_fields_sql() is not needed when includecustomfields is true, '. 'use $userfieldsselects instead.'); } // Raw list of fields. $fields = array('id'); // Add additional name fields. $fields = array_merge($fields, \core_user\fields::get_name_fields(), $this->extrafields); // Prepend the table alias. if ($u) { foreach ($fields as &$field) { $field = $u . '.' . $field; } } return implode(',', $fields); } /** * Returns an array with SQL to perform a search and the params that go into it. * * @param string $search the text to search for. * @param string $u the table alias for the user table in the query being * built. May be ''. * @return array an array with two elements, a fragment of SQL to go in the * where clause the query, and an array containing any required parameters. * this uses ? style placeholders. */ protected function search_sql(string $search, string $u): array { $extrafields = $this->includecustomfields ? array_values($this->userfieldsmappings) : $this->extrafields; return users_search_sql($search, $u, $this->searchanywhere, $extrafields, $this->exclude, $this->validatinguserids); } /** * Used to generate a nice message when there are too many users to show. * * The message includes the number of users that currently match, and the * text of the message depends on whether the search term is non-blank. * * @param string $search the search term, as passed in to the find users method. * @param int $count the number of users that currently match. * @return array in the right format to return from the find_users method. */ protected function too_many_results($search, $count) { if ($search) { $a = new stdClass; $a->count = $count; $a->search = $search; return array(get_string('toomanyusersmatchsearch', '', $a) => array(), get_string('pleasesearchmore') => array()); } else { return array(get_string('toomanyuserstoshow', '', $count) => array(), get_string('pleaseusesearch') => array()); } } /** * Output the list of <optgroup>s and <options>s that go inside the select. * * This method should do the same as the JavaScript method * user_selector.prototype.handle_response. * * @param array $groupedusers an array, as returned by find_users. * @param string $search * @return string HTML code. */ protected function output_options($groupedusers, $search) { $output = ''; // Ensure that the list of previously selected users is up to date. $this->get_selected_users(); // If $groupedusers is empty, make a 'no matching users' group. If there is // only one selected user, set a flag to select them if that option is turned on. $select = false; if (empty($groupedusers)) { if (!empty($search)) { $groupedusers = array(get_string('nomatchingusers', '', $search) => array()); } else { $groupedusers = array(get_string('none') => array()); } } else if ($this->autoselectunique && count($groupedusers) == 1 && count(reset($groupedusers)) == 1) { $select = true; if (!$this->multiselect) { $this->selected = array(); } } // Output each optgroup. foreach ($groupedusers as $groupname => $users) { $output .= $this->output_optgroup($groupname, $users, $select); } // If there were previously selected users who do not match the search, show them too. if ($this->preserveselected && !empty($this->selected)) { $output .= $this->output_optgroup(get_string('previouslyselectedusers', '', $search), $this->selected, true); } // This method trashes $this->selected, so clear the cache so it is rebuilt before anyone tried to use it again. $this->selected = null; return $output; } /** * Output one particular optgroup. Used by the preceding function output_options. * * @param string $groupname the label for this optgroup. * @param array $users the users to put in this optgroup. * @param boolean $select if true, select the users in this group. * @return string HTML code. */ protected function output_optgroup($groupname, $users, $select) { if (!empty($users)) { $output = ' <optgroup label="' . htmlspecialchars($groupname, ENT_COMPAT) . ' (' . count($users) . ')">' . "\n"; foreach ($users as $user) { $attributes = ''; if (!empty($user->disabled)) { $attributes .= ' disabled="disabled"'; } else if ($select || isset($this->selected[$user->id])) { $attributes .= ' selected="selected"'; } unset($this->selected[$user->id]); $output .= ' <option' . $attributes . ' value="' . $user->id . '">' . $this->output_user($user) . "</option>\n"; if (!empty($user->infobelow)) { // Poor man's indent here is because CSS styles do not work in select options, except in Firefox. $output .= ' <option disabled="disabled" class="userselector-infobelow">' . ' ' . s($user->infobelow) . '</option>'; } } } else { $output = ' <optgroup label="' . htmlspecialchars($groupname, ENT_COMPAT) . '">' . "\n"; $output .= ' <option disabled="disabled"> </option>' . "\n"; } $output .= " </optgroup>\n"; return $output; } /** * Convert a user object to a string suitable for displaying as an option in the list box. * * @param object $user the user to display. * @return string a string representation of the user. */ public function output_user($user) { $out = fullname($user, $this->viewfullnames); if ($this->extrafields) { $displayfields = array(); foreach ($this->extrafields as $field) { $displayfields[] = s($user->{$field}); } $out .= ' (' . implode(', ', $displayfields) . ')'; } return $out; } /** * Returns the string to use for the search button caption. * * @return string the caption for the search button. */ protected function search_button_caption() { return get_string('search'); } /** * Initialise one of the option checkboxes, either from the request, or failing that from the * user_preferences table, or finally from the given default. * * @param string $name * @param mixed $default * @return mixed|null|string */ private function initialise_option($name, $default) { $param = optional_param($name, null, PARAM_BOOL); if (is_null($param)) { return get_user_preferences($name, $default); } else { set_user_preference($name, $param); return $param; } } /** * Output one of the options checkboxes. * * @param string $name * @param string $on * @param string $label * @return string */ private function option_checkbox($name, $on, $label) { if ($on) { $checked = ' checked="checked"'; } else { $checked = ''; } $name = 'userselector_' . $name; // For the benefit of brain-dead IE, the id must be different from the name of the hidden form field above. // It seems that document.getElementById('frog') in IE will return and element with name="frog". $output = '<div class="form-check"><input type="hidden" name="' . $name . '" value="0" />' . '<label class="form-check-label" for="' . $name . 'id">' . '<input class="form-check-input" type="checkbox" id="' . $name . 'id" name="' . $name . '" value="1"' . $checked . ' /> ' . $label . "</label> </div>\n"; user_preference_allow_ajax_update($name, PARAM_BOOL); return $output; } /** * Initialises JS for this control. * * @param string $search * @return string any HTML needed here. */ protected function initialise_javascript($search) { global $USER, $PAGE, $OUTPUT; $output = ''; // Put the options into the session, to allow search.php to respond to the ajax requests. $options = $this->get_options(); $hash = md5(serialize($options)); $USER->userselectors[$hash] = $options; // Initialise the selector. $PAGE->requires->js_init_call( 'M.core_user.init_user_selector', array($this->name, $hash, $this->extrafields, $search), false, self::$jsmodule ); return $output; } } /** * Base class to avoid duplicating code. * * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class groups_user_selector_base extends user_selector_base { /** @var int */ protected $groupid; /** @var int */ protected $courseid; /** * Constructor. * * @param string $name control name * @param array $options should have two elements with keys groupid and courseid. */ public function __construct($name, $options) { global $CFG; $options['accesscontext'] = context_course::instance($options['courseid']); $options['includecustomfields'] = true; parent::__construct($name, $options); $this->groupid = $options['groupid']; $this->courseid = $options['courseid']; require_once($CFG->dirroot . '/group/lib.php'); } /** * Returns options for this selector. * @return array */ protected function get_options() { $options = parent::get_options(); $options['groupid'] = $this->groupid; $options['courseid'] = $this->courseid; return $options; } /** * Creates an organised array from given data. * * @param array $roles array in the format returned by groups_calculate_role_people. * @param string $search * @return array array in the format find_users is supposed to return. */ protected function convert_array_format($roles, $search) { if (empty($roles)) { $roles = array(); } $groupedusers = array(); foreach ($roles as $role) { if ($search) { $a = new stdClass; $a->role = $role->name; $a->search = $search; $groupname = get_string('matchingsearchandrole', '', $a); } else { $groupname = $role->name; } $groupedusers[$groupname] = $role->users; foreach ($groupedusers[$groupname] as &$user) { unset($user->roles); $user->fullname = fullname($user); if (!empty($user->component)) { $user->infobelow = get_string('addedby', 'group', get_string('pluginname', $user->component)); } } } return $groupedusers; } } /** * User selector subclass for the list of users who are in a certain group. * * Used on the add group memebers page. * * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_members_selector extends groups_user_selector_base { /** * Finds users to display in this control. * @param string $search * @return array */ public function find_users($search) { list($wherecondition, $params) = $this->search_sql($search, 'u'); list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext, $this->userfieldsmappings); $roles = groups_get_members_by_role($this->groupid, $this->courseid, $this->userfieldsselects . ', gm.component', $sort, $wherecondition, array_merge($params, $sortparams, $this->userfieldsparams), $this->userfieldsjoin); return $this->convert_array_format($roles, $search); } } /** * User selector subclass for the list of users who are not in a certain group. * * Used on the add group members page. * * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_non_members_selector extends groups_user_selector_base { /** * An array of user ids populated by find_users() used in print_user_summaries() * @var array */ private $potentialmembersids = array(); /** * Output user. * * @param stdClass $user * @return string */ public function output_user($user) { return parent::output_user($user) . ' (' . $user->numgroups . ')'; } /** * Returns the user selector JavaScript module * @return array */ public function get_js_module() { return self::$jsmodule; } /** * Creates a global JS variable (userSummaries) that is used by the group selector * to print related information when the user clicks on a user in the groups UI. * * Used by /group/clientlib.js * * @global moodle_page $PAGE * @param int $courseid */ public function print_user_summaries($courseid) { global $PAGE; $usersummaries = $this->get_user_summaries($courseid); $PAGE->requires->data_for_js('userSummaries', $usersummaries); } /** * Construct HTML lists of group-memberships of the current set of users. * * Used in user/selector/search.php to repopulate the userSummaries JS global * that is created in self::print_user_summaries() above. * * @param int $courseid The course * @return string[] Array of HTML lists of groups. */ public function get_user_summaries($courseid) { global $DB; $usersummaries = array(); // Get other groups user already belongs to. $usergroups = array(); $potentialmembersids = $this->potentialmembersids; if (empty($potentialmembersids) == false) { list($membersidsclause, $params) = $DB->get_in_or_equal($potentialmembersids, SQL_PARAMS_NAMED, 'pm'); $sql = "SELECT u.id AS userid, g.* FROM {user} u JOIN {groups_members} gm ON u.id = gm.userid JOIN {groups} g ON gm.groupid = g.id WHERE u.id $membersidsclause AND g.courseid = :courseid "; $params['courseid'] = $courseid; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $usergroup) { $usergroups[$usergroup->userid][$usergroup->id] = $usergroup; } $rs->close(); foreach ($potentialmembersids as $userid) { if (isset($usergroups[$userid])) { $usergrouplist = html_writer::start_tag('ul'); foreach ($usergroups[$userid] as $groupitem) { $usergrouplist .= html_writer::tag('li', format_string($groupitem->name)); } $usergrouplist .= html_writer::end_tag('ul'); } else { $usergrouplist = ''; } $usersummaries[] = $usergrouplist; } } return $usersummaries; } /** * Finds users to display in this control. * * @param string $search * @return array */ public function find_users($search) { global $DB; // Get list of allowed roles. $context = context_course::instance($this->courseid); if ($validroleids = groups_get_possible_roles($context)) { list($roleids, $roleparams) = $DB->get_in_or_equal($validroleids, SQL_PARAMS_NAMED, 'r'); } else { $roleids = " = -1"; $roleparams = array(); } // We want to query both the current context and parent contexts. list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); // Get the search condition. list($searchcondition, $searchparams) = $this->search_sql($search, 'u'); // Build the SQL. $enrolledjoin = get_enrolled_join($context, 'u.id'); $wheres = []; $wheres[] = $enrolledjoin->wheres; $wheres[] = 'u.deleted = 0'; $wheres[] = 'gm.id IS NULL'; $wheres = implode(' AND ', $wheres); $wheres .= ' AND ' . $searchcondition; $fields = "SELECT r.id AS roleid, u.id AS userid, " . $this->userfieldsselects . ", (SELECT count(igm.groupid) FROM {groups_members} igm JOIN {groups} ig ON igm.groupid = ig.id WHERE igm.userid = u.id AND ig.courseid = :courseid) AS numgroups"; $sql = " FROM {user} u $enrolledjoin->joins LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid $relatedctxsql AND ra.roleid $roleids) LEFT JOIN {role} r ON r.id = ra.roleid LEFT JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid) $this->userfieldsjoin WHERE $wheres"; list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext, $this->userfieldsmappings); $orderby = ' ORDER BY ' . $sort; $params = array_merge($searchparams, $roleparams, $relatedctxparams, $enrolledjoin->params, $this->userfieldsparams); $params['courseid'] = $this->courseid; $params['groupid'] = $this->groupid; if (!$this->is_validating()) { $potentialmemberscount = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $sql", $params); if ($potentialmemberscount > $this->maxusersperpage) { return $this->too_many_results($search, $potentialmemberscount); } } $rs = $DB->get_recordset_sql("$fields $sql $orderby", array_merge($params, $sortparams)); $roles = groups_calculate_role_people($rs, $context); // Don't hold onto user IDs if we're doing validation. if (empty($this->validatinguserids) ) { if ($roles) { foreach ($roles as $k => $v) { if ($v) { foreach ($v->users as $uid => $userobject) { $this->potentialmembersids[] = $uid; } } } } } return $this->convert_array_format($roles, $search); } } forum.php 0000644 00000006543 15151162244 0006416 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Allows you to edit a users forum preferences * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once('../config.php'); require_once($CFG->libdir.'/gdlib.php'); require_once($CFG->dirroot.'/user/forum_form.php'); require_once($CFG->dirroot.'/user/editlib.php'); require_once($CFG->dirroot.'/user/lib.php'); $userid = optional_param('id', $USER->id, PARAM_INT); // User id. $courseid = optional_param('course', SITEID, PARAM_INT); // Course id (defaults to Site). $PAGE->set_url('/user/forum.php', array('id' => $userid, 'course' => $courseid)); list($user, $course) = useredit_setup_preference_page($userid, $courseid); // Create form. $forumform = new user_edit_forum_form(null, array('userid' => $user->id)); $user->markasreadonnotification = get_user_preferences('forum_markasreadonnotification', 1, $user->id); $user->useexperimentalui = get_user_preferences('forum_useexperimentalui', 0, $user->id); $forumform->set_data($user); $redirect = new moodle_url("/user/preferences.php", array('userid' => $user->id)); if ($forumform->is_cancelled()) { redirect($redirect); } else if ($data = $forumform->get_data()) { $user->maildigest = $data->maildigest; $user->autosubscribe = $data->autosubscribe; $user->preference_forum_useexperimentalui = $data->useexperimentalui; if (!empty($CFG->forum_trackreadposts)) { $user->trackforums = $data->trackforums; if (property_exists($data, 'markasreadonnotification')) { $user->preference_forum_markasreadonnotification = $data->markasreadonnotification; } } unset($user->markasreadonnotification); useredit_update_user_preference($user); user_update_user($user, false, false); // Trigger event. \core\event\user_updated::create_from_userid($user->id)->trigger(); if ($USER->id == $user->id) { $USER->maildigest = $data->maildigest; $USER->autosubscribe = $data->autosubscribe; if (!empty($CFG->forum_trackreadposts)) { $USER->trackforums = $data->trackforums; } } redirect($redirect, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } // Display page header. $streditmyforum = get_string('forumpreferences'); $userfullname = fullname($user, true); $PAGE->navbar->includesettingsbase = true; $PAGE->add_body_class('limitedwidth'); $PAGE->set_title("$course->shortname: $streditmyforum"); $PAGE->set_heading($userfullname); echo $OUTPUT->header(); echo $OUTPUT->heading($streditmyforum); // Finally display THE form. $forumform->display(); // And proper footer. echo $OUTPUT->footer(); upgrade.txt 0000644 00000013143 15151162244 0006737 0 ustar 00 This files describes API changes for code that uses the user API. === 4.1.3 === * External function core_user_external::add_user_private_files() now returns moodle_exception when the user quota is exceeded === 4.1.2 === * New method `core_user::is_current_user`, useful for components implementing permission callbacks for their preferences * New `profile_get_user_field` method for returning profile field instance of given type * The `profile_field_base::is_visible` method now accepts an optional `$context` argument * Added get_internalfield_list() and get_internalfields() in the user_field_mapping class. The get_internalfield_list() returns data in an array by grouping profile fields based on field categories, used for internal field name dropdown in the user field mapping of Oauth2 services The get_internalfields() converts the result from get_internalfield_list() into flat array, used to save/update the profile data when a user uses OAuth2 services. * Added get_profile_field_names() and get_profile_field_list() in the profile_field_base class. The get_profile_field_names() returns the list of valid custom profile user fields. The get_profile_field_list() returns the profile fields in a format that can be used for choices in a group select menu. === 4.1 === * Added a new method is_transform_supported() in the profile_field_base class. The purpose is to allow the field to be transformed during the export process. It has been implemented in the Date/Time data type (Applied in 4.1, 4.0.6). * user_get_user_details_courses() now accepts an optional second parameter, an array of userfields that should be returned. The values passed into the $userfields parameter must all be included in the return from user_get_default_fields(). It also allows you to reduce how much of a user record is required by the method. The minimum user record fields are: * id * deleted * all potential fullname fields * Participant filter is moved to core as an API which can be used in different areas of core by implementing the API and filterable objects. As a part of making the API mature as a core one, these are the js files moved from core user to core library: * user/amd/src/local/participantsfilter/filter.js → lib/amd/src/datafilter/filtertype.js * user/amd/src/local/participantsfilter/filtertypes/country.js → lib/amd/src/datafilter/filtertypes/country.js * user/amd/src/local/participantsfilter/filtertypes/courseid.js → lib/amd/src/datafilter/filtertypes/courseid.js * user/amd/src/local/participantsfilter/filtertypes/keyword.js → lib/amd/src/datafilter/filtertypes/keyword.js * user/amd/src/local/participantsfilter/selectors.js → lib/amd/src/datafilter/selectors.js The following mustache have been moved from core user to core library: * user/templates/local/participantsfilter/filterrow.mustache → lib/templates/datafilter/filter_row.mustache * user/templates/local/participantsfilter/filtertype.mustache → lib/templates/datafilter/filter_type.mustache * user/templates/local/participantsfilter/filtertypes.mustache → lib/templates/datafilter/filter_types.mustache * user/templates/local/participantsfilter/autocomplete_layout.mustache → lib/templates/datafilter/autocomplete_layout.mustache * user/templates/local/participantsfilter/autocomplete_selection.mustache → lib/templates/datafilter/autocomplete_selection.mustache * user/templates/local/participantsfilter/autocomplete_selection_items.mustache → lib/templates/datafilter/autocomplete_selection_items.mustache Class participant_filter now extends core filter api in core user. * The unified_filter function has been finally deprecated and cannot be used anymore * The class \core_user\output\unified_filter has been finally deprecated and removed === 4.0 === * External function core_user_external::update_users() will now fail on a per user basis. Previously if one user update failed all users in the operation would fail. * External function core_user_external::update_users() now returns an error code and message to why a user update action failed. * New method `core_user\fields::get_sql_fullname` for retrieving user fullname format in SQL statement * The `profile_get_custom_field_data_by_shortname` method now accepts an optional parameter to determine whether to use case-sensitive matching of the profile field shortname or not (default true) === 3.11 === * Added new core_user/form_user_selector JS module that can be used as the 'ajax' handler for the autocomplete form element implementing the user selector. * Added new external function core_user_external::search_identity(). The main purpose of this external function is to provide data for asynchronous user selectors and similar widgets. It allows to search users matching the given query in their name or other available identity fields. === 3.9 === * The unified filter has been replaced by the participants filter. The following have therefore been deprecated: * Library functions: * user_get_participants_sql * user_get_total_participants * user_get_participants * Unified filter renderer (core_user_renderer::unified_filter) * Unified filter renderable (\core_user\output\unified_filter) * Unified filter JavaScript (core_user/unified_filter.js and core_user/unified_filter_datasource.js) * Unified filter template (unified_filter.mustache) === 3.6 === * The following functions have been finally deprecated and can not be used anymore: * useredit_update_picture() * core_user_external::update_user_preferences() now allows to unset existing preferences values. If the preference value field is not set, the preference will be unset. profile.php 0000644 00000021116 15151162244 0006717 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Public Profile -- a user's public profile page * * - each user can currently have their own page (cloned from system and then customised) * - users can add any blocks they want * - the administrators can define a default site public profile for users who have * not created their own public profile * * This script implements the user's view of the public profile, and allows editing * of the public profile. * * @package core_user * @copyright 2010 Remote-Learner.net * @author Hubert Chathi <hubert@remote-learner.net> * @author Olav Jordan <olav.jordan@remote-learner.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../config.php'); require_once($CFG->dirroot . '/my/lib.php'); require_once($CFG->dirroot . '/user/profile/lib.php'); require_once($CFG->dirroot . '/user/lib.php'); require_once($CFG->libdir.'/filelib.php'); $userid = optional_param('id', 0, PARAM_INT); $edit = optional_param('edit', null, PARAM_BOOL); // Turn editing on and off. $reset = optional_param('reset', null, PARAM_BOOL); $PAGE->set_url('/user/profile.php', array('id' => $userid)); if (!empty($CFG->forceloginforprofiles)) { require_login(); if (isguestuser()) { $PAGE->set_context(context_system::instance()); echo $OUTPUT->header(); echo $OUTPUT->confirm(get_string('guestcantaccessprofiles', 'error'), get_login_url(), $CFG->wwwroot); echo $OUTPUT->footer(); die; } } else if (!empty($CFG->forcelogin)) { require_login(); } $userid = $userid ? $userid : $USER->id; // Owner of the page. if ((!$user = $DB->get_record('user', array('id' => $userid))) || ($user->deleted)) { $PAGE->set_context(context_system::instance()); echo $OUTPUT->header(); if (!$user) { echo $OUTPUT->notification(get_string('invaliduser', 'error')); } else { echo $OUTPUT->notification(get_string('userdeleted')); } echo $OUTPUT->footer(); die; } $currentuser = ($user->id == $USER->id); $context = $usercontext = context_user::instance($userid, MUST_EXIST); if (!user_can_view_profile($user, null, $context)) { // Course managers can be browsed at site level. If not forceloginforprofiles, allow access (bug #4366). $struser = get_string('user'); $PAGE->set_context(context_system::instance()); $PAGE->set_title("$SITE->shortname: $struser"); // Do not leak the name. $PAGE->set_heading($struser); $PAGE->set_pagelayout('mypublic'); $PAGE->add_body_class('limitedwidth'); $PAGE->set_url('/user/profile.php', array('id' => $userid)); $PAGE->navbar->add($struser); echo $OUTPUT->header(); echo $OUTPUT->notification(get_string('usernotavailable', 'error')); echo $OUTPUT->footer(); exit; } // Get the profile page. Should always return something unless the database is broken. if (!$currentpage = my_get_page($userid, MY_PAGE_PUBLIC)) { throw new \moodle_exception('mymoodlesetup'); } $PAGE->set_context($context); $PAGE->set_pagelayout('mypublic'); $PAGE->add_body_class('limitedwidth'); $PAGE->set_pagetype('user-profile'); // Set up block editing capabilities. if (isguestuser()) { // Guests can never edit their profile. $USER->editing = $edit = 0; // Just in case. $PAGE->set_blocks_editing_capability('moodle/my:configsyspages'); // unlikely :). } else { if ($currentuser) { $PAGE->set_blocks_editing_capability('moodle/user:manageownblocks'); } else { $PAGE->set_blocks_editing_capability('moodle/user:manageblocks'); } } // Start setting up the page. $strpublicprofile = get_string('publicprofile'); $PAGE->blocks->add_region('content'); $PAGE->set_subpage($currentpage->id); $PAGE->set_title(fullname($user).": $strpublicprofile"); $PAGE->set_heading(fullname($user)); if (!$currentuser) { $PAGE->navigation->extend_for_user($user); if ($node = $PAGE->settingsnav->get('userviewingsettings'.$user->id)) { $node->forceopen = true; } } else if ($node = $PAGE->settingsnav->get('dashboard', navigation_node::TYPE_CONTAINER)) { $node->forceopen = true; } if ($node = $PAGE->settingsnav->get('root')) { $node->forceopen = false; } // Toggle the editing state and switches. if ($PAGE->user_allowed_editing()) { if ($reset !== null) { if (!is_null($userid)) { if (!$currentpage = my_reset_page($userid, MY_PAGE_PUBLIC, 'user-profile')) { throw new \moodle_exception('reseterror', 'my'); } redirect(new moodle_url('/user/profile.php', array('id' => $userid))); } } else if ($edit !== null) { // Editing state was specified. $USER->editing = $edit; // Change editing state. } else { // Editing state is in session. if ($currentpage->userid) { // It's a page we can edit, so load from session. if (!empty($USER->editing)) { $edit = 1; } else { $edit = 0; } } else { // For the page to display properly with the user context header the page blocks need to // be copied over to the user context. if (!$currentpage = my_copy_page($userid, MY_PAGE_PUBLIC, 'user-profile')) { throw new \moodle_exception('mymoodlesetup'); } $PAGE->set_context($usercontext); $PAGE->set_subpage($currentpage->id); // It's a system page and they are not allowed to edit system pages. $USER->editing = $edit = 0; // Disable editing completely, just to be safe. } } // Add button for editing page. $params = array('edit' => !$edit, 'id' => $userid); $resetbutton = ''; $resetstring = get_string('resetpage', 'my'); $reseturl = new moodle_url("$CFG->wwwroot/user/profile.php", array('edit' => 1, 'reset' => 1, 'id' => $userid)); if (!$currentpage->userid) { // Viewing a system page -- let the user customise it. $editstring = get_string('updatemymoodleon'); $params['edit'] = 1; } else if (empty($edit)) { $editstring = get_string('updatemymoodleon'); $resetbutton = $OUTPUT->single_button($reseturl, $resetstring); } else { $editstring = get_string('updatemymoodleoff'); $resetbutton = $OUTPUT->single_button($reseturl, $resetstring); } $url = new moodle_url("$CFG->wwwroot/user/profile.php", $params); $button = ''; if (!$PAGE->theme->haseditswitch) { $button = $OUTPUT->single_button($url, $editstring); } $PAGE->set_button($resetbutton . $button); } else { $USER->editing = $edit = 0; } // Trigger a user profile viewed event. profile_view($user, $usercontext); // TODO WORK OUT WHERE THE NAV BAR IS! echo $OUTPUT->header(); echo '<div class="userprofile">'; $hiddenfields = []; if (!has_capability('moodle/user:viewhiddendetails', $usercontext)) { $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); } if ($user->description && !isset($hiddenfields['description'])) { echo '<div class="description">'; if (!empty($CFG->profilesforenrolledusersonly) && !$currentuser && !$DB->record_exists('role_assignments', array('userid' => $user->id))) { echo get_string('profilenotshown', 'moodle'); } else { $user->description = file_rewrite_pluginfile_urls($user->description, 'pluginfile.php', $usercontext->id, 'user', 'profile', null); echo format_text($user->description, $user->descriptionformat); } echo '</div>'; } echo $OUTPUT->custom_block_region('content'); // Render custom blocks. $renderer = $PAGE->get_renderer('core_user', 'myprofile'); $tree = core_user\output\myprofile\manager::build_tree($user, $currentuser); echo $renderer->render($tree); echo '</div>'; // Userprofile class. echo $OUTPUT->footer(); externallib.php 0000644 00000254116 15151162244 0007600 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * External user API * * @package core_user * @category external * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once("$CFG->libdir/externallib.php"); /** * User external functions * * @package core_user * @category external * @copyright 2011 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.2 */ class core_user_external extends external_api { /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function create_users_parameters() { global $CFG; $userfields = [ 'createpassword' => new external_value(PARAM_BOOL, 'True if password should be created and mailed to user.', VALUE_OPTIONAL), // General. 'username' => new external_value(core_user::get_property_type('username'), 'Username policy is defined in Moodle security config.'), 'auth' => new external_value(core_user::get_property_type('auth'), 'Auth plugins include manual, ldap, etc', VALUE_DEFAULT, 'manual', core_user::get_property_null('auth')), 'password' => new external_value(core_user::get_property_type('password'), 'Plain text password consisting of any characters', VALUE_OPTIONAL), 'firstname' => new external_value(core_user::get_property_type('firstname'), 'The first name(s) of the user'), 'lastname' => new external_value(core_user::get_property_type('lastname'), 'The family name of the user'), 'email' => new external_value(core_user::get_property_type('email'), 'A valid and unique email address'), 'maildisplay' => new external_value(core_user::get_property_type('maildisplay'), 'Email visibility', VALUE_OPTIONAL), 'city' => new external_value(core_user::get_property_type('city'), 'Home city of the user', VALUE_OPTIONAL), 'country' => new external_value(core_user::get_property_type('country'), 'Home country code of the user, such as AU or CZ', VALUE_OPTIONAL), 'timezone' => new external_value(core_user::get_property_type('timezone'), 'Timezone code such as Australia/Perth, or 99 for default', VALUE_OPTIONAL), 'description' => new external_value(core_user::get_property_type('description'), 'User profile description, no HTML', VALUE_OPTIONAL), // Additional names. 'firstnamephonetic' => new external_value(core_user::get_property_type('firstnamephonetic'), 'The first name(s) phonetically of the user', VALUE_OPTIONAL), 'lastnamephonetic' => new external_value(core_user::get_property_type('lastnamephonetic'), 'The family name phonetically of the user', VALUE_OPTIONAL), 'middlename' => new external_value(core_user::get_property_type('middlename'), 'The middle name of the user', VALUE_OPTIONAL), 'alternatename' => new external_value(core_user::get_property_type('alternatename'), 'The alternate name of the user', VALUE_OPTIONAL), // Interests. 'interests' => new external_value(PARAM_TEXT, 'User interests (separated by commas)', VALUE_OPTIONAL), // Optional. 'idnumber' => new external_value(core_user::get_property_type('idnumber'), 'An arbitrary ID code number perhaps from the institution', VALUE_DEFAULT, ''), 'institution' => new external_value(core_user::get_property_type('institution'), 'institution', VALUE_OPTIONAL), 'department' => new external_value(core_user::get_property_type('department'), 'department', VALUE_OPTIONAL), 'phone1' => new external_value(core_user::get_property_type('phone1'), 'Phone 1', VALUE_OPTIONAL), 'phone2' => new external_value(core_user::get_property_type('phone2'), 'Phone 2', VALUE_OPTIONAL), 'address' => new external_value(core_user::get_property_type('address'), 'Postal address', VALUE_OPTIONAL), // Other user preferences stored in the user table. 'lang' => new external_value(core_user::get_property_type('lang'), 'Language code such as "en", must exist on server', VALUE_DEFAULT, core_user::get_property_default('lang'), core_user::get_property_null('lang')), 'calendartype' => new external_value(core_user::get_property_type('calendartype'), 'Calendar type such as "gregorian", must exist on server', VALUE_DEFAULT, $CFG->calendartype, VALUE_OPTIONAL), 'theme' => new external_value(core_user::get_property_type('theme'), 'Theme name such as "standard", must exist on server', VALUE_OPTIONAL), 'mailformat' => new external_value(core_user::get_property_type('mailformat'), 'Mail format code is 0 for plain text, 1 for HTML etc', VALUE_OPTIONAL), // Custom user profile fields. 'customfields' => new external_multiple_structure( new external_single_structure( [ 'type' => new external_value(PARAM_ALPHANUMEXT, 'The name of the custom field'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field') ] ), 'User custom fields (also known as user profil fields)', VALUE_OPTIONAL), // User preferences. 'preferences' => new external_multiple_structure( new external_single_structure( [ 'type' => new external_value(PARAM_RAW, 'The name of the preference'), 'value' => new external_value(PARAM_RAW, 'The value of the preference') ] ), 'User preferences', VALUE_OPTIONAL), ]; return new external_function_parameters( [ 'users' => new external_multiple_structure( new external_single_structure($userfields) ) ] ); } /** * Create one or more users. * * @throws invalid_parameter_exception * @param array $users An array of users to create. * @return array An array of arrays * @since Moodle 2.2 */ public static function create_users($users) { global $CFG, $DB; require_once($CFG->dirroot."/lib/weblib.php"); require_once($CFG->dirroot."/user/lib.php"); require_once($CFG->dirroot."/user/editlib.php"); require_once($CFG->dirroot."/user/profile/lib.php"); // Required for customfields related function. // Ensure the current user is allowed to run this function. $context = context_system::instance(); self::validate_context($context); require_capability('moodle/user:create', $context); // Do basic automatic PARAM checks on incoming data, using params description. // If any problems are found then exceptions are thrown with helpful error messages. $params = self::validate_parameters(self::create_users_parameters(), array('users' => $users)); $availableauths = core_component::get_plugin_list('auth'); unset($availableauths['mnet']); // These would need mnethostid too. unset($availableauths['webservice']); // We do not want new webservice users for now. $availablethemes = core_component::get_plugin_list('theme'); $availablelangs = get_string_manager()->get_list_of_translations(); $transaction = $DB->start_delegated_transaction(); $userids = array(); foreach ($params['users'] as $user) { // Make sure that the username, firstname and lastname are not blank. foreach (array('username', 'firstname', 'lastname') as $fieldname) { if (trim($user[$fieldname]) === '') { throw new invalid_parameter_exception('The field '.$fieldname.' cannot be blank'); } } // Make sure that the username doesn't already exist. if ($DB->record_exists('user', array('username' => $user['username'], 'mnethostid' => $CFG->mnet_localhost_id))) { throw new invalid_parameter_exception('Username already exists: '.$user['username']); } // Make sure auth is valid. if (empty($availableauths[$user['auth']])) { throw new invalid_parameter_exception('Invalid authentication type: '.$user['auth']); } // Make sure lang is valid. if (empty($availablelangs[$user['lang']])) { throw new invalid_parameter_exception('Invalid language code: '.$user['lang']); } // Make sure lang is valid. if (!empty($user['theme']) && empty($availablethemes[$user['theme']])) { // Theme is VALUE_OPTIONAL, // so no default value // We need to test if the client sent it // => !empty($user['theme']). throw new invalid_parameter_exception('Invalid theme: '.$user['theme']); } // Make sure we have a password or have to create one. $authplugin = get_auth_plugin($user['auth']); if ($authplugin->is_internal() && empty($user['password']) && empty($user['createpassword'])) { throw new invalid_parameter_exception('Invalid password: you must provide a password, or set createpassword.'); } $user['confirmed'] = true; $user['mnethostid'] = $CFG->mnet_localhost_id; // Start of user info validation. // Make sure we validate current user info as handled by current GUI. See user/editadvanced_form.php func validation(). if (!validate_email($user['email'])) { throw new invalid_parameter_exception('Email address is invalid: '.$user['email']); } else if (empty($CFG->allowaccountssameemail)) { // Make a case-insensitive query for the given email address. $select = $DB->sql_equal('email', ':email', false) . ' AND mnethostid = :mnethostid'; $params = array( 'email' => $user['email'], 'mnethostid' => $user['mnethostid'] ); // If there are other user(s) that already have the same email, throw an error. if ($DB->record_exists_select('user', $select, $params)) { throw new invalid_parameter_exception('Email address already exists: '.$user['email']); } } // End of user info validation. $createpassword = !empty($user['createpassword']); unset($user['createpassword']); $updatepassword = false; if ($authplugin->is_internal()) { if ($createpassword) { $user['password'] = ''; } else { $updatepassword = true; } } else { $user['password'] = AUTH_PASSWORD_NOT_CACHED; } // Create the user data now! $user['id'] = user_create_user($user, $updatepassword, false); $userobject = (object)$user; // Set user interests. if (!empty($user['interests'])) { $trimmedinterests = array_map('trim', explode(',', $user['interests'])); $interests = array_filter($trimmedinterests, function($value) { return !empty($value); }); useredit_update_interests($userobject, $interests); } // Custom fields. if (!empty($user['customfields'])) { foreach ($user['customfields'] as $customfield) { // Profile_save_data() saves profile file it's expecting a user with the correct id, // and custom field to be named profile_field_"shortname". $user["profile_field_".$customfield['type']] = $customfield['value']; } profile_save_data((object) $user); } if ($createpassword) { setnew_password_and_mail($userobject); unset_user_preference('create_password', $userobject); set_user_preference('auth_forcepasswordchange', 1, $userobject); } // Trigger event. \core\event\user_created::create_from_userid($user['id'])->trigger(); // Preferences. if (!empty($user['preferences'])) { $userpref = (object)$user; foreach ($user['preferences'] as $preference) { $userpref->{'preference_'.$preference['type']} = $preference['value']; } useredit_update_user_preference($userpref); } $userids[] = array('id' => $user['id'], 'username' => $user['username']); } $transaction->allow_commit(); return $userids; } /** * Returns description of method result value * * @return external_description * @since Moodle 2.2 */ public static function create_users_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(core_user::get_property_type('id'), 'user id'), 'username' => new external_value(core_user::get_property_type('username'), 'user name'), ) ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function delete_users_parameters() { return new external_function_parameters( array( 'userids' => new external_multiple_structure(new external_value(core_user::get_property_type('id'), 'user ID')), ) ); } /** * Delete users * * @throws moodle_exception * @param array $userids * @return null * @since Moodle 2.2 */ public static function delete_users($userids) { global $CFG, $DB, $USER; require_once($CFG->dirroot."/user/lib.php"); // Ensure the current user is allowed to run this function. $context = context_system::instance(); require_capability('moodle/user:delete', $context); self::validate_context($context); $params = self::validate_parameters(self::delete_users_parameters(), array('userids' => $userids)); $transaction = $DB->start_delegated_transaction(); foreach ($params['userids'] as $userid) { $user = $DB->get_record('user', array('id' => $userid, 'deleted' => 0), '*', MUST_EXIST); // Must not allow deleting of admins or self!!! if (is_siteadmin($user)) { throw new moodle_exception('useradminodelete', 'error'); } if ($USER->id == $user->id) { throw new moodle_exception('usernotdeletederror', 'error'); } user_delete_user($user); } $transaction->allow_commit(); return null; } /** * Returns description of method result value * * @return null * @since Moodle 2.2 */ public static function delete_users_returns() { return null; } /** * Returns description of method parameters. * * @return external_function_parameters * @since Moodle 3.2 */ public static function update_user_preferences_parameters() { return new external_function_parameters( array( 'userid' => new external_value(PARAM_INT, 'id of the user, default to current user', VALUE_DEFAULT, 0), 'emailstop' => new external_value(core_user::get_property_type('emailstop'), 'Enable or disable notifications for this user', VALUE_DEFAULT, null), 'preferences' => new external_multiple_structure( new external_single_structure( array( 'type' => new external_value(PARAM_RAW, 'The name of the preference'), 'value' => new external_value(PARAM_RAW, 'The value of the preference, do not set this field if you want to remove (unset) the current value.', VALUE_DEFAULT, null), ) ), 'User preferences', VALUE_DEFAULT, array() ) ) ); } /** * Update the user's preferences. * * @param int $userid * @param bool|null $emailstop * @param array $preferences * @return null * @since Moodle 3.2 */ public static function update_user_preferences($userid = 0, $emailstop = null, $preferences = array()) { 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; } $systemcontext = context_system::instance(); self::validate_context($systemcontext); $params = array( 'userid' => $userid, 'emailstop' => $emailstop, 'preferences' => $preferences ); $params = self::validate_parameters(self::update_user_preferences_parameters(), $params); $preferences = $params['preferences']; // Preferences. if (!empty($preferences)) { $userpref = ['id' => $userid]; foreach ($preferences as $preference) { /* * Rename user message provider preferences to avoid orphan settings on old app versions. * @todo Remove this "translation" block on MDL-73284. */ if (preg_match('/message_provider_.*_loggedin/', $preference['type']) || preg_match('/message_provider_.*_loggedoff/', $preference['type'])) { $nameparts = explode('_', $preference['type']); array_pop($nameparts); $preference['type'] = implode('_', $nameparts).'_enabled'; } $userpref['preference_' . $preference['type']] = $preference['value']; } useredit_update_user_preference($userpref); } // Check if they want to update the email. if ($emailstop !== null) { $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; } } } return null; } /** * Returns description of method result value * * @return null * @since Moodle 3.2 */ public static function update_user_preferences_returns() { return null; } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function update_users_parameters() { $userfields = [ 'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'), // General. 'username' => new external_value(core_user::get_property_type('username'), 'Username policy is defined in Moodle security config.', VALUE_OPTIONAL, '', NULL_NOT_ALLOWED), 'auth' => new external_value(core_user::get_property_type('auth'), 'Auth plugins include manual, ldap, etc', VALUE_OPTIONAL, '', NULL_NOT_ALLOWED), 'suspended' => new external_value(core_user::get_property_type('suspended'), 'Suspend user account, either false to enable user login or true to disable it', VALUE_OPTIONAL), 'password' => new external_value(core_user::get_property_type('password'), 'Plain text password consisting of any characters', VALUE_OPTIONAL, '', NULL_NOT_ALLOWED), 'firstname' => new external_value(core_user::get_property_type('firstname'), 'The first name(s) of the user', VALUE_OPTIONAL, '', NULL_NOT_ALLOWED), 'lastname' => new external_value(core_user::get_property_type('lastname'), 'The family name of the user', VALUE_OPTIONAL), 'email' => new external_value(core_user::get_property_type('email'), 'A valid and unique email address', VALUE_OPTIONAL, '', NULL_NOT_ALLOWED), 'maildisplay' => new external_value(core_user::get_property_type('maildisplay'), 'Email visibility', VALUE_OPTIONAL), 'city' => new external_value(core_user::get_property_type('city'), 'Home city of the user', VALUE_OPTIONAL), 'country' => new external_value(core_user::get_property_type('country'), 'Home country code of the user, such as AU or CZ', VALUE_OPTIONAL), 'timezone' => new external_value(core_user::get_property_type('timezone'), 'Timezone code such as Australia/Perth, or 99 for default', VALUE_OPTIONAL), 'description' => new external_value(core_user::get_property_type('description'), 'User profile description, no HTML', VALUE_OPTIONAL), // User picture. 'userpicture' => new external_value(PARAM_INT, 'The itemid where the new user picture has been uploaded to, 0 to delete', VALUE_OPTIONAL), // Additional names. 'firstnamephonetic' => new external_value(core_user::get_property_type('firstnamephonetic'), 'The first name(s) phonetically of the user', VALUE_OPTIONAL), 'lastnamephonetic' => new external_value(core_user::get_property_type('lastnamephonetic'), 'The family name phonetically of the user', VALUE_OPTIONAL), 'middlename' => new external_value(core_user::get_property_type('middlename'), 'The middle name of the user', VALUE_OPTIONAL), 'alternatename' => new external_value(core_user::get_property_type('alternatename'), 'The alternate name of the user', VALUE_OPTIONAL), // Interests. 'interests' => new external_value(PARAM_TEXT, 'User interests (separated by commas)', VALUE_OPTIONAL), // Optional. 'idnumber' => new external_value(core_user::get_property_type('idnumber'), 'An arbitrary ID code number perhaps from the institution', VALUE_OPTIONAL), 'institution' => new external_value(core_user::get_property_type('institution'), 'Institution', VALUE_OPTIONAL), 'department' => new external_value(core_user::get_property_type('department'), 'Department', VALUE_OPTIONAL), 'phone1' => new external_value(core_user::get_property_type('phone1'), 'Phone', VALUE_OPTIONAL), 'phone2' => new external_value(core_user::get_property_type('phone2'), 'Mobile phone', VALUE_OPTIONAL), 'address' => new external_value(core_user::get_property_type('address'), 'Postal address', VALUE_OPTIONAL), // Other user preferences stored in the user table. 'lang' => new external_value(core_user::get_property_type('lang'), 'Language code such as "en", must exist on server', VALUE_OPTIONAL, '', NULL_NOT_ALLOWED), 'calendartype' => new external_value(core_user::get_property_type('calendartype'), 'Calendar type such as "gregorian", must exist on server', VALUE_OPTIONAL, '', NULL_NOT_ALLOWED), 'theme' => new external_value(core_user::get_property_type('theme'), 'Theme name such as "standard", must exist on server', VALUE_OPTIONAL), 'mailformat' => new external_value(core_user::get_property_type('mailformat'), 'Mail format code is 0 for plain text, 1 for HTML etc', VALUE_OPTIONAL), // Custom user profile fields. 'customfields' => new external_multiple_structure( new external_single_structure( [ 'type' => new external_value(PARAM_ALPHANUMEXT, 'The name of the custom field'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field') ] ), 'User custom fields (also known as user profil fields)', VALUE_OPTIONAL), // User preferences. 'preferences' => new external_multiple_structure( new external_single_structure( [ 'type' => new external_value(PARAM_RAW, 'The name of the preference'), 'value' => new external_value(PARAM_RAW, 'The value of the preference') ] ), 'User preferences', VALUE_OPTIONAL), ]; return new external_function_parameters( [ 'users' => new external_multiple_structure( new external_single_structure($userfields) ) ] ); } /** * Update users * * @param array $users * @return null * @since Moodle 2.2 */ public static function update_users($users) { 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(); require_capability('moodle/user:update', $context); self::validate_context($context); $params = self::validate_parameters(self::update_users_parameters(), array('users' => $users)); $filemanageroptions = array('maxbytes' => $CFG->maxbytes, 'subdirs' => 0, 'maxfiles' => 1, 'accepted_types' => 'optimised_image'); $warnings = array(); foreach ($params['users'] as $user) { // Catch any exception while updating a user and return it as a warning. try { $transaction = $DB->start_delegated_transaction(); // First check the user exists. if (!$existinguser = core_user::get_user($user['id'])) { throw new moodle_exception('invaliduserid', '', '', null, 'Invalid user ID'); } // Check if we are trying to update an admin. if ($existinguser->id != $USER->id and is_siteadmin($existinguser) and !is_siteadmin($USER)) { throw new moodle_exception('usernotupdatedadmin', '', '', null, 'Cannot update admin accounts'); } // Other checks (deleted, remote or guest users). if ($existinguser->deleted) { throw new moodle_exception('usernotupdateddeleted', '', '', null, 'User is a deleted user'); } if (is_mnet_remote_user($existinguser)) { throw new moodle_exception('usernotupdatedremote', '', '', null, 'User is a remote user'); } if (isguestuser($existinguser->id)) { throw new moodle_exception('usernotupdatedguest', '', '', null, 'Cannot update guest account'); } // Check duplicated emails. if (isset($user['email']) && $user['email'] !== $existinguser->email) { if (!validate_email($user['email'])) { throw new moodle_exception('useremailinvalid', '', '', null, 'Invalid email address'); } else if (empty($CFG->allowaccountssameemail)) { // Make a case-insensitive query for the given email address // and make sure to exclude the user being updated. $select = $DB->sql_equal('email', ':email', false) . ' AND mnethostid = :mnethostid AND id <> :userid'; $params = array( 'email' => $user['email'], 'mnethostid' => $CFG->mnet_localhost_id, 'userid' => $user['id'] ); // Skip if there are other user(s) that already have the same email. if ($DB->record_exists_select('user', $select, $params)) { throw new moodle_exception('useremailduplicate', '', '', null, 'Duplicate email address'); } } } user_update_user($user, true, false); $userobject = (object)$user; // Update user picture if it was specified for this user. if (empty($CFG->disableuserimages) && isset($user['userpicture'])) { $userobject->deletepicture = null; if ($user['userpicture'] == 0) { $userobject->deletepicture = true; } else { $userobject->imagefile = $user['userpicture']; } core_user::update_picture($userobject, $filemanageroptions); } // Update user interests. if (!empty($user['interests'])) { $trimmedinterests = array_map('trim', explode(',', $user['interests'])); $interests = array_filter($trimmedinterests, function($value) { return !empty($value); }); useredit_update_interests($userobject, $interests); } // Update user custom fields. if (!empty($user['customfields'])) { foreach ($user['customfields'] as $customfield) { // Profile_save_data() saves profile file it's expecting a user with the correct id, // and custom field to be named profile_field_"shortname". $user["profile_field_".$customfield['type']] = $customfield['value']; } profile_save_data((object) $user); } // Trigger event. \core\event\user_updated::create_from_userid($user['id'])->trigger(); // Preferences. if (!empty($user['preferences'])) { $userpref = clone($existinguser); foreach ($user['preferences'] as $preference) { $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']); } $transaction->allow_commit(); } catch (Exception $e) { try { $transaction->rollback($e); } catch (Exception $e) { $warning = []; $warning['item'] = 'user'; $warning['itemid'] = $user['id']; if ($e instanceof moodle_exception) { $warning['warningcode'] = $e->errorcode; } else { $warning['warningcode'] = $e->getCode(); } $warning['message'] = $e->getMessage(); $warnings[] = $warning; } } } return ['warnings' => $warnings]; } /** * Returns description of method result value * * @return external_description * @since Moodle 2.2 */ public static function update_users_returns() { return new external_single_structure( array( 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.4 */ public static function get_users_by_field_parameters() { return new external_function_parameters( array( 'field' => new external_value(PARAM_ALPHA, 'the search field can be \'id\' or \'idnumber\' or \'username\' or \'email\''), 'values' => new external_multiple_structure( new external_value(PARAM_RAW, 'the value to match')) ) ); } /** * Get user information for a unique field. * * @throws coding_exception * @throws invalid_parameter_exception * @param string $field * @param array $values * @return array An array of arrays containg user profiles. * @since Moodle 2.4 */ public static function get_users_by_field($field, $values) { global $CFG, $USER, $DB; require_once($CFG->dirroot . "/user/lib.php"); $params = self::validate_parameters(self::get_users_by_field_parameters(), array('field' => $field, 'values' => $values)); // This array will keep all the users that are allowed to be searched, // according to the current user's privileges. $cleanedvalues = array(); switch ($field) { case 'id': $paramtype = core_user::get_property_type('id'); break; case 'idnumber': $paramtype = core_user::get_property_type('idnumber'); break; case 'username': $paramtype = core_user::get_property_type('username'); break; case 'email': $paramtype = core_user::get_property_type('email'); break; default: throw new coding_exception('invalid field parameter', 'The search field \'' . $field . '\' is not supported, look at the web service documentation'); } // Clean the values. foreach ($values as $value) { $cleanedvalue = clean_param($value, $paramtype); if ( $value != $cleanedvalue) { throw new invalid_parameter_exception('The field \'' . $field . '\' value is invalid: ' . $value . '(cleaned value: '.$cleanedvalue.')'); } $cleanedvalues[] = $cleanedvalue; } // Retrieve the users. $users = $DB->get_records_list('user', $field, $cleanedvalues, 'id'); $context = context_system::instance(); self::validate_context($context); // Finally retrieve each users information. $returnedusers = array(); foreach ($users as $user) { $userdetails = user_get_user_details_courses($user); // Return the user only if the searched field is returned. // Otherwise it means that the $USER was not allowed to search the returned user. if (!empty($userdetails) and !empty($userdetails[$field])) { $returnedusers[] = $userdetails; } } return $returnedusers; } /** * Returns description of method result value * * @return external_multiple_structure * @since Moodle 2.4 */ public static function get_users_by_field_returns() { return new external_multiple_structure(self::user_description()); } /** * Returns description of get_users() parameters. * * @return external_function_parameters * @since Moodle 2.5 */ public static function get_users_parameters() { return new external_function_parameters( array( 'criteria' => new external_multiple_structure( new external_single_structure( array( 'key' => new external_value(PARAM_ALPHA, 'the user column to search, expected keys (value format) are: "id" (int) matching user id, "lastname" (string) user last name (Note: you can use % for searching but it may be considerably slower!), "firstname" (string) user first name (Note: you can use % for searching but it may be considerably slower!), "idnumber" (string) matching user idnumber, "username" (string) matching user username, "email" (string) user email (Note: you can use % for searching but it may be considerably slower!), "auth" (string) matching user auth plugin'), 'value' => new external_value(PARAM_RAW, 'the value to search') ) ), 'the key/value pairs to be considered in user search. Values can not be empty. Specify different keys only once (fullname => \'user1\', auth => \'manual\', ...) - key occurences are forbidden. The search is executed with AND operator on the criterias. Invalid criterias (keys) are ignored, the search is still executed on the valid criterias. You can search without criteria, but the function is not designed for it. It could very slow or timeout. The function is designed to search some specific users.' ) ) ); } /** * Retrieve matching user. * * @throws moodle_exception * @param array $criteria the allowed array keys are id/lastname/firstname/idnumber/username/email/auth. * @return array An array of arrays containing user profiles. * @since Moodle 2.5 */ public static function get_users($criteria = array()) { global $CFG, $USER, $DB; require_once($CFG->dirroot . "/user/lib.php"); $params = self::validate_parameters(self::get_users_parameters(), array('criteria' => $criteria)); // Validate the criteria and retrieve the users. $users = array(); $warnings = array(); $sqlparams = array(); $usedkeys = array(); // Do not retrieve deleted users. $sql = ' deleted = 0'; foreach ($params['criteria'] as $criteriaindex => $criteria) { // Check that the criteria has never been used. if (array_key_exists($criteria['key'], $usedkeys)) { throw new moodle_exception('keyalreadyset', '', '', null, 'The key ' . $criteria['key'] . ' can only be sent once'); } else { $usedkeys[$criteria['key']] = true; } $invalidcriteria = false; // Clean the parameters. $paramtype = PARAM_RAW; switch ($criteria['key']) { case 'id': $paramtype = core_user::get_property_type('id'); break; case 'idnumber': $paramtype = core_user::get_property_type('idnumber'); break; case 'username': $paramtype = core_user::get_property_type('username'); break; case 'email': // We use PARAM_RAW to allow searches with %. $paramtype = core_user::get_property_type('email'); break; case 'auth': $paramtype = core_user::get_property_type('auth'); break; case 'lastname': case 'firstname': $paramtype = core_user::get_property_type('firstname'); break; default: // Send back a warning that this search key is not supported in this version. // This warning will make the function extandable without breaking clients. $warnings[] = array( 'item' => $criteria['key'], 'warningcode' => 'invalidfieldparameter', 'message' => 'The search key \'' . $criteria['key'] . '\' is not supported, look at the web service documentation' ); // Do not add this invalid criteria to the created SQL request. $invalidcriteria = true; unset($params['criteria'][$criteriaindex]); break; } if (!$invalidcriteria) { $cleanedvalue = clean_param($criteria['value'], $paramtype); $sql .= ' AND '; // Create the SQL. switch ($criteria['key']) { case 'id': case 'idnumber': case 'username': case 'auth': $sql .= $criteria['key'] . ' = :' . $criteria['key']; $sqlparams[$criteria['key']] = $cleanedvalue; break; case 'email': case 'lastname': case 'firstname': $sql .= $DB->sql_like($criteria['key'], ':' . $criteria['key'], false); $sqlparams[$criteria['key']] = $cleanedvalue; break; default: break; } } } $users = $DB->get_records_select('user', $sql, $sqlparams, 'id ASC'); // Finally retrieve each users information. $returnedusers = array(); foreach ($users as $user) { $userdetails = user_get_user_details_courses($user); // Return the user only if all the searched fields are returned. // Otherwise it means that the $USER was not allowed to search the returned user. if (!empty($userdetails)) { $validuser = true; foreach ($params['criteria'] as $criteria) { if (empty($userdetails[$criteria['key']])) { $validuser = false; } } if ($validuser) { $returnedusers[] = $userdetails; } } } return array('users' => $returnedusers, 'warnings' => $warnings); } /** * Returns description of get_users result value. * * @return external_description * @since Moodle 2.5 */ public static function get_users_returns() { return new external_single_structure( array('users' => new external_multiple_structure( self::user_description() ), 'warnings' => new external_warnings('always set to \'key\'', 'faulty key name') ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function get_course_user_profiles_parameters() { return new external_function_parameters( array( 'userlist' => new external_multiple_structure( new external_single_structure( array( 'userid' => new external_value(core_user::get_property_type('id'), 'userid'), 'courseid' => new external_value(PARAM_INT, 'courseid'), ) ) ) ) ); } /** * Get course participant's details * * @param array $userlist array of user ids and according course ids * @return array An array of arrays describing course participants * @since Moodle 2.2 */ public static function get_course_user_profiles($userlist) { global $CFG, $USER, $DB; require_once($CFG->dirroot . "/user/lib.php"); $params = self::validate_parameters(self::get_course_user_profiles_parameters(), array('userlist' => $userlist)); $userids = array(); $courseids = array(); foreach ($params['userlist'] as $value) { $userids[] = $value['userid']; $courseids[$value['userid']] = $value['courseid']; } // Cache all courses. $courses = array(); list($sqlcourseids, $params) = $DB->get_in_or_equal(array_unique($courseids), SQL_PARAMS_NAMED); $cselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $cjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; $params['contextlevel'] = CONTEXT_COURSE; $coursesql = "SELECT c.* $cselect FROM {course} c $cjoin WHERE c.id $sqlcourseids"; $rs = $DB->get_recordset_sql($coursesql, $params); foreach ($rs as $course) { // Adding course contexts to cache. context_helper::preload_from_record($course); // Cache courses. $courses[$course->id] = $course; } $rs->close(); list($sqluserids, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $uselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $ujoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel)"; $params['contextlevel'] = CONTEXT_USER; $usersql = "SELECT u.* $uselect FROM {user} u $ujoin WHERE u.id $sqluserids"; $users = $DB->get_recordset_sql($usersql, $params); $result = array(); foreach ($users as $user) { if (!empty($user->deleted)) { continue; } context_helper::preload_from_record($user); $course = $courses[$courseids[$user->id]]; $context = context_course::instance($courseids[$user->id], IGNORE_MISSING); self::validate_context($context); if ($userarray = user_get_user_details($user, $course)) { $result[] = $userarray; } } $users->close(); return $result; } /** * Returns description of method result value * * @return external_description * @since Moodle 2.2 */ public static function get_course_user_profiles_returns() { $additionalfields = array( 'groups' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'group id'), 'name' => new external_value(PARAM_RAW, 'group name'), 'description' => new external_value(PARAM_RAW, 'group description'), 'descriptionformat' => new external_format_value('description'), ) ), 'user groups', VALUE_OPTIONAL), 'roles' => new external_multiple_structure( new external_single_structure( array( 'roleid' => new external_value(PARAM_INT, 'role id'), 'name' => new external_value(PARAM_RAW, 'role name'), 'shortname' => new external_value(PARAM_ALPHANUMEXT, 'role shortname'), 'sortorder' => new external_value(PARAM_INT, 'role sortorder') ) ), 'user roles', VALUE_OPTIONAL), 'enrolledcourses' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'Id of the course'), 'fullname' => new external_value(PARAM_RAW, 'Fullname of the course'), 'shortname' => new external_value(PARAM_RAW, 'Shortname of the course') ) ), 'Courses where the user is enrolled - limited by which courses the user is able to see', VALUE_OPTIONAL) ); return new external_multiple_structure(self::user_description($additionalfields)); } /** * Create user return value description. * * @param array $additionalfields some additional field * @return single_structure_description */ public static function user_description($additionalfields = array()) { $userfields = array( 'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'), 'username' => new external_value(core_user::get_property_type('username'), 'The username', VALUE_OPTIONAL), 'firstname' => new external_value(core_user::get_property_type('firstname'), 'The first name(s) of the user', VALUE_OPTIONAL), 'lastname' => new external_value(core_user::get_property_type('lastname'), 'The family name of the user', VALUE_OPTIONAL), 'fullname' => new external_value(core_user::get_property_type('firstname'), 'The fullname of the user'), 'email' => new external_value(core_user::get_property_type('email'), 'An email address - allow email as root@localhost', VALUE_OPTIONAL), 'address' => new external_value(core_user::get_property_type('address'), 'Postal address', VALUE_OPTIONAL), 'phone1' => new external_value(core_user::get_property_type('phone1'), 'Phone 1', VALUE_OPTIONAL), 'phone2' => new external_value(core_user::get_property_type('phone2'), 'Phone 2', VALUE_OPTIONAL), 'department' => new external_value(core_user::get_property_type('department'), 'department', VALUE_OPTIONAL), 'institution' => new external_value(core_user::get_property_type('institution'), 'institution', VALUE_OPTIONAL), 'idnumber' => new external_value(core_user::get_property_type('idnumber'), 'An arbitrary ID code number perhaps from the institution', VALUE_OPTIONAL), 'interests' => new external_value(PARAM_TEXT, 'user interests (separated by commas)', VALUE_OPTIONAL), 'firstaccess' => new external_value(core_user::get_property_type('firstaccess'), 'first access to the site (0 if never)', VALUE_OPTIONAL), 'lastaccess' => new external_value(core_user::get_property_type('lastaccess'), 'last access to the site (0 if never)', VALUE_OPTIONAL), 'auth' => new external_value(core_user::get_property_type('auth'), 'Auth plugins include manual, ldap, etc', VALUE_OPTIONAL), 'suspended' => new external_value(core_user::get_property_type('suspended'), 'Suspend user account, either false to enable user login or true to disable it', VALUE_OPTIONAL), 'confirmed' => new external_value(core_user::get_property_type('confirmed'), 'Active user: 1 if confirmed, 0 otherwise', VALUE_OPTIONAL), 'lang' => new external_value(core_user::get_property_type('lang'), 'Language code such as "en", must exist on server', VALUE_OPTIONAL), 'calendartype' => new external_value(core_user::get_property_type('calendartype'), 'Calendar type such as "gregorian", must exist on server', VALUE_OPTIONAL), 'theme' => new external_value(core_user::get_property_type('theme'), 'Theme name such as "standard", must exist on server', VALUE_OPTIONAL), 'timezone' => new external_value(core_user::get_property_type('timezone'), 'Timezone code such as Australia/Perth, or 99 for default', VALUE_OPTIONAL), 'mailformat' => new external_value(core_user::get_property_type('mailformat'), 'Mail format code is 0 for plain text, 1 for HTML etc', VALUE_OPTIONAL), 'description' => new external_value(core_user::get_property_type('description'), 'User profile description', VALUE_OPTIONAL), 'descriptionformat' => new external_format_value(core_user::get_property_type('descriptionformat'), VALUE_OPTIONAL), 'city' => new external_value(core_user::get_property_type('city'), 'Home city of the user', VALUE_OPTIONAL), 'country' => new external_value(core_user::get_property_type('country'), 'Home country code of the user, such as AU or CZ', VALUE_OPTIONAL), 'profileimageurlsmall' => new external_value(PARAM_URL, 'User image profile URL - small version'), 'profileimageurl' => new external_value(PARAM_URL, 'User image profile URL - big version'), 'customfields' => new external_multiple_structure( new external_single_structure( array( 'type' => new external_value(PARAM_ALPHANUMEXT, 'The type of the custom field - text field, checkbox...'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field'), 'name' => new external_value(PARAM_RAW, 'The name of the custom field'), 'shortname' => new external_value(PARAM_RAW, 'The shortname of the custom field - to be able to build the field class in the code'), ) ), 'User custom fields (also known as user profile fields)', VALUE_OPTIONAL), 'preferences' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_RAW, 'The name of the preferences'), 'value' => new external_value(PARAM_RAW, 'The value of the preference'), ) ), 'Users preferences', VALUE_OPTIONAL) ); if (!empty($additionalfields)) { $userfields = array_merge($userfields, $additionalfields); } return new external_single_structure($userfields); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.6 */ public static function add_user_private_files_parameters() { return new external_function_parameters( array( 'draftid' => new external_value(PARAM_INT, 'draft area id') ) ); } /** * Copy files from a draft area to users private files area. * * @throws invalid_parameter_exception * @throws moodle_exception * @param int $draftid Id of a draft area containing files. * @return array An array of warnings * @since Moodle 2.6 */ public static function add_user_private_files($draftid) { global $CFG, $USER; require_once($CFG->libdir . "/filelib.php"); $params = self::validate_parameters(self::add_user_private_files_parameters(), array('draftid' => $draftid)); if (isguestuser()) { throw new invalid_parameter_exception('Guest users cannot upload files'); } $context = context_user::instance($USER->id); require_capability('moodle/user:manageownfiles', $context); $maxbytes = $CFG->userquota; $maxareabytes = $CFG->userquota; if (has_capability('moodle/user:ignoreuserquota', $context)) { $maxbytes = USER_CAN_IGNORE_FILE_SIZE_LIMITS; $maxareabytes = FILE_AREA_MAX_BYTES_UNLIMITED; } else { // Get current used space for this user. $usedspace = file_get_user_used_space(); // Get the total size of the new files we want to add to private files. $newfilesinfo = file_get_draft_area_info($params['draftid']); if (($newfilesinfo['filesize_without_references'] + $usedspace) > $maxareabytes) { throw new moodle_exception('maxareabytes'); } } $options = array('subdirs' => 1, 'maxbytes' => $maxbytes, 'maxfiles' => -1, 'areamaxbytes' => $maxareabytes); file_merge_files_from_draft_area_into_filearea($draftid, $context->id, 'user', 'private', 0, $options); return null; } /** * Returns description of method result value * * @return external_description * @since Moodle 2.2 */ public static function add_user_private_files_returns() { return null; } /** * Returns description of method parameters. * * @return external_function_parameters * @since Moodle 2.6 */ public static function add_user_device_parameters() { return new external_function_parameters( array( 'appid' => new external_value(PARAM_NOTAGS, 'the app id, usually something like com.moodle.moodlemobile'), 'name' => new external_value(PARAM_NOTAGS, 'the device name, \'occam\' or \'iPhone\' etc.'), 'model' => new external_value(PARAM_NOTAGS, 'the device model \'Nexus4\' or \'iPad1,1\' etc.'), 'platform' => new external_value(PARAM_NOTAGS, 'the device platform \'iOS\' or \'Android\' etc.'), 'version' => new external_value(PARAM_NOTAGS, 'the device version \'6.1.2\' or \'4.2.2\' etc.'), 'pushid' => new external_value(PARAM_RAW, 'the device PUSH token/key/identifier/registration id'), 'uuid' => new external_value(PARAM_RAW, 'the device UUID') ) ); } /** * Add a user device in Moodle database (for PUSH notifications usually). * * @throws moodle_exception * @param string $appid The app id, usually something like com.moodle.moodlemobile. * @param string $name The device name, occam or iPhone etc. * @param string $model The device model Nexus4 or iPad1.1 etc. * @param string $platform The device platform iOs or Android etc. * @param string $version The device version 6.1.2 or 4.2.2 etc. * @param string $pushid The device PUSH token/key/identifier/registration id. * @param string $uuid The device UUID. * @return array List of possible warnings. * @since Moodle 2.6 */ public static function add_user_device($appid, $name, $model, $platform, $version, $pushid, $uuid) { global $CFG, $USER, $DB; require_once($CFG->dirroot . "/user/lib.php"); $params = self::validate_parameters(self::add_user_device_parameters(), array('appid' => $appid, 'name' => $name, 'model' => $model, 'platform' => $platform, 'version' => $version, 'pushid' => $pushid, 'uuid' => $uuid )); $warnings = array(); // Prevent duplicate keys for users. if ($DB->get_record('user_devices', array('pushid' => $params['pushid'], 'userid' => $USER->id))) { $warnings['warning'][] = array( 'item' => $params['pushid'], 'warningcode' => 'existingkeyforthisuser', 'message' => 'This key is already stored for this user' ); return $warnings; } // Notice that we can have multiple devices because previously it was allowed to have repeated ones. // Since we don't have a clear way to decide which one is the more appropiate, we update all. if ($userdevices = $DB->get_records('user_devices', array('uuid' => $params['uuid'], 'appid' => $params['appid'], 'userid' => $USER->id))) { foreach ($userdevices as $userdevice) { $userdevice->version = $params['version']; // Maybe the user upgraded the device. $userdevice->pushid = $params['pushid']; $userdevice->timemodified = time(); $DB->update_record('user_devices', $userdevice); } } else { $userdevice = new stdclass; $userdevice->userid = $USER->id; $userdevice->appid = $params['appid']; $userdevice->name = $params['name']; $userdevice->model = $params['model']; $userdevice->platform = $params['platform']; $userdevice->version = $params['version']; $userdevice->pushid = $params['pushid']; $userdevice->uuid = $params['uuid']; $userdevice->timecreated = time(); $userdevice->timemodified = $userdevice->timecreated; if (!$DB->insert_record('user_devices', $userdevice)) { throw new moodle_exception("There was a problem saving in the database the device with key: " . $params['pushid']); } } return $warnings; } /** * Returns description of method result value. * * @return external_multiple_structure * @since Moodle 2.6 */ public static function add_user_device_returns() { return new external_multiple_structure( new external_warnings() ); } /** * Returns description of method parameters. * * @return external_function_parameters * @since Moodle 2.9 */ public static function remove_user_device_parameters() { return new external_function_parameters( array( 'uuid' => new external_value(PARAM_RAW, 'the device UUID'), 'appid' => new external_value(PARAM_NOTAGS, 'the app id, if empty devices matching the UUID for the user will be removed', VALUE_DEFAULT, ''), ) ); } /** * Remove a user device from the Moodle database (for PUSH notifications usually). * * @param string $uuid The device UUID. * @param string $appid The app id, opitonal parameter. If empty all the devices fmatching the UUID or the user will be removed. * @return array List of possible warnings and removal status. * @since Moodle 2.9 */ public static function remove_user_device($uuid, $appid = "") { global $CFG; require_once($CFG->dirroot . "/user/lib.php"); $params = self::validate_parameters(self::remove_user_device_parameters(), array('uuid' => $uuid, 'appid' => $appid)); $context = context_system::instance(); self::validate_context($context); // Warnings array, it can be empty at the end but is mandatory. $warnings = array(); $removed = user_remove_user_device($params['uuid'], $params['appid']); if (!$removed) { $warnings[] = array( 'item' => $params['uuid'], 'warningcode' => 'devicedoesnotexist', 'message' => 'The device doesn\'t exists in the database' ); } $result = array( 'removed' => $removed, 'warnings' => $warnings ); return $result; } /** * Returns description of method result value. * * @return external_multiple_structure * @since Moodle 2.9 */ public static function remove_user_device_returns() { return new external_single_structure( array( 'removed' => new external_value(PARAM_BOOL, 'True if removed, false if not removed because it doesn\'t exists'), 'warnings' => new external_warnings(), ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 */ public static function view_user_list_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'id of the course, 0 for site') ) ); } /** * Trigger the user_list_viewed event. * * @param int $courseid id of course * @return array of warnings and status result * @since Moodle 2.9 * @throws moodle_exception */ public static function view_user_list($courseid) { global $CFG; require_once($CFG->dirroot . "/user/lib.php"); require_once($CFG->dirroot . '/course/lib.php'); $params = self::validate_parameters(self::view_user_list_parameters(), array( 'courseid' => $courseid )); $warnings = array(); if (empty($params['courseid'])) { $params['courseid'] = SITEID; } $course = get_course($params['courseid']); if ($course->id == SITEID) { $context = context_system::instance(); } else { $context = context_course::instance($course->id); } self::validate_context($context); course_require_view_participants($context); user_list_view($course, $context); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return external_description * @since Moodle 2.9 */ public static function view_user_list_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 */ public static function view_user_profile_parameters() { return new external_function_parameters( array( 'userid' => new external_value(PARAM_INT, 'id of the user, 0 for current user', VALUE_REQUIRED), 'courseid' => new external_value(PARAM_INT, 'id of the course, default site course', VALUE_DEFAULT, 0) ) ); } /** * Trigger the user profile viewed event. * * @param int $userid id of user * @param int $courseid id of course * @return array of warnings and status result * @since Moodle 2.9 * @throws moodle_exception */ public static function view_user_profile($userid, $courseid = 0) { global $CFG, $USER; require_once($CFG->dirroot . "/user/profile/lib.php"); $params = self::validate_parameters(self::view_user_profile_parameters(), array( 'userid' => $userid, 'courseid' => $courseid )); $warnings = array(); if (empty($params['userid'])) { $params['userid'] = $USER->id; } if (empty($params['courseid'])) { $params['courseid'] = SITEID; } $course = get_course($params['courseid']); $user = core_user::get_user($params['userid'], '*', MUST_EXIST); core_user::require_active_user($user); if ($course->id == SITEID) { $coursecontext = context_system::instance();; } else { $coursecontext = context_course::instance($course->id); } self::validate_context($coursecontext); $currentuser = $USER->id == $user->id; $usercontext = context_user::instance($user->id); if (!$currentuser and !has_capability('moodle/user:viewdetails', $coursecontext) and !has_capability('moodle/user:viewdetails', $usercontext)) { throw new moodle_exception('cannotviewprofile'); } // Case like user/profile.php. if ($course->id == SITEID) { profile_view($user, $usercontext); } else { // Case like user/view.php. if (!$currentuser and !can_access_course($course, $user, '', true)) { throw new moodle_exception('notenrolledprofile'); } profile_view($user, $coursecontext, $course); } $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return external_description * @since Moodle 2.9 */ public static function view_user_profile_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.2 */ public static function get_user_preferences_parameters() { return new external_function_parameters( array( 'name' => new external_value(PARAM_RAW, 'preference name, empty for all', VALUE_DEFAULT, ''), 'userid' => new external_value(PARAM_INT, 'id of the user, default to current user', VALUE_DEFAULT, 0) ) ); } /** * Return user preferences. * * @param string $name preference name, empty for all * @param int $userid id of the user, 0 for current user * @return array of warnings and preferences * @since Moodle 3.2 * @throws moodle_exception */ public static function get_user_preferences($name = '', $userid = 0) { global $USER; $params = self::validate_parameters(self::get_user_preferences_parameters(), array( 'name' => $name, 'userid' => $userid )); $preferences = array(); $warnings = array(); $context = context_system::instance(); self::validate_context($context); if (empty($params['name'])) { $name = null; } if (empty($params['userid'])) { $user = null; } else { $user = core_user::get_user($params['userid'], '*', MUST_EXIST); core_user::require_active_user($user); if ($user->id != $USER->id) { // Only admins can retrieve other users preferences. require_capability('moodle/site:config', $context); } } $userpreferences = get_user_preferences($name, null, $user); // Check if we received just one preference. if (!is_array($userpreferences)) { $userpreferences = array($name => $userpreferences); } foreach ($userpreferences as $name => $value) { $preferences[] = array( 'name' => $name, 'value' => $value, ); } $result = array(); $result['preferences'] = $preferences; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return external_description * @since Moodle 3.2 */ public static function get_user_preferences_returns() { return new external_single_structure( array( 'preferences' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_RAW, 'The name of the preference'), 'value' => new external_value(PARAM_RAW, 'The value of the preference'), ) ), 'User custom fields (also known as user profile fields)' ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.2 */ public static function update_picture_parameters() { return new external_function_parameters( array( 'draftitemid' => new external_value(PARAM_INT, 'Id of the user draft file to use as image'), 'delete' => new external_value(PARAM_BOOL, 'If we should delete the user picture', VALUE_DEFAULT, false), 'userid' => new external_value(PARAM_INT, 'Id of the user, 0 for current user', VALUE_DEFAULT, 0) ) ); } /** * Update or delete the user picture in the site * * @param int $draftitemid id of the user draft file to use as image * @param bool $delete if we should delete the user picture * @param int $userid id of the user, 0 for current user * @return array warnings and success status * @since Moodle 3.2 * @throws moodle_exception */ public static function update_picture($draftitemid, $delete = false, $userid = 0) { global $CFG, $USER, $PAGE; $params = self::validate_parameters( self::update_picture_parameters(), array( 'draftitemid' => $draftitemid, 'delete' => $delete, 'userid' => $userid ) ); $context = context_system::instance(); self::validate_context($context); if (!empty($CFG->disableuserimages)) { throw new moodle_exception('userimagesdisabled', 'admin'); } if (empty($params['userid']) or $params['userid'] == $USER->id) { $user = $USER; require_capability('moodle/user:editownprofile', $context); } else { $user = core_user::get_user($params['userid'], '*', MUST_EXIST); core_user::require_active_user($user); $personalcontext = context_user::instance($user->id); require_capability('moodle/user:editprofile', $personalcontext); if (is_siteadmin($user) and !is_siteadmin($USER)) { // Only admins may edit other admins. throw new moodle_exception('useradmineditadmin'); } } // Load the appropriate auth plugin. $userauth = get_auth_plugin($user->auth); if (is_mnet_remote_user($user) or !$userauth->can_edit_profile() or $userauth->edit_profile_url()) { throw new moodle_exception('noprofileedit', 'auth'); } $filemanageroptions = array( 'maxbytes' => $CFG->maxbytes, 'subdirs' => 0, 'maxfiles' => 1, 'accepted_types' => 'optimised_image' ); $user->deletepicture = $params['delete']; $user->imagefile = $params['draftitemid']; $success = core_user::update_picture($user, $filemanageroptions); $result = array( 'success' => $success, 'warnings' => array(), ); if ($success) { $userpicture = new user_picture(core_user::get_user($user->id)); $userpicture->size = 1; // Size f1. $result['profileimageurl'] = $userpicture->get_url($PAGE)->out(false); } return $result; } /** * Returns description of method result value * * @return external_description * @since Moodle 3.2 */ public static function update_picture_returns() { return new external_single_structure( array( 'success' => new external_value(PARAM_BOOL, 'True if the image was updated, false otherwise.'), 'profileimageurl' => new external_value(PARAM_URL, 'New profile user image url', VALUE_OPTIONAL), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.2 */ public static function set_user_preferences_parameters() { return new external_function_parameters( array( 'preferences' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_RAW, 'The name of the preference'), 'value' => new external_value(PARAM_RAW, 'The value of the preference'), 'userid' => new external_value(PARAM_INT, 'Id of the user to set the preference'), ) ) ) ) ); } /** * Set user preferences. * * @param array $preferences list of preferences including name, value and userid * @return array of warnings and preferences saved * @since Moodle 3.2 * @throws moodle_exception */ public static function set_user_preferences($preferences) { global $USER; $params = self::validate_parameters(self::set_user_preferences_parameters(), array('preferences' => $preferences)); $warnings = array(); $saved = array(); $context = context_system::instance(); self::validate_context($context); $userscache = array(); foreach ($params['preferences'] as $pref) { // Check to which user set the preference. if (!empty($userscache[$pref['userid']])) { $user = $userscache[$pref['userid']]; } else { try { $user = core_user::get_user($pref['userid'], '*', MUST_EXIST); core_user::require_active_user($user); $userscache[$pref['userid']] = $user; } catch (Exception $e) { $warnings[] = array( 'item' => 'user', 'itemid' => $pref['userid'], 'warningcode' => 'invaliduser', 'message' => $e->getMessage() ); continue; } } try { 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', 'itemid' => $user->id, 'warningcode' => 'errorsavingpreference', 'message' => $e->getMessage() ); } } $result = array(); $result['saved'] = $saved; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return external_description * @since Moodle 3.2 */ public static function set_user_preferences_returns() { return new external_single_structure( array( 'saved' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_RAW, 'The name of the preference'), 'userid' => new external_value(PARAM_INT, 'The user the preference was set for'), ) ), 'Preferences saved' ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters. * * @return external_function_parameters * @since Moodle 3.2 */ public static function agree_site_policy_parameters() { return new external_function_parameters(array()); } /** * Agree the site policy for the current user. * * @return array of warnings and status result * @since Moodle 3.2 * @throws moodle_exception */ public static function agree_site_policy() { global $CFG, $DB, $USER; $warnings = array(); $context = context_system::instance(); try { // We expect an exception here since the user didn't agree the site policy yet. self::validate_context($context); } catch (Exception $e) { // We are expecting only a sitepolicynotagreed exception. if (!($e instanceof moodle_exception) or $e->errorcode != 'sitepolicynotagreed') { // In case we receive a different exception, throw it. throw $e; } } $manager = new \core_privacy\local\sitepolicy\manager(); if (!empty($USER->policyagreed)) { $status = false; $warnings[] = array( 'item' => 'user', 'itemid' => $USER->id, 'warningcode' => 'alreadyagreed', 'message' => 'The user already agreed the site policy.' ); } else if (!$manager->is_defined()) { $status = false; $warnings[] = array( 'item' => 'user', 'itemid' => $USER->id, 'warningcode' => 'nositepolicy', 'message' => 'The site does not have a site policy configured.' ); } else { $status = $manager->accept(); } $result = array(); $result['status'] = $status; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value. * * @return external_description * @since Moodle 3.2 */ public static function agree_site_policy_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'Status: true only if we set the policyagreed to 1 for the user'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters. * * @return external_function_parameters * @since Moodle 3.4 */ public static function get_private_files_info_parameters() { return new external_function_parameters( array( 'userid' => new external_value(PARAM_INT, 'Id of the user, default to current user.', VALUE_DEFAULT, 0) ) ); } /** * Returns general information about files in the user private files area. * * @param int $userid Id of the user, default to current user. * @return array of warnings and file area information * @since Moodle 3.4 * @throws moodle_exception */ public static function get_private_files_info($userid = 0) { global $CFG, $USER; require_once($CFG->libdir . '/filelib.php'); $params = self::validate_parameters(self::get_private_files_info_parameters(), array('userid' => $userid)); $warnings = array(); $context = context_system::instance(); self::validate_context($context); if (empty($params['userid']) || $params['userid'] == $USER->id) { $usercontext = context_user::instance($USER->id); require_capability('moodle/user:manageownfiles', $usercontext); } else { $user = core_user::get_user($params['userid'], '*', MUST_EXIST); core_user::require_active_user($user); // Only admins can retrieve other users information. require_capability('moodle/site:config', $context); $usercontext = context_user::instance($user->id); } $fileareainfo = file_get_file_area_info($usercontext->id, 'user', 'private'); $result = array(); $result['filecount'] = $fileareainfo['filecount']; $result['foldercount'] = $fileareainfo['foldercount']; $result['filesize'] = $fileareainfo['filesize']; $result['filesizewithoutreferences'] = $fileareainfo['filesize_without_references']; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value. * * @return external_description * @since Moodle 3.4 */ public static function get_private_files_info_returns() { return new external_single_structure( array( 'filecount' => new external_value(PARAM_INT, 'Number of files in the area.'), 'foldercount' => new external_value(PARAM_INT, 'Number of folders in the area.'), 'filesize' => new external_value(PARAM_INT, 'Total size of the files in the area.'), 'filesizewithoutreferences' => new external_value(PARAM_INT, 'Total size of the area excluding file references'), 'warnings' => new external_warnings() ) ); } } view.php 0000644 00000021404 15151162244 0006231 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Display profile for a particular user * * @package core_user * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../config.php"); require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/badgeslib.php'); $id = optional_param('id', 0, PARAM_INT); // User id. $courseid = optional_param('course', SITEID, PARAM_INT); // course id (defaults to Site). $showallcourses = optional_param('showallcourses', 0, PARAM_INT); // See your own profile by default. if (empty($id)) { require_login(); $id = $USER->id; } if ($courseid == SITEID) { // Since Moodle 2.0 all site-level profiles are shown by profile.php. redirect($CFG->wwwroot.'/user/profile.php?id='.$id); // Immediate redirect. } $PAGE->set_url('/user/view.php', array('id' => $id, 'course' => $courseid)); $user = $DB->get_record('user', array('id' => $id), '*', MUST_EXIST); $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $currentuser = ($user->id == $USER->id); $systemcontext = context_system::instance(); $coursecontext = context_course::instance($course->id); $usercontext = context_user::instance($user->id, IGNORE_MISSING); // Check we are not trying to view guest's profile. if (isguestuser($user)) { // Can not view profile of guest - thre is nothing to see there. throw new \moodle_exception('invaliduserid'); } $PAGE->set_context($coursecontext); if (!empty($CFG->forceloginforprofiles)) { require_login(); // We can not log in to course due to the parent hack below. // Guests do not have permissions to view anyone's profile if forceloginforprofiles is set. if (isguestuser()) { $PAGE->set_secondary_navigation(false); echo $OUTPUT->header(); echo $OUTPUT->confirm(get_string('guestcantaccessprofiles', 'error'), get_login_url(), $CFG->wwwroot); echo $OUTPUT->footer(); die; } } $PAGE->set_course($course); $PAGE->set_pagetype('course-view-' . $course->format); // To get the blocks exactly like the course. $PAGE->add_body_class('path-user'); // So we can style it independently. $PAGE->set_other_editing_capability('moodle/course:manageactivities'); // Set the Moodle docs path explicitly because the default behaviour // of inhereting the pagetype will lead to an incorrect docs location. $PAGE->set_docs_path('user/profile'); $isparent = false; if (!$currentuser and !$user->deleted and $DB->record_exists('role_assignments', array('userid' => $USER->id, 'contextid' => $usercontext->id)) and has_capability('moodle/user:viewdetails', $usercontext)) { // TODO: very ugly hack - do not force "parents" to enrol into course their child is enrolled in, // this way they may access the profile where they get overview of grades and child activity in course, // please note this is just a guess! require_login(); $isparent = true; $PAGE->navigation->set_userid_for_parent_checks($id); } else { // Normal course. require_login($course); // What to do with users temporary accessing this course? should they see the details? } $strpersonalprofile = get_string('personalprofile'); $strparticipants = get_string("participants"); $struser = get_string("user"); $fullname = fullname($user, has_capability('moodle/site:viewfullnames', $coursecontext)); // Now test the actual capabilities and enrolment in course. if ($currentuser) { if (!is_viewing($coursecontext) && !is_enrolled($coursecontext)) { // Need to have full access to a course to see the rest of own info. $referer = get_local_referer(false); if (!empty($referer)) { redirect($referer, get_string('notenrolled', '', $fullname)); } echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('notenrolled', '', $fullname)); echo $OUTPUT->footer(); die; } } else { // Somebody else. $PAGE->set_title("$strpersonalprofile: "); $PAGE->set_heading("$strpersonalprofile: "); // Check to see if the user can see this user's profile. if (!user_can_view_profile($user, $course, $usercontext) && !$isparent) { throw new \moodle_exception('cannotviewprofile'); } if (!is_enrolled($coursecontext, $user->id)) { // TODO: the only potential problem is that managers and inspectors might post in forum, but the link // to profile would not work - maybe a new capability - moodle/user:freely_acessile_profile_for_anybody // or test for course:inspect capability. if (has_capability('moodle/role:assign', $coursecontext)) { $PAGE->navbar->add($fullname); $notice = get_string('notenrolled', '', $fullname); } else { $PAGE->navbar->add($struser); $notice = get_string('notenrolledprofile', '', $fullname); } $referer = get_local_referer(false); if (!empty($referer)) { redirect($referer, $notice); } echo $OUTPUT->header(); echo $OUTPUT->heading($notice); echo $OUTPUT->footer(); exit; } if (!isloggedin() or isguestuser()) { // Do not use require_login() here because we might have already used require_login($course). redirect(get_login_url()); } } $PAGE->set_title("$course->fullname: $strpersonalprofile: $fullname"); $PAGE->set_heading($course->fullname); $PAGE->set_pagelayout('mypublic'); $PAGE->add_body_class('limitedwidth'); // Locate the users settings in the settings navigation and force it open. // This MUST be done after we've set up the page as it is going to cause theme and output to initialise. if (!$currentuser) { $PAGE->navigation->extend_for_user($user); if ($node = $PAGE->settingsnav->get('userviewingsettings'.$user->id)) { $node->forceopen = true; } } else if ($node = $PAGE->settingsnav->get('usercurrentsettings', navigation_node::TYPE_CONTAINER)) { $node->forceopen = true; } if ($node = $PAGE->settingsnav->get('courseadmin')) { $node->forceopen = false; } echo $OUTPUT->header(); echo '<div class="userprofile">'; $headerinfo = array('heading' => fullname($user), 'user' => $user, 'usercontext' => $usercontext); echo $OUTPUT->context_header($headerinfo, 2); if ($user->deleted) { echo $OUTPUT->heading(get_string('userdeleted')); if (!has_capability('moodle/user:update', $coursecontext)) { echo $OUTPUT->footer(); die; } } // OK, security out the way, now we are showing the user. // Trigger a user profile viewed event. profile_view($user, $coursecontext, $course); $hiddenfields = []; if (!has_capability('moodle/user:viewhiddendetails', $coursecontext)) { $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); } if ($user->description && !isset($hiddenfields['description'])) { echo '<div class="description">'; if (!empty($CFG->profilesforenrolledusersonly) && !$DB->record_exists('role_assignments', array('userid' => $id))) { echo get_string('profilenotshown', 'moodle'); } else { if ($courseid == SITEID) { $user->description = file_rewrite_pluginfile_urls($user->description, 'pluginfile.php', $usercontext->id, 'user', 'profile', null); } else { // We have to make a little detour thought the course context to verify the access control for course profile. $user->description = file_rewrite_pluginfile_urls($user->description, 'pluginfile.php', $coursecontext->id, 'user', 'profile', $user->id); } $options = array('overflowdiv' => true); echo format_text($user->description, $user->descriptionformat, $options); } echo '</div>'; // Description class. } // Render custom blocks. $renderer = $PAGE->get_renderer('core_user', 'myprofile'); $tree = core_user\output\myprofile\manager::build_tree($user, $currentuser, $course); echo $renderer->render($tree); echo '</div>'; // Userprofile class. echo $OUTPUT->footer(); editor_form.php 0000644 00000005007 15151162244 0007571 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form to edit a users editor preferences. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. } require_once($CFG->dirroot.'/lib/formslib.php'); /** * Class user_edit_editor_form. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_edit_editor_form extends moodleform { /** * Define the form. */ public function definition () { global $CFG, $COURSE; $mform = $this->_form; $editors = editors_get_enabled(); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); if (count($editors) > 1) { $choices = array('' => get_string('defaulteditor')); $firsteditor = ''; foreach (array_keys($editors) as $editor) { if (!$firsteditor) { $firsteditor = $editor; } $choices[$editor] = get_string('pluginname', 'editor_' . $editor); } $mform->addElement('select', 'preference_htmleditor', get_string('textediting'), $choices); $mform->addHelpButton('preference_htmleditor', 'textediting'); $mform->setDefault('preference_htmleditor', ''); } else { // Empty string means use the first chosen text editor. $mform->addElement('hidden', 'preference_htmleditor'); $mform->setDefault('preference_htmleditor', ''); $mform->setType('preference_htmleditor', PARAM_PLUGIN); } $this->add_action_buttons(true, get_string('savechanges')); } } grouppix.php 0000644 00000003443 15151162244 0007137 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This function fetches group pictures from the data directory. * * Syntax: pix.php/groupid/f1.jpg or pix.php/groupid/f2.jpg * OR: ?file=groupid/f1.jpg or ?file=groupid/f2.jpg * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ // Disable moodle specific debug messages and any errors in output. define('NO_DEBUG_DISPLAY', true); define('NO_MOODLE_COOKIES', true); // Session not used here. require_once('../config.php'); require_once($CFG->libdir.'/filelib.php'); $relativepath = get_file_argument(); $args = explode('/', trim($relativepath, '/')); if (count($args) == 2) { $groupid = (integer)$args[0]; $image = $args[1]; $pathname = $CFG->dataroot.'/groups/'.$groupid.'/'.$image; } else { $image = 'f1.png'; $pathname = $CFG->dirroot.'/pix/g/f1.png'; } if (file_exists($pathname) and !is_dir($pathname)) { send_file($pathname, $image); } else { header('HTTP/1.0 404 not found'); throw new \moodle_exception('filenotfound', 'error'); // This is not displayed on IIS?? } language_form.php 0000644 00000005632 15151162244 0010072 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form to edit a users preferred language * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. } require_once($CFG->dirroot.'/lib/formslib.php'); require_once($CFG->dirroot.'/user/lib.php'); /** * Class user_edit_form. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_edit_language_form extends moodleform { /** * Define the form. */ public function definition () { global $CFG, $COURSE, $USER; $mform = $this->_form; $userid = $USER->id; if (is_array($this->_customdata)) { if (array_key_exists('userid', $this->_customdata)) { $userid = $this->_customdata['userid']; } } // Add some extra hidden fields. $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'course', $COURSE->id); $mform->setType('course', PARAM_INT); $purpose = user_edit_map_field_purpose($userid, 'lang'); $translations = get_string_manager()->get_list_of_translations(); $mform->addElement('select', 'lang', get_string('preferredlanguage'), $translations, $purpose); $mform->setDefault('lang', core_user::get_property_default('lang')); $this->add_action_buttons(true, get_string('savechanges')); } /** * Extend the form definition after the data has been parsed. */ public function definition_after_data() { global $CFG, $DB, $OUTPUT; $mform = $this->_form; // If language does not exist, use site default lang. if ($langsel = $mform->getElementValue('lang')) { $lang = reset($langsel); // Check lang exists. if (!get_string_manager()->translation_exists($lang, false)) { $langel =& $mform->getElement('lang'); $langel->setValue(core_user::get_property_default('lang')); } } } } index.php 0000644 00000026424 15151162244 0006375 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Lists all the users within a given course. * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once('../config.php'); require_once($CFG->dirroot.'/user/lib.php'); require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->dirroot.'/notes/lib.php'); require_once($CFG->libdir.'/tablelib.php'); require_once($CFG->libdir.'/filelib.php'); require_once($CFG->dirroot.'/enrol/locallib.php'); use core_table\local\filter\filter; use core_table\local\filter\integer_filter; use core_table\local\filter\string_filter; $participantsperpage = intval(get_config('moodlecourse', 'participantsperpage')); define('DEFAULT_PAGE_SIZE', (!empty($participantsperpage) ? $participantsperpage : 20)); $page = optional_param('page', 0, PARAM_INT); // Which page to show. $perpage = optional_param('perpage', DEFAULT_PAGE_SIZE, PARAM_INT); // How many per page. $contextid = optional_param('contextid', 0, PARAM_INT); // One of this or. $courseid = optional_param('id', 0, PARAM_INT); // This are required. $newcourse = optional_param('newcourse', false, PARAM_BOOL); $roleid = optional_param('roleid', 0, PARAM_INT); $urlgroupid = optional_param('group', 0, PARAM_INT); $PAGE->set_url('/user/index.php', array( 'page' => $page, 'perpage' => $perpage, 'contextid' => $contextid, 'id' => $courseid, 'newcourse' => $newcourse)); if ($contextid) { $context = context::instance_by_id($contextid, MUST_EXIST); if ($context->contextlevel != CONTEXT_COURSE) { throw new \moodle_exception('invalidcontext'); } $course = $DB->get_record('course', array('id' => $context->instanceid), '*', MUST_EXIST); } else { $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $context = context_course::instance($course->id, MUST_EXIST); } // Not needed anymore. unset($contextid); unset($courseid); require_login($course); $systemcontext = context_system::instance(); $isfrontpage = ($course->id == SITEID); $frontpagectx = context_course::instance(SITEID); if ($isfrontpage) { $PAGE->set_pagelayout('admin'); course_require_view_participants($systemcontext); } else { $PAGE->set_pagelayout('incourse'); course_require_view_participants($context); } // Trigger events. user_list_view($course, $context); $PAGE->set_title("$course->shortname: ".get_string('participants')); $PAGE->set_heading($course->fullname); $PAGE->set_pagetype('course-view-participants'); $PAGE->set_docs_path('enrol/users'); $PAGE->add_body_class('path-user'); // So we can style it independently. $PAGE->set_other_editing_capability('moodle/course:manageactivities'); // Expand the users node in the settings navigation when it exists because those pages // are related to this one. $node = $PAGE->settingsnav->find('users', navigation_node::TYPE_CONTAINER); if ($node) { $node->force_open(); } echo $OUTPUT->header(); $participanttable = new \core_user\table\participants("user-index-participants-{$course->id}"); // Manage enrolments. $manager = new course_enrolment_manager($PAGE, $course); $enrolbuttons = $manager->get_manual_enrol_buttons(); $enrolrenderer = $PAGE->get_renderer('core_enrol'); $enrolbuttonsout = ''; foreach ($enrolbuttons as $enrolbutton) { $enrolbuttonsout .= $enrolrenderer->render($enrolbutton); } echo $OUTPUT->render_participants_tertiary_nav($course, html_writer::div($enrolbuttonsout, '', [ 'data-region' => 'wrapper', 'data-table-uniqueid' => $participanttable->uniqueid, ])); echo $OUTPUT->heading(get_string('enrolledusers', 'enrol')); $filterset = new \core_user\table\participants_filterset(); $filterset->add_filter(new integer_filter('courseid', filter::JOINTYPE_DEFAULT, [(int)$course->id])); $canaccessallgroups = has_capability('moodle/site:accessallgroups', $context); $filtergroupids = $urlgroupid ? [$urlgroupid] : []; // Force group filtering if user should only see a subset of groups' users. if ($course->groupmode != NOGROUPS && !$canaccessallgroups) { if ($filtergroupids) { $filtergroupids = array_intersect( $filtergroupids, array_keys(groups_get_all_groups($course->id, $USER->id)) ); } else { $filtergroupids = array_keys(groups_get_all_groups($course->id, $USER->id)); } if (empty($filtergroupids)) { if ($course->groupmode == SEPARATEGROUPS) { // The user is not in a group so show message and exit. echo $OUTPUT->notification(get_string('notingroup')); echo $OUTPUT->footer(); exit(); } else { $filtergroupids = [(int) groups_get_course_group($course, true)]; } } } // Apply groups filter if included in URL or forced due to lack of capabilities. if (!empty($filtergroupids)) { $filterset->add_filter(new integer_filter('groups', filter::JOINTYPE_DEFAULT, $filtergroupids)); } // Display single group information if requested in the URL. if ($urlgroupid > 0 && ($course->groupmode != SEPARATEGROUPS || $canaccessallgroups)) { $grouprenderer = $PAGE->get_renderer('core_group'); $groupdetailpage = new \core_group\output\group_details($urlgroupid); echo $grouprenderer->group_details($groupdetailpage); } // Filter by role if passed via URL (used on profile page). if ($roleid) { $viewableroles = get_profile_roles($context); // Apply filter if the user can view this role. if (array_key_exists($roleid, $viewableroles)) { $filterset->add_filter(new integer_filter('roles', filter::JOINTYPE_DEFAULT, [$roleid])); } } // Render the user filters. $userrenderer = $PAGE->get_renderer('core_user'); echo $userrenderer->participants_filter($context, $participanttable->uniqueid); echo '<div class="userlist">'; // Do this so we can get the total number of rows. ob_start(); $participanttable->set_filterset($filterset); $participanttable->out($perpage, true); $participanttablehtml = ob_get_contents(); ob_end_clean(); echo html_writer::start_tag('form', [ 'action' => 'action_redir.php', 'method' => 'post', 'id' => 'participantsform', 'data-course-id' => $course->id, 'data-table-unique-id' => $participanttable->uniqueid, ]); echo '<div>'; echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />'; echo '<input type="hidden" name="returnto" value="'.s($PAGE->url->out(false)).'" />'; echo html_writer::tag( 'p', get_string('countparticipantsfound', 'core_user', $participanttable->totalrows), [ 'data-region' => 'participant-count', ] ); echo $participanttablehtml; $bulkoptions = (object) [ 'uniqueid' => $participanttable->uniqueid, ]; echo '<br /><div class="buttons"><div class="form-inline">'; echo html_writer::start_tag('div', array('class' => 'btn-group')); if ($participanttable->get_page_size() < $participanttable->totalrows) { // Select all users, refresh table showing all users and mark them all selected. $label = get_string('selectalluserswithcount', 'moodle', $participanttable->totalrows); echo html_writer::empty_tag('input', [ 'type' => 'button', 'id' => 'checkall', 'class' => 'btn btn-secondary', 'value' => $label, 'data-target-page-size' => TABLE_SHOW_ALL_PAGE_SIZE, ]); } echo html_writer::end_tag('div'); $displaylist = array(); if (!empty($CFG->messaging) && has_all_capabilities(['moodle/site:sendmessage', 'moodle/course:bulkmessaging'], $context)) { $displaylist['#messageselect'] = get_string('messageselectadd'); } if (!empty($CFG->enablenotes) && has_capability('moodle/notes:manage', $context) && $context->id != $frontpagectx->id) { $displaylist['#addgroupnote'] = get_string('addnewnote', 'notes'); } $params = ['operation' => 'download_participants']; $downloadoptions = []; $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat'); foreach ($formats as $format) { if ($format->is_enabled()) { $params = ['operation' => 'download_participants', 'dataformat' => $format->name]; $url = new moodle_url('bulkchange.php', $params); $downloadoptions[$url->out(false)] = get_string('dataformat', $format->component); } } if (!empty($downloadoptions)) { $displaylist[] = [get_string('downloadas', 'table') => $downloadoptions]; } if ($context->id != $frontpagectx->id) { $instances = $manager->get_enrolment_instances(); $plugins = $manager->get_enrolment_plugins(false); foreach ($instances as $key => $instance) { if (!isset($plugins[$instance->enrol])) { // Weird, some broken stuff in plugin. continue; } $plugin = $plugins[$instance->enrol]; $bulkoperations = $plugin->get_bulk_operations($manager); $pluginoptions = []; foreach ($bulkoperations as $key => $bulkoperation) { $params = ['plugin' => $plugin->get_name(), 'operation' => $key]; $url = new moodle_url('bulkchange.php', $params); $pluginoptions[$url->out(false)] = $bulkoperation->get_title(); } if (!empty($pluginoptions)) { $name = get_string('pluginname', 'enrol_' . $plugin->get_name()); $displaylist[] = [$name => $pluginoptions]; } } } $selectactionparams = array( 'id' => 'formactionid', 'class' => 'ml-2', 'data-action' => 'toggle', 'data-togglegroup' => 'participants-table', 'data-toggle' => 'action', 'disabled' => 'disabled' ); $label = html_writer::tag('label', get_string("withselectedusers"), ['for' => 'formactionid', 'class' => 'col-form-label d-inline']); $select = html_writer::select($displaylist, 'formaction', '', ['' => 'choosedots'], $selectactionparams); echo html_writer::tag('div', $label . $select); echo '<input type="hidden" name="id" value="' . $course->id . '" />'; echo '<div class="d-none" data-region="state-help-icon">' . $OUTPUT->help_icon('publishstate', 'notes') . '</div>'; echo '</div></div></div>'; $bulkoptions->noteStateNames = note_get_state_names(); echo '</form>'; $PAGE->requires->js_call_amd('core_user/participants', 'init', [$bulkoptions]); echo '</div>'; // Userlist. $enrolrenderer = $PAGE->get_renderer('core_enrol'); // Need to re-generate the buttons to avoid having elements with duplicate ids on the page. $enrolbuttons = $manager->get_manual_enrol_buttons(); $enrolbuttonsout = ''; foreach ($enrolbuttons as $enrolbutton) { $enrolbuttonsout .= $enrolrenderer->render($enrolbutton); } echo html_writer::div($enrolbuttonsout, 'd-flex justify-content-end', [ 'data-region' => 'wrapper', 'data-table-uniqueid' => $participanttable->uniqueid, ]); echo $OUTPUT->footer(); lib.php 0000644 00000146740 15151162244 0006040 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * External user API * * @package core_user * @copyright 2009 Moodle Pty Ltd (http://moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('USER_FILTER_ENROLMENT', 1); define('USER_FILTER_GROUP', 2); define('USER_FILTER_LAST_ACCESS', 3); define('USER_FILTER_ROLE', 4); define('USER_FILTER_STATUS', 5); define('USER_FILTER_STRING', 6); /** * Creates a user * * @throws moodle_exception * @param stdClass $user user to create * @param bool $updatepassword if true, authentication plugin will update password. * @param bool $triggerevent set false if user_created event should not be triggred. * This will not affect user_password_updated event triggering. * @return int id of the newly created user */ function user_create_user($user, $updatepassword = true, $triggerevent = true) { global $DB; // Set the timecreate field to the current time. if (!is_object($user)) { $user = (object) $user; } // Check username. if (trim($user->username) === '') { throw new moodle_exception('invalidusernameblank'); } if ($user->username !== core_text::strtolower($user->username)) { throw new moodle_exception('usernamelowercase'); } if ($user->username !== core_user::clean_field($user->username, 'username')) { throw new moodle_exception('invalidusername'); } // Save the password in a temp value for later. if ($updatepassword && isset($user->password)) { // Check password toward the password policy. if (!check_password_policy($user->password, $errmsg, $user)) { throw new moodle_exception($errmsg); } $userpassword = $user->password; unset($user->password); } // Apply default values for user preferences that are stored in users table. if (!isset($user->calendartype)) { $user->calendartype = core_user::get_property_default('calendartype'); } if (!isset($user->maildisplay)) { $user->maildisplay = core_user::get_property_default('maildisplay'); } if (!isset($user->mailformat)) { $user->mailformat = core_user::get_property_default('mailformat'); } if (!isset($user->maildigest)) { $user->maildigest = core_user::get_property_default('maildigest'); } if (!isset($user->autosubscribe)) { $user->autosubscribe = core_user::get_property_default('autosubscribe'); } if (!isset($user->trackforums)) { $user->trackforums = core_user::get_property_default('trackforums'); } if (!isset($user->lang)) { $user->lang = core_user::get_property_default('lang'); } if (!isset($user->city)) { $user->city = core_user::get_property_default('city'); } if (!isset($user->country)) { // The default value of $CFG->country is 0, but that isn't a valid property for the user field, so switch to ''. $user->country = core_user::get_property_default('country') ?: ''; } $user->timecreated = time(); $user->timemodified = $user->timecreated; // Validate user data object. $uservalidation = core_user::validate($user); if ($uservalidation !== true) { foreach ($uservalidation as $field => $message) { debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER); $user->$field = core_user::clean_field($user->$field, $field); } } // Insert the user into the database. $newuserid = $DB->insert_record('user', $user); // Create USER context for this user. $usercontext = context_user::instance($newuserid); // Update user password if necessary. if (isset($userpassword)) { // Get full database user row, in case auth is default. $newuser = $DB->get_record('user', array('id' => $newuserid)); $authplugin = get_auth_plugin($newuser->auth); $authplugin->user_update_password($newuser, $userpassword); } // Trigger event If required. if ($triggerevent) { \core\event\user_created::create_from_userid($newuserid)->trigger(); } // Purge the associated caches for the current user only. $presignupcache = \cache::make('core', 'presignup'); $presignupcache->purge_current_user(); return $newuserid; } /** * Update a user with a user object (will compare against the ID) * * @throws moodle_exception * @param stdClass $user the user to update * @param bool $updatepassword if true, authentication plugin will update password. * @param bool $triggerevent set false if user_updated event should not be triggred. * This will not affect user_password_updated event triggering. */ function user_update_user($user, $updatepassword = true, $triggerevent = true) { global $DB; // Set the timecreate field to the current time. if (!is_object($user)) { $user = (object) $user; } // Check username. if (isset($user->username)) { if ($user->username !== core_text::strtolower($user->username)) { throw new moodle_exception('usernamelowercase'); } else { if ($user->username !== core_user::clean_field($user->username, 'username')) { throw new moodle_exception('invalidusername'); } } } // Unset password here, for updating later, if password update is required. if ($updatepassword && isset($user->password)) { // Check password toward the password policy. if (!check_password_policy($user->password, $errmsg, $user)) { throw new moodle_exception($errmsg); } $passwd = $user->password; unset($user->password); } // Make sure calendartype, if set, is valid. if (empty($user->calendartype)) { // Unset this variable, must be an empty string, which we do not want to update the calendartype to. unset($user->calendartype); } $user->timemodified = time(); // Validate user data object. $uservalidation = core_user::validate($user); if ($uservalidation !== true) { foreach ($uservalidation as $field => $message) { debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER); $user->$field = core_user::clean_field($user->$field, $field); } } $DB->update_record('user', $user); if ($updatepassword) { // Get full user record. $updateduser = $DB->get_record('user', array('id' => $user->id)); // If password was set, then update its hash. if (isset($passwd)) { $authplugin = get_auth_plugin($updateduser->auth); if ($authplugin->can_change_password()) { $authplugin->user_update_password($updateduser, $passwd); } } } // Trigger event if required. if ($triggerevent) { \core\event\user_updated::create_from_userid($user->id)->trigger(); } } /** * Marks user deleted in internal user database and notifies the auth plugin. * Also unenrols user from all roles and does other cleanup. * * @todo Decide if this transaction is really needed (look for internal TODO:) * @param object $user Userobject before delete (without system magic quotes) * @return boolean success */ function user_delete_user($user) { return delete_user($user); } /** * Get users by id * * @param array $userids id of users to retrieve * @return array */ function user_get_users_by_id($userids) { global $DB; return $DB->get_records_list('user', 'id', $userids); } /** * Returns the list of default 'displayable' fields * * Contains database field names but also names used to generate information, such as enrolledcourses * * @return array of user fields */ function user_get_default_fields() { return array( 'id', 'username', 'fullname', 'firstname', 'lastname', 'email', 'address', 'phone1', 'phone2', 'department', 'institution', 'interests', 'firstaccess', 'lastaccess', 'auth', 'confirmed', 'idnumber', 'lang', 'theme', 'timezone', 'mailformat', 'description', 'descriptionformat', 'city', 'country', 'profileimageurlsmall', 'profileimageurl', 'customfields', 'groups', 'roles', 'preferences', 'enrolledcourses', 'suspended', 'lastcourseaccess' ); } /** * * Give user record from mdl_user, build an array contains all user details. * * Warning: description file urls are 'webservice/pluginfile.php' is use. * it can be changed with $CFG->moodlewstextformatlinkstoimagesfile * * @throws moodle_exception * @param stdClass $user user record from mdl_user * @param stdClass $course moodle course * @param array $userfields required fields * @return array|null */ function user_get_user_details($user, $course = null, array $userfields = array()) { global $USER, $DB, $CFG, $PAGE; require_once($CFG->dirroot . "/user/profile/lib.php"); // Custom field library. require_once($CFG->dirroot . "/lib/filelib.php"); // File handling on description and friends. $defaultfields = user_get_default_fields(); if (empty($userfields)) { $userfields = $defaultfields; } foreach ($userfields as $thefield) { if (!in_array($thefield, $defaultfields)) { throw new moodle_exception('invaliduserfield', 'error', '', $thefield); } } // Make sure id and fullname are included. if (!in_array('id', $userfields)) { $userfields[] = 'id'; } if (!in_array('fullname', $userfields)) { $userfields[] = 'fullname'; } if (!empty($course)) { $context = context_course::instance($course->id); $usercontext = context_user::instance($user->id); $canviewdetailscap = (has_capability('moodle/user:viewdetails', $context) || has_capability('moodle/user:viewdetails', $usercontext)); } else { $context = context_user::instance($user->id); $usercontext = $context; $canviewdetailscap = has_capability('moodle/user:viewdetails', $usercontext); } $currentuser = ($user->id == $USER->id); $isadmin = is_siteadmin($USER); // This does not need to include custom profile fields as it is only used to check specific // fields below. $showuseridentityfields = \core_user\fields::get_identity_fields($context, false); if (!empty($course)) { $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context); } else { $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context); } $canviewfullnames = has_capability('moodle/site:viewfullnames', $context); if (!empty($course)) { $canviewuseremail = has_capability('moodle/course:useremail', $context); } else { $canviewuseremail = false; } $cannotviewdescription = !empty($CFG->profilesforenrolledusersonly) && !$currentuser && !$DB->record_exists('role_assignments', array('userid' => $user->id)); if (!empty($course)) { $canaccessallgroups = has_capability('moodle/site:accessallgroups', $context); } else { $canaccessallgroups = false; } if (!$currentuser && !$canviewdetailscap && !has_coursecontact_role($user->id)) { // Skip this user details. return null; } $userdetails = array(); $userdetails['id'] = $user->id; if (in_array('username', $userfields)) { if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) { $userdetails['username'] = $user->username; } } if ($isadmin or $canviewfullnames) { if (in_array('firstname', $userfields)) { $userdetails['firstname'] = $user->firstname; } if (in_array('lastname', $userfields)) { $userdetails['lastname'] = $user->lastname; } } $userdetails['fullname'] = fullname($user, $canviewfullnames); if (in_array('customfields', $userfields)) { $categories = profile_get_user_fields_with_data_by_category($user->id); $userdetails['customfields'] = array(); foreach ($categories as $categoryid => $fields) { foreach ($fields as $formfield) { if ($formfield->is_visible() and !$formfield->is_empty()) { // TODO: Part of MDL-50728, this conditional coding must be moved to // proper profile fields API so they are self-contained. // We only use display_data in fields that require text formatting. if ($formfield->field->datatype == 'text' or $formfield->field->datatype == 'textarea') { $fieldvalue = $formfield->display_data(); } else { // Cases: datetime, checkbox and menu. $fieldvalue = $formfield->data; } $userdetails['customfields'][] = array('name' => $formfield->field->name, 'value' => $fieldvalue, 'type' => $formfield->field->datatype, 'shortname' => $formfield->field->shortname); } } } // Unset customfields if it's empty. if (empty($userdetails['customfields'])) { unset($userdetails['customfields']); } } // Profile image. if (in_array('profileimageurl', $userfields)) { $userpicture = new user_picture($user); $userpicture->size = 1; // Size f1. $userdetails['profileimageurl'] = $userpicture->get_url($PAGE)->out(false); } if (in_array('profileimageurlsmall', $userfields)) { if (!isset($userpicture)) { $userpicture = new user_picture($user); } $userpicture->size = 0; // Size f2. $userdetails['profileimageurlsmall'] = $userpicture->get_url($PAGE)->out(false); } // Hidden user field. if ($canviewhiddenuserfields) { $hiddenfields = array(); } else { $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); } if (!empty($user->address) && (in_array('address', $userfields) && in_array('address', $showuseridentityfields) || $isadmin)) { $userdetails['address'] = $user->address; } if (!empty($user->phone1) && (in_array('phone1', $userfields) && in_array('phone1', $showuseridentityfields) || $isadmin)) { $userdetails['phone1'] = $user->phone1; } if (!empty($user->phone2) && (in_array('phone2', $userfields) && in_array('phone2', $showuseridentityfields) || $isadmin)) { $userdetails['phone2'] = $user->phone2; } if (isset($user->description) && ((!isset($hiddenfields['description']) && !$cannotviewdescription) or $isadmin)) { if (in_array('description', $userfields)) { // Always return the descriptionformat if description is requested. list($userdetails['description'], $userdetails['descriptionformat']) = external_format_text($user->description, $user->descriptionformat, $usercontext->id, 'user', 'profile', null); } } if (in_array('country', $userfields) && (!isset($hiddenfields['country']) or $isadmin) && $user->country) { $userdetails['country'] = $user->country; } if (in_array('city', $userfields) && (!isset($hiddenfields['city']) or $isadmin) && $user->city) { $userdetails['city'] = $user->city; } if (in_array('timezone', $userfields) && (!isset($hiddenfields['timezone']) || $isadmin) && $user->timezone) { $userdetails['timezone'] = $user->timezone; } if (in_array('suspended', $userfields) && (!isset($hiddenfields['suspended']) or $isadmin)) { $userdetails['suspended'] = (bool)$user->suspended; } if (in_array('firstaccess', $userfields) && (!isset($hiddenfields['firstaccess']) or $isadmin)) { if ($user->firstaccess) { $userdetails['firstaccess'] = $user->firstaccess; } else { $userdetails['firstaccess'] = 0; } } if (in_array('lastaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) { if ($user->lastaccess) { $userdetails['lastaccess'] = $user->lastaccess; } else { $userdetails['lastaccess'] = 0; } } // Hidden fields restriction to lastaccess field applies to both site and course access time. if (in_array('lastcourseaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) { if (isset($user->lastcourseaccess)) { $userdetails['lastcourseaccess'] = $user->lastcourseaccess; } else { $userdetails['lastcourseaccess'] = 0; } } if (in_array('email', $userfields) && ( $currentuser or (!isset($hiddenfields['email']) and ( $user->maildisplay == core_user::MAILDISPLAY_EVERYONE or ($user->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY and enrol_sharing_course($user, $USER)) or $canviewuseremail // TODO: Deprecate/remove for MDL-37479. )) or in_array('email', $showuseridentityfields) )) { $userdetails['email'] = $user->email; } if (in_array('interests', $userfields)) { $interests = core_tag_tag::get_item_tags_array('core', 'user', $user->id, core_tag_tag::BOTH_STANDARD_AND_NOT, 0, false); if ($interests) { $userdetails['interests'] = join(', ', $interests); } } // Departement/Institution/Idnumber are not displayed on any profile, however you can get them from editing profile. if (in_array('idnumber', $userfields) && $user->idnumber) { if (in_array('idnumber', $showuseridentityfields) or $currentuser or has_capability('moodle/user:viewalldetails', $context)) { $userdetails['idnumber'] = $user->idnumber; } } if (in_array('institution', $userfields) && $user->institution) { if (in_array('institution', $showuseridentityfields) or $currentuser or has_capability('moodle/user:viewalldetails', $context)) { $userdetails['institution'] = $user->institution; } } // Isset because it's ok to have department 0. if (in_array('department', $userfields) && isset($user->department)) { if (in_array('department', $showuseridentityfields) or $currentuser or has_capability('moodle/user:viewalldetails', $context)) { $userdetails['department'] = $user->department; } } if (in_array('roles', $userfields) && !empty($course)) { // Not a big secret. $roles = get_user_roles($context, $user->id, false); $userdetails['roles'] = array(); foreach ($roles as $role) { $userdetails['roles'][] = array( 'roleid' => $role->roleid, 'name' => $role->name, 'shortname' => $role->shortname, 'sortorder' => $role->sortorder ); } } // Return user groups. if (in_array('groups', $userfields) && !empty($course)) { if ($usergroups = groups_get_all_groups($course->id, $user->id)) { $userdetails['groups'] = []; foreach ($usergroups as $group) { if ($course->groupmode == SEPARATEGROUPS && !$canaccessallgroups && $user->id != $USER->id) { // In separate groups, I only have to see the groups shared between both users. if (!groups_is_member($group->id, $USER->id)) { continue; } } $userdetails['groups'][] = [ 'id' => $group->id, 'name' => format_string($group->name), 'description' => format_text($group->description, $group->descriptionformat, ['context' => $context]), 'descriptionformat' => $group->descriptionformat ]; } } } // List of courses where the user is enrolled. if (in_array('enrolledcourses', $userfields) && !isset($hiddenfields['mycourses'])) { $enrolledcourses = array(); if ($mycourses = enrol_get_users_courses($user->id, true)) { foreach ($mycourses as $mycourse) { if ($mycourse->category) { $coursecontext = context_course::instance($mycourse->id); $enrolledcourse = array(); $enrolledcourse['id'] = $mycourse->id; $enrolledcourse['fullname'] = format_string($mycourse->fullname, true, array('context' => $coursecontext)); $enrolledcourse['shortname'] = format_string($mycourse->shortname, true, array('context' => $coursecontext)); $enrolledcourses[] = $enrolledcourse; } } $userdetails['enrolledcourses'] = $enrolledcourses; } } // User preferences. if (in_array('preferences', $userfields) && $currentuser) { $preferences = array(); $userpreferences = get_user_preferences(); foreach ($userpreferences as $prefname => $prefvalue) { $preferences[] = array('name' => $prefname, 'value' => $prefvalue); } $userdetails['preferences'] = $preferences; } if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) { $extrafields = ['auth', 'confirmed', 'lang', 'theme', 'mailformat']; foreach ($extrafields as $extrafield) { if (in_array($extrafield, $userfields) && isset($user->$extrafield)) { $userdetails[$extrafield] = $user->$extrafield; } } } // Clean lang and auth fields for external functions (it may content uninstalled themes or language packs). if (isset($userdetails['lang'])) { $userdetails['lang'] = clean_param($userdetails['lang'], PARAM_LANG); } if (isset($userdetails['theme'])) { $userdetails['theme'] = clean_param($userdetails['theme'], PARAM_THEME); } return $userdetails; } /** * Tries to obtain user details, either recurring directly to the user's system profile * or through one of the user's course enrollments (course profile). * * You can use the $userfields parameter to reduce the amount of a user record that is required by the method. * The minimum user fields are: * * id * * deleted * * all potential fullname fields * * @param stdClass $user The user. * @param array $userfields An array of userfields to be returned, the values must be a * subset of user_get_default_fields (optional) * @return array if unsuccessful or the allowed user details. */ function user_get_user_details_courses($user, array $userfields = []) { global $USER; $userdetails = null; $systemprofile = false; if (can_view_user_details_cap($user) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) { $systemprofile = true; } // Try using system profile. if ($systemprofile) { $userdetails = user_get_user_details($user, null, $userfields); } else { // Try through course profile. // Get the courses that the user is enrolled in (only active). $courses = enrol_get_users_courses($user->id, true); foreach ($courses as $course) { if (user_can_view_profile($user, $course)) { $userdetails = user_get_user_details($user, $course, $userfields); } } } return $userdetails; } /** * Check if $USER have the necessary capabilities to obtain user details. * * @param stdClass $user * @param stdClass $course if null then only consider system profile otherwise also consider the course's profile. * @return bool true if $USER can view user details. */ function can_view_user_details_cap($user, $course = null) { // Check $USER has the capability to view the user details at user context. $usercontext = context_user::instance($user->id); $result = has_capability('moodle/user:viewdetails', $usercontext); // Otherwise can $USER see them at course context. if (!$result && !empty($course)) { $context = context_course::instance($course->id); $result = has_capability('moodle/user:viewdetails', $context); } return $result; } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block * @return array */ function user_page_type_list($pagetype, $parentcontext, $currentcontext) { return array('user-profile' => get_string('page-user-profile', 'pagetype')); } /** * Count the number of failed login attempts for the given user, since last successful login. * * @param int|stdclass $user user id or object. * @param bool $reset Resets failed login count, if set to true. * * @return int number of failed login attempts since the last successful login. */ function user_count_login_failures($user, $reset = true) { global $DB; if (!is_object($user)) { $user = $DB->get_record('user', array('id' => $user), '*', MUST_EXIST); } if ($user->deleted) { // Deleted user, nothing to do. return 0; } $count = get_user_preferences('login_failed_count_since_success', 0, $user); if ($reset) { set_user_preference('login_failed_count_since_success', 0, $user); } return $count; } /** * Converts a string into a flat array of menu items, where each menu items is a * stdClass with fields type, url, title. * * @param string $text the menu items definition * @param moodle_page $page the current page * @return array */ function user_convert_text_to_menu_items($text, $page) { global $OUTPUT, $CFG; $lines = explode("\n", $text); $items = array(); $lastchild = null; $lastdepth = null; $lastsort = 0; $children = array(); foreach ($lines as $line) { $line = trim($line); $bits = explode('|', $line, 2); $itemtype = 'link'; if (preg_match("/^#+$/", $line)) { $itemtype = 'divider'; } else if (!array_key_exists(0, $bits) or empty($bits[0])) { // Every item must have a name to be valid. continue; } else { $bits[0] = ltrim($bits[0], '-'); } // Create the child. $child = new stdClass(); $child->itemtype = $itemtype; if ($itemtype === 'divider') { // Add the divider to the list of children and skip link // processing. $children[] = $child; continue; } // Name processing. $namebits = explode(',', $bits[0], 2); if (count($namebits) == 2) { // Check the validity of the identifier part of the string. if (clean_param($namebits[0], PARAM_STRINGID) !== '') { // Treat this as a language string. $child->title = get_string($namebits[0], $namebits[1]); $child->titleidentifier = implode(',', $namebits); } } if (empty($child->title)) { // Use it as is, don't even clean it. $child->title = $bits[0]; $child->titleidentifier = str_replace(" ", "-", $bits[0]); } // URL processing. if (!array_key_exists(1, $bits) or empty($bits[1])) { // Set the url to null, and set the itemtype to invalid. $bits[1] = null; $child->itemtype = "invalid"; } else { // Nasty hack to replace the grades with the direct url. if (strpos($bits[1], '/grade/report/mygrades.php') !== false) { $bits[1] = user_mygrades_url(); } // Make sure the url is a moodle url. $bits[1] = new moodle_url(trim($bits[1])); } $child->url = $bits[1]; // Add this child to the list of children. $children[] = $child; } return $children; } /** * Get a list of essential user navigation items. * * @param stdclass $user user object. * @param moodle_page $page page object. * @param array $options associative array. * options are: * - avatarsize=35 (size of avatar image) * @return stdClass $returnobj navigation information object, where: * * $returnobj->navitems array array of links where each link is a * stdClass with fields url, title, and * pix * $returnobj->metadata array array of useful user metadata to be * used when constructing navigation; * fields include: * * ROLE FIELDS * asotherrole bool whether viewing as another role * rolename string name of the role * * USER FIELDS * These fields are for the currently-logged in user, or for * the user that the real user is currently logged in as. * * userid int the id of the user in question * userfullname string the user's full name * userprofileurl moodle_url the url of the user's profile * useravatar string a HTML fragment - the rendered * user_picture for this user * userloginfail string an error string denoting the number * of login failures since last login * * "REAL USER" FIELDS * These fields are for when asotheruser is true, and * correspond to the underlying "real user". * * asotheruser bool whether viewing as another user * realuserid int the id of the user in question * realuserfullname string the user's full name * realuserprofileurl moodle_url the url of the user's profile * realuseravatar string a HTML fragment - the rendered * user_picture for this user * * MNET PROVIDER FIELDS * asmnetuser bool whether viewing as a user from an * MNet provider * mnetidprovidername string name of the MNet provider * mnetidproviderwwwroot string URL of the MNet provider */ function user_get_user_navigation_info($user, $page, $options = array()) { global $OUTPUT, $DB, $SESSION, $CFG; $returnobject = new stdClass(); $returnobject->navitems = array(); $returnobject->metadata = array(); $guest = isguestuser(); if (!isloggedin() || $guest) { $returnobject->unauthenticateduser = [ 'guest' => $guest, 'content' => $guest ? 'loggedinasguest' : 'loggedinnot', ]; return $returnobject; } $course = $page->course; // Query the environment. $context = context_course::instance($course->id); // Get basic user metadata. $returnobject->metadata['userid'] = $user->id; $returnobject->metadata['userfullname'] = fullname($user); $returnobject->metadata['userprofileurl'] = new moodle_url('/user/profile.php', array( 'id' => $user->id )); $avataroptions = array('link' => false, 'visibletoscreenreaders' => false); if (!empty($options['avatarsize'])) { $avataroptions['size'] = $options['avatarsize']; } $returnobject->metadata['useravatar'] = $OUTPUT->user_picture ( $user, $avataroptions ); // Build a list of items for a regular user. // Query MNet status. if ($returnobject->metadata['asmnetuser'] = is_mnet_remote_user($user)) { $mnetidprovider = $DB->get_record('mnet_host', array('id' => $user->mnethostid)); $returnobject->metadata['mnetidprovidername'] = $mnetidprovider->name; $returnobject->metadata['mnetidproviderwwwroot'] = $mnetidprovider->wwwroot; } // Did the user just log in? if (isset($SESSION->justloggedin)) { // Don't unset this flag as login_info still needs it. if (!empty($CFG->displayloginfailures)) { // Don't reset the count either, as login_info() still needs it too. if ($count = user_count_login_failures($user, false)) { // Get login failures string. $a = new stdClass(); $a->attempts = html_writer::tag('span', $count, array('class' => 'value mr-1 font-weight-bold')); $returnobject->metadata['userloginfail'] = get_string('failedloginattempts', '', $a); } } } $returnobject->metadata['asotherrole'] = false; // Before we add the last items (usually a logout + switch role link), add any // custom-defined items. $customitems = user_convert_text_to_menu_items($CFG->customusermenuitems, $page); $custommenucount = 0; foreach ($customitems as $item) { $returnobject->navitems[] = $item; if ($item->itemtype !== 'divider' && $item->itemtype !== 'invalid') { $custommenucount++; } } if ($custommenucount > 0) { // Only add a divider if we have customusermenuitems. $divider = new stdClass(); $divider->itemtype = 'divider'; $returnobject->navitems[] = $divider; } // Links: Preferences. $preferences = new stdClass(); $preferences->itemtype = 'link'; $preferences->url = new moodle_url('/user/preferences.php'); $preferences->title = get_string('preferences'); $preferences->titleidentifier = 'preferences,moodle'; $returnobject->navitems[] = $preferences; if (is_role_switched($course->id)) { if ($role = $DB->get_record('role', array('id' => $user->access['rsw'][$context->path]))) { // Build role-return link instead of logout link. $rolereturn = new stdClass(); $rolereturn->itemtype = 'link'; $rolereturn->url = new moodle_url('/course/switchrole.php', array( 'id' => $course->id, 'sesskey' => sesskey(), 'switchrole' => 0, 'returnurl' => $page->url->out_as_local_url(false) )); $rolereturn->title = get_string('switchrolereturn'); $rolereturn->titleidentifier = 'switchrolereturn,moodle'; $returnobject->navitems[] = $rolereturn; $returnobject->metadata['asotherrole'] = true; $returnobject->metadata['rolename'] = role_get_name($role, $context); } } else { // Build switch role link. $roles = get_switchable_roles($context); if (is_array($roles) && (count($roles) > 0)) { $switchrole = new stdClass(); $switchrole->itemtype = 'link'; $switchrole->url = new moodle_url('/course/switchrole.php', array( 'id' => $course->id, 'switchrole' => -1, 'returnurl' => $page->url->out_as_local_url(false) )); $switchrole->title = get_string('switchroleto'); $switchrole->titleidentifier = 'switchroleto,moodle'; $returnobject->navitems[] = $switchrole; } } if ($returnobject->metadata['asotheruser'] = \core\session\manager::is_loggedinas()) { $realuser = \core\session\manager::get_realuser(); // Save values for the real user, as $user will be full of data for the // user is disguised as. $returnobject->metadata['realuserid'] = $realuser->id; $returnobject->metadata['realuserfullname'] = fullname($realuser); $returnobject->metadata['realuserprofileurl'] = new moodle_url('/user/profile.php', [ 'id' => $realuser->id ]); $returnobject->metadata['realuseravatar'] = $OUTPUT->user_picture($realuser, $avataroptions); // Build a user-revert link. $userrevert = new stdClass(); $userrevert->itemtype = 'link'; $userrevert->url = new moodle_url('/course/loginas.php', [ 'id' => $course->id, 'sesskey' => sesskey() ]); $userrevert->title = get_string('logout'); $userrevert->titleidentifier = 'logout,moodle'; $returnobject->navitems[] = $userrevert; } else { // Build a logout link. $logout = new stdClass(); $logout->itemtype = 'link'; $logout->url = new moodle_url('/login/logout.php', ['sesskey' => sesskey()]); $logout->title = get_string('logout'); $logout->titleidentifier = 'logout,moodle'; $returnobject->navitems[] = $logout; } return $returnobject; } /** * Add password to the list of used hashes for this user. * * This is supposed to be used from: * 1/ change own password form * 2/ password reset process * 3/ user signup in auth plugins if password changing supported * * @param int $userid user id * @param string $password plaintext password * @return void */ function user_add_password_history($userid, $password) { global $CFG, $DB; if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) { return; } // Note: this is using separate code form normal password hashing because // we need to have this under control in the future. Also the auth // plugin might not store the passwords locally at all. $record = new stdClass(); $record->userid = $userid; $record->hash = password_hash($password, PASSWORD_DEFAULT); $record->timecreated = time(); $DB->insert_record('user_password_history', $record); $i = 0; $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC'); foreach ($records as $record) { $i++; if ($i > $CFG->passwordreuselimit) { $DB->delete_records('user_password_history', array('id' => $record->id)); } } } /** * Was this password used before on change or reset password page? * * The $CFG->passwordreuselimit setting determines * how many times different password needs to be used * before allowing previously used password again. * * @param int $userid user id * @param string $password plaintext password * @return bool true if password reused */ function user_is_previously_used_password($userid, $password) { global $CFG, $DB; if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) { return false; } $reused = false; $i = 0; $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC'); foreach ($records as $record) { $i++; if ($i > $CFG->passwordreuselimit) { $DB->delete_records('user_password_history', array('id' => $record->id)); continue; } // NOTE: this is slow but we cannot compare the hashes directly any more. if (password_verify($password, $record->hash)) { $reused = true; } } return $reused; } /** * Remove a user device from the Moodle database (for PUSH notifications usually). * * @param string $uuid The device UUID. * @param string $appid The app id. If empty all the devices matching the UUID for the user will be removed. * @return bool true if removed, false if the device didn't exists in the database * @since Moodle 2.9 */ function user_remove_user_device($uuid, $appid = "") { global $DB, $USER; $conditions = array('uuid' => $uuid, 'userid' => $USER->id); if (!empty($appid)) { $conditions['appid'] = $appid; } if (!$DB->count_records('user_devices', $conditions)) { return false; } $DB->delete_records('user_devices', $conditions); return true; } /** * Trigger user_list_viewed event. * * @param stdClass $course course object * @param stdClass $context course context object * @since Moodle 2.9 */ function user_list_view($course, $context) { $event = \core\event\user_list_viewed::create(array( 'objectid' => $course->id, 'courseid' => $course->id, 'context' => $context, 'other' => array( 'courseshortname' => $course->shortname, 'coursefullname' => $course->fullname ) )); $event->trigger(); } /** * Returns the url to use for the "Grades" link in the user navigation. * * @param int $userid The user's ID. * @param int $courseid The course ID if available. * @return mixed A URL to be directed to for "Grades". */ function user_mygrades_url($userid = null, $courseid = SITEID) { global $CFG, $USER; $url = null; if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report != 'external') { if (isset($userid) && $USER->id != $userid) { // Send to the gradebook report. $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php', array('id' => $courseid, 'userid' => $userid)); } else { $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php'); } } else if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report == 'external' && !empty($CFG->gradereport_mygradeurl)) { $url = $CFG->gradereport_mygradeurl; } else { $url = $CFG->wwwroot; } return $url; } /** * Check if the current user has permission to view details of the supplied user. * * This function supports two modes: * If the optional $course param is omitted, then this function finds all shared courses and checks whether the current user has * permission in any of them, returning true if so. * If the $course param is provided, then this function checks permissions in ONLY that course. * * @param object $user The other user's details. * @param object $course if provided, only check permissions in this course. * @param context $usercontext The user context if available. * @return bool true for ability to view this user, else false. */ function user_can_view_profile($user, $course = null, $usercontext = null) { global $USER, $CFG; if ($user->deleted) { return false; } // Do we need to be logged in? if (empty($CFG->forceloginforprofiles)) { return true; } else { if (!isloggedin() || isguestuser()) { // User is not logged in and forceloginforprofile is set, we need to return now. return false; } } // Current user can always view their profile. if ($USER->id == $user->id) { return true; } // Use callbacks so that (primarily) local plugins can prevent or allow profile access. $forceallow = false; $plugintypes = get_plugins_with_function('control_view_profile'); foreach ($plugintypes as $plugins) { foreach ($plugins as $pluginfunction) { $result = $pluginfunction($user, $course, $usercontext); switch ($result) { case core_user::VIEWPROFILE_DO_NOT_PREVENT: // If the plugin doesn't stop access, just continue to next plugin or use // default behaviour. break; case core_user::VIEWPROFILE_FORCE_ALLOW: // Record that we are definitely going to allow it (unless another plugin // returns _PREVENT). $forceallow = true; break; case core_user::VIEWPROFILE_PREVENT: // If any plugin returns PREVENT then we return false, regardless of what // other plugins said. return false; } } } if ($forceallow) { return true; } // Course contacts have visible profiles always. if (has_coursecontact_role($user->id)) { return true; } // If we're only checking the capabilities in the single provided course. if (isset($course)) { // Confirm that $user is enrolled in the $course we're checking. if (is_enrolled(context_course::instance($course->id), $user)) { $userscourses = array($course); } } else { // Else we're checking whether the current user can view $user's profile anywhere, so check user context first. if (empty($usercontext)) { $usercontext = context_user::instance($user->id); } if (has_capability('moodle/user:viewdetails', $usercontext) || has_capability('moodle/user:viewalldetails', $usercontext)) { return true; } // This returns context information, so we can preload below. $userscourses = enrol_get_all_users_courses($user->id); } if (empty($userscourses)) { return false; } foreach ($userscourses as $userscourse) { context_helper::preload_from_record($userscourse); $coursecontext = context_course::instance($userscourse->id); if (has_capability('moodle/user:viewdetails', $coursecontext) || has_capability('moodle/user:viewalldetails', $coursecontext)) { if (!groups_user_groups_visible($userscourse, $user->id)) { // Not a member of the same group. continue; } return true; } } return false; } /** * Returns users tagged with a specified tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function user_get_tagged_users($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { global $PAGE; if ($ctx && $ctx != context_system::instance()->id) { $usercount = 0; } else { // Users can only be displayed in system context. $usercount = $tag->count_tagged_items('core', 'user', 'it.deleted=:notdeleted', array('notdeleted' => 0)); } $perpage = $exclusivemode ? 24 : 5; $content = ''; $totalpages = ceil($usercount / $perpage); if ($usercount) { $userlist = $tag->get_tagged_items('core', 'user', $page * $perpage, $perpage, 'it.deleted=:notdeleted', array('notdeleted' => 0)); $renderer = $PAGE->get_renderer('core', 'user'); $content .= $renderer->user_list($userlist, $exclusivemode); } return new core_tag\output\tagindex($tag, 'core', 'user', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } /** * Returns SQL that can be used to limit a query to a period where the user last accessed / did not access a course. * * @param int $accesssince The unix timestamp to compare to users' last access * @param string $tableprefix * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional) * @return string */ function user_get_course_lastaccess_sql($accesssince = null, $tableprefix = 'ul', $haveaccessed = false) { return user_get_lastaccess_sql('timeaccess', $accesssince, $tableprefix, $haveaccessed); } /** * Returns SQL that can be used to limit a query to a period where the user last accessed / did not access the system. * * @param int $accesssince The unix timestamp to compare to users' last access * @param string $tableprefix * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional) * @return string */ function user_get_user_lastaccess_sql($accesssince = null, $tableprefix = 'u', $haveaccessed = false) { return user_get_lastaccess_sql('lastaccess', $accesssince, $tableprefix, $haveaccessed); } /** * Returns SQL that can be used to limit a query to a period where the user last accessed or * did not access something recorded by a given table. * * @param string $columnname The name of the access column to check against * @param int $accesssince The unix timestamp to compare to users' last access * @param string $tableprefix The query prefix of the table to check * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional) * @return string */ function user_get_lastaccess_sql($columnname, $accesssince, $tableprefix, $haveaccessed = false) { if (empty($accesssince)) { return ''; } // Only users who have accessed since $accesssince. if ($haveaccessed) { if ($accesssince == -1) { // Include all users who have logged in at some point. $sql = "({$tableprefix}.{$columnname} IS NOT NULL AND {$tableprefix}.{$columnname} != 0)"; } else { // Users who have accessed since the specified time. $sql = "{$tableprefix}.{$columnname} IS NOT NULL AND {$tableprefix}.{$columnname} != 0 AND {$tableprefix}.{$columnname} >= {$accesssince}"; } } else { // Only users who have not accessed since $accesssince. if ($accesssince == -1) { // Users who have never accessed. $sql = "({$tableprefix}.{$columnname} IS NULL OR {$tableprefix}.{$columnname} = 0)"; } else { // Users who have not accessed since the specified time. $sql = "({$tableprefix}.{$columnname} IS NULL OR ({$tableprefix}.{$columnname} != 0 AND {$tableprefix}.{$columnname} < {$accesssince}))"; } } return $sql; } /** * Callback for inplace editable API. * * @param string $itemtype - Only user_roles is supported. * @param string $itemid - Courseid and userid separated by a : * @param string $newvalue - json encoded list of roleids. * @return \core\output\inplace_editable */ function core_user_inplace_editable($itemtype, $itemid, $newvalue) { if ($itemtype === 'user_roles') { return \core_user\output\user_roles_editable::update($itemid, $newvalue); } } /** * Map an internal field name to a valid purpose from: "https://www.w3.org/TR/WCAG21/#input-purposes" * * @param integer $userid * @param string $fieldname * @return string $purpose (empty string if there is no mapping). */ function user_edit_map_field_purpose($userid, $fieldname) { global $USER; $currentuser = ($userid == $USER->id) && !\core\session\manager::is_loggedinas(); // These are the fields considered valid to map and auto fill from a browser. // We do not include fields that are in a collapsed section by default because // the browser could auto-fill the field and cause a new value to be saved when // that field was never visible. $validmappings = array( 'username' => 'username', 'password' => 'current-password', 'firstname' => 'given-name', 'lastname' => 'family-name', 'middlename' => 'additional-name', 'email' => 'email', 'country' => 'country', 'lang' => 'language' ); $purpose = ''; // Only set a purpose when editing your own user details. if ($currentuser && isset($validmappings[$fieldname])) { $purpose = ' autocomplete="' . $validmappings[$fieldname] . '" '; } return $purpose; } contactsitesupport.php 0000644 00000005747 15151162244 0011250 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contact site support. * * @copyright 2022 Simey Lameze <simey@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once('../config.php'); require_once($CFG->dirroot . '/user/lib.php'); $user = isloggedin() && !isguestuser() ? $USER : null; // If not allowed to view this page, redirect to the homepage. This would be where the site has // disabled support, or limited it to authenticated users and the current user is a guest or not logged in. if (!isset($CFG->supportavailability) || $CFG->supportavailability == CONTACT_SUPPORT_DISABLED || ($CFG->supportavailability == CONTACT_SUPPORT_AUTHENTICATED && is_null($user))) { redirect($CFG->wwwroot); } if (!empty($CFG->supportpage)) { redirect($CFG->supportpage); } $PAGE->set_context(context_system::instance()); $PAGE->set_url('/user/contactsitesupport.php'); $PAGE->set_title(get_string('contactsitesupport', 'admin')); $PAGE->set_heading(get_string('contactsitesupport', 'admin')); $PAGE->set_pagelayout('standard'); $renderer = $PAGE->get_renderer('user'); $form = new \core_user\form\contactsitesupport_form(null, $user); if ($form->is_cancelled()) { redirect($CFG->wwwroot); } else if ($form->is_submitted() && $form->is_validated() && confirm_sesskey()) { $data = $form->get_data(); $from = $user ?? core_user::get_noreply_user(); $subject = get_string('supportemailsubject', 'admin', format_string($SITE->fullname)); $data->notloggedinuser = (!$user); $message = $renderer->render_from_template('user/contact_site_support_email_body', $data); if (!email_to_user(core_user::get_support_user(), $from, $subject, $message)) { $supportemail = $CFG->supportemail; $form->set_data($data); $templatectx = [ 'supportemail' => $user ? html_writer::link("mailto:{$supportemail}", $supportemail) : false, 'supportform' => $form->render(), ]; $output = $renderer->render_from_template('user/contact_site_support_not_available', $templatectx); } else { $level = \core\output\notification::NOTIFY_SUCCESS; redirect($CFG->wwwroot, get_string('supportmessagesent', 'user'), 3, $level); } } else { $output = $form->render(); } echo $OUTPUT->header(); echo $output; echo $OUTPUT->footer(); profilesys.php 0000644 00000004546 15151162244 0007466 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * System Public Profile. * * This script allows the site administrator to edit the default site * profile. * * @package core_user * @copyright 2010 Remote-Learner.net * @author Hubert Chathi <hubert@remote-learner.net> * @author Olav Jordan <olav.jordan@remote-learner.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../config.php'); require_once($CFG->dirroot . '/my/lib.php'); require_once($CFG->libdir.'/adminlib.php'); $resetall = optional_param('resetall', null, PARAM_BOOL); $header = "$SITE->fullname: ".get_string('publicprofile')." (".get_string('myprofile', 'admin').")"; $PAGE->set_blocks_editing_capability('moodle/my:configsyspages'); admin_externalpage_setup('profilepage', '', null, '', array('pagelayout' => 'mypublic')); if ($resetall && confirm_sesskey()) { my_reset_page_for_all_users(MY_PAGE_PUBLIC, 'user-profile'); redirect($PAGE->url, get_string('allprofileswerereset', 'my')); } // Override pagetype to show blocks properly. $PAGE->set_pagetype('user-profile'); $PAGE->set_title($header); $PAGE->set_heading($header); $PAGE->blocks->add_region('content'); // Get the Public Profile page info. Should always return something unless the database is broken. if (!$currentpage = my_get_page(null, MY_PAGE_PUBLIC)) { throw new \moodle_exception('publicprofilesetup'); } $PAGE->set_subpage($currentpage->id); $url = new moodle_url($PAGE->url, array('resetall' => 1)); $button = $OUTPUT->single_button($url, get_string('reseteveryonesprofile', 'my')); $PAGE->set_button($button . $PAGE->button); echo $OUTPUT->header(); echo $OUTPUT->custom_block_region('content'); echo $OUTPUT->footer(); classes/analytics/target/upcoming_activities_due.php 0000644 00000022772 15151162244 0017104 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Upcoming activities due target. * * @package core * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\analytics\target; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/lib/enrollib.php'); /** * Upcoming activities due target. * * @package core * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class upcoming_activities_due extends \core_analytics\local\target\binary { /** * Machine learning backends are not required to predict. * * @return bool */ public static function based_on_assumptions() { return true; } /** * Only update last analysis time when analysables are processed. * @return bool */ public function always_update_analysis_time(): bool { return false; } /** * Only upcoming stuff. * * @param \core_analytics\local\time_splitting\base $timesplitting * @return bool */ public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool { return ($timesplitting instanceof \core_analytics\local\time_splitting\after_now); } /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name() : \lang_string { return new \lang_string('target:upcomingactivitiesdue', 'user'); } /** * Overwritten to show a simpler language string. * * @param int $modelid * @param \context $context * @return string */ public function get_insight_subject(int $modelid, \context $context) { return get_string('youhaveupcomingactivitiesdue'); } /** * classes_description * * @return string[] */ protected static function classes_description() { return array( get_string('no'), get_string('yes'), ); } /** * Returns the predicted classes that will be ignored. * * @return array */ public function ignored_predicted_classes() { // No need to process users without upcoming activities due. return array(0); } /** * get_analyser_class * * @return string */ public function get_analyser_class() { return '\core\analytics\analyser\users'; } /** * All users are ok. * * @param \core_analytics\analysable $analysable * @param mixed $fortraining * @return true|string */ public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) { // The calendar API used by \core_course\analytics\indicator\activities_due is already checking // if the user has any courses. return true; } /** * Samples are users and all of them are ok. * * @param int $sampleid * @param \core_analytics\analysable $analysable * @param bool $fortraining * @return bool */ public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) { return true; } /** * Calculation based on activities due indicator. * * @param int $sampleid * @param \core_analytics\analysable $analysable * @param int $starttime * @param int $endtime * @return float */ protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) { $activitiesdueindicator = $this->retrieve('\core_course\analytics\indicator\activities_due', $sampleid); if ($activitiesdueindicator == \core_course\analytics\indicator\activities_due::get_max_value()) { return 1; } return 0; } /** * No need to link to the insights report in this case. * * @return bool */ public function link_insights_report(): bool { return false; } /** * Returns the body message for an insight of a single prediction. * * This default method is executed when the analysable used by the model generates one insight * for each analysable (one_sample_per_analysable === true) * * @param \context $context * @param \stdClass $user * @param \core_analytics\prediction $prediction * @param \core_analytics\action[] $actions Passed by reference to remove duplicate links to actions. * @return array Plain text msg, HTML message and the main URL for this * insight (you can return null if you are happy with the * default insight URL calculated in prediction_info()) */ public function get_insight_body_for_prediction(\context $context, \stdClass $user, \core_analytics\prediction $prediction, array &$actions) { global $OUTPUT; $fullmessageplaintext = get_string('youhaveupcomingactivitiesdueinfo', 'moodle', $user->firstname); $sampledata = $prediction->get_sample_data(); $activitiesdue = $sampledata['core_course\analytics\indicator\activities_due:extradata']; if (empty($activitiesdue)) { // We can throw an exception here because this is a target based on assumptions and we require the // activities_due indicator. throw new \coding_exception('The activities_due indicator must be part of the model indicators.'); } $activitiestext = []; foreach ($activitiesdue as $key => $activitydue) { // Human-readable version. $activitiesdue[$key]->formattedtime = userdate($activitydue->time); // We provide the URL to the activity through a script that records the user click. $activityurl = new \moodle_url($activitydue->url); $actionurl = \core_analytics\prediction_action::transform_to_forward_url($activityurl, 'viewupcoming', $prediction->get_prediction_data()->id); $activitiesdue[$key]->url = $actionurl->out(false); if (count($activitiesdue) === 1) { // We will use this activity as the main URL of this insight. $insighturl = $actionurl; } $activitiestext[] = $activitydue->name . ': ' . $activitiesdue[$key]->url; } foreach ($actions as $key => $action) { if ($action->get_action_name() === 'viewupcoming') { // Use it as the main URL of the insight if there are multiple activities due. if (empty($insighturl)) { $insighturl = $action->get_url(); } // Remove the 'viewupcoming' action from the list of actions for this prediction as the action has // been included in the link to the activity. unset($actions[$key]); break; } } $activitieshtml = $OUTPUT->render_from_template('core_user/upcoming_activities_due_insight_body', (object) [ 'activitiesdue' => array_values($activitiesdue), 'userfirstname' => $user->firstname ]); return [ FORMAT_PLAIN => $fullmessageplaintext . PHP_EOL . PHP_EOL . implode(PHP_EOL, $activitiestext) . PHP_EOL, FORMAT_HTML => $activitieshtml, 'url' => $insighturl, ]; } /** * Adds a view upcoming events action. * * @param \core_analytics\prediction $prediction * @param mixed $includedetailsaction * @param bool $isinsightuser * @return \core_analytics\prediction_action[] */ public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false, $isinsightuser = false) { global $CFG, $USER; $parentactions = parent::prediction_actions($prediction, $includedetailsaction, $isinsightuser); if (!$isinsightuser && $USER->id != $prediction->get_prediction_data()->sampleid) { return $parentactions; } // We force a lookahead of 30 days so we are sure that the upcoming activities due are shown. $url = new \moodle_url('/calendar/view.php', ['view' => 'upcoming', 'lookahead' => '30']); $pix = new \pix_icon('i/calendar', get_string('viewupcomingactivitiesdue', 'calendar')); $action = new \core_analytics\prediction_action('viewupcoming', $prediction, $url, $pix, get_string('viewupcomingactivitiesdue', 'calendar')); return array_merge([$action], $parentactions); } } classes/analytics/indicator/user_profile_set.php 0000644 00000005546 15151162244 0016241 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * User profile set indicator. * * @package core_user * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * User profile set indicator. * * @package core_user * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_profile_set extends \core_analytics\local\indicator\linear { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name() : \lang_string { return new \lang_string('indicator:completeduserprofile'); } /** * required_sample_data * * @return string[] */ public static function required_sample_data() { return array('user'); } /** * calculate_sample * * @param int $sampleid * @param string $sampleorigin * @param int $starttime * @param int $endtime * @return float */ protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) { global $CFG; $user = $this->retrieve('user', $sampleid); // Nothing set results in -1. $calculatedvalue = self::MIN_VALUE; if (\core_user::awaiting_action($user)) { return self::MIN_VALUE; } if (!$user->confirmed) { return self::MIN_VALUE; } if ($user->description != '') { $calculatedvalue += 1; } if ($user->picture != '') { $calculatedvalue += 1; } // 0.2 for any of the following fields being set (some of them may even be compulsory or have a default). $fields = array('institution', 'department', 'address', 'city', 'country'); foreach ($fields as $fieldname) { if ($user->{$fieldname} != '') { $calculatedvalue += 0.2; } } return $this->limit_value($calculatedvalue); } } classes/analytics/indicator/user_track_forums.php 0000644 00000004151 15151162244 0016414 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * User tracks forums indicator. * * @package core_user * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * User tracks forums indicator. * * @package core_user * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_track_forums extends \core_analytics\local\indicator\binary { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name() : \lang_string { return new \lang_string('indicator:userforumstracking'); } /** * required_sample_data * * @return string[] */ public static function required_sample_data() { return array('user'); } /** * calculate_sample * * @param int $sampleid * @param string $samplesorigin * @param int $starttime * @param int $endtime * @return float */ protected function calculate_sample($sampleid, $samplesorigin, $starttime = false, $endtime = false) { $user = $this->retrieve('user', $sampleid); return ($user->trackforums) ? self::get_max_value() : self::get_min_value(); } } classes/output/user_roles_editable.php 0000644 00000021756 15151162244 0014301 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class core_user\output\user_roles_editable * * @package core_user * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\output; use context_course; use core_user; use core_external; use coding_exception; defined('MOODLE_INTERNAL') || die(); /** * Class to display list of user roles. * * @package core_user * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_roles_editable extends \core\output\inplace_editable { /** @var $context */ private $context = null; /** @var \stdClass[] $courseroles */ private $courseroles; /** @var \stdClass[] $profileroles */ private $profileroles; /** @var \stdClass[] $viewableroles */ private $viewableroles; /** @var \stdClass[] $assignableroles */ private $assignableroles; /** * Constructor. * * @param \stdClass $course The current course * @param \context $context The course context * @param \stdClass $user The current user * @param \stdClass[] $courseroles The list of course roles. * @param \stdClass[] $assignableroles The list of assignable roles in this course. * @param \stdClass[] $profileroles The list of roles that should be visible in a users profile. * @param \stdClass[] $userroles The list of user roles. */ public function __construct($course, $context, $user, $courseroles, $assignableroles, $profileroles, $userroles, $viewableroles = null) { if ($viewableroles === null) { debugging('Constructor for user_roles_editable now needs the result of get_viewable_roles passed as viewableroles'); } // Check capabilities to get editable value. $editable = has_capability('moodle/role:assign', $context); // Invent an itemid. $itemid = $course->id . ':' . $user->id; $getrole = function($role) { return $role->roleid; }; $ids = array_values(array_unique(array_map($getrole, $userroles))); $value = json_encode($ids); // Remember these for the display value. $this->courseroles = $courseroles; $this->profileroles = $profileroles; $this->viewableroles = array_keys($viewableroles); $this->assignableroles = array_keys($assignableroles); $this->context = $context; parent::__construct('core_user', 'user_roles', $itemid, $editable, $value, $value); // Removed the roles that were assigned to the user at a different context. $options = $assignableroles; foreach ($userroles as $role) { if (isset($assignableroles[$role->roleid])) { if ($role->contextid != $context->id) { unset($options[$role->roleid]); } } } $this->edithint = get_string('xroleassignments', 'role', fullname($user)); $this->editlabel = get_string('xroleassignments', 'role', fullname($user)); $attributes = ['multiple' => true]; $this->set_type_autocomplete($options, $attributes); } /** * Export this data so it can be used as the context for a mustache template. * * @param \renderer_base $output * @return array */ public function export_for_template(\renderer_base $output) { $listofroles = []; $roleids = json_decode($this->value); $viewableroleids = array_intersect($roleids, array_merge($this->viewableroles, $this->assignableroles)); foreach ($viewableroleids as $id) { // If this is a student, we only show a subset of the roles. if ($this->editable || array_key_exists($id, $this->profileroles)) { $listofroles[] = format_string($this->courseroles[$id]->localname, true, ['context' => $this->context]); } } if (!empty($listofroles)) { $this->displayvalue = implode(', ', $listofroles); } else if (!empty($roleids) && empty($viewableroleids)) { $this->displayvalue = get_string('novisibleroles', 'role'); } else { $this->displayvalue = get_string('noroles', 'role'); } return parent::export_for_template($output); } /** * Updates the value in database and returns itself, called from inplace_editable callback * * @param int $itemid * @param mixed $newvalue * @return \self */ public static function update($itemid, $newvalue) { global $DB, $CFG; require_once($CFG->libdir . '/external/externallib.php'); // Check caps. // Do the thing. // Return one of me. // Validate the inputs. list($courseid, $userid) = explode(':', $itemid, 2); $courseid = clean_param($courseid, PARAM_INT); $userid = clean_param($userid, PARAM_INT); $roleids = json_decode($newvalue); foreach ($roleids as $index => $roleid) { $roleids[$index] = clean_param($roleid, PARAM_INT); } // Check user is enrolled in the course. $context = context_course::instance($courseid); core_external::validate_context($context); // Check permissions. require_capability('moodle/role:assign', $context); if (!is_enrolled($context, $userid)) { throw new coding_exception('User does not belong to the course'); } // Check that all the groups belong to the course. $allroles = role_fix_names(get_all_roles($context), $context, ROLENAME_BOTH); $assignableroles = get_assignable_roles($context, ROLENAME_BOTH, false); $viewableroles = get_viewable_roles($context); $userrolesbyid = get_user_roles($context, $userid, true, 'c.contextlevel DESC, r.sortorder ASC'); $profileroles = get_profile_roles($context); // Set an array where the index is the roleid. $userroles = array(); foreach ($userrolesbyid as $id => $role) { $userroles[$role->roleid] = $role; } $rolestoprocess = []; foreach ($roleids as $roleid) { if (!isset($assignableroles[$roleid])) { throw new coding_exception('Role cannot be assigned in this course.'); } $rolestoprocess[$roleid] = $roleid; } // Process adds. foreach ($rolestoprocess as $roleid) { if (!isset($userroles[$roleid])) { // Add them. $id = role_assign($roleid, $userid, $context); // Keep this variable in sync. $role = new \stdClass(); $role->id = $id; $role->roleid = $roleid; $role->contextid = $context->id; $userroles[$role->roleid] = $role; } } // Process removals. foreach ($assignableroles as $roleid => $rolename) { if (isset($userroles[$roleid]) && !isset($rolestoprocess[$roleid])) { // Do not remove the role if we are not in the same context. if ($userroles[$roleid]->contextid != $context->id) { continue; } $ras = $DB->get_records('role_assignments', ['contextid' => $context->id, 'userid' => $userid, 'roleid' => $roleid]); $allremoved = true; foreach ($ras as $ra) { if ($ra->component) { if (strpos($ra->component, 'enrol_') !== 0) { continue; } if (!$plugin = enrol_get_plugin(substr($ra->component, 6))) { continue; } if ($plugin->roles_protected()) { $allremoved = false; continue; } } role_unassign($ra->roleid, $ra->userid, $ra->contextid, $ra->component, $ra->itemid); } if ($allremoved) { unset($userroles[$roleid]); } } } $course = get_course($courseid); $user = core_user::get_user($userid); return new self($course, $context, $user, $allroles, $assignableroles, $profileroles, $userroles, $viewableroles); } } classes/output/myprofile/renderer.php 0000644 00000010441 15151162244 0014067 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * myprofile renderer. * * @package core_user * @copyright 2015 onwards Ankit Agarwal <ankit.agrr@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\output\myprofile; defined('MOODLE_INTERNAL') || die; /** * Report log renderer's for printing reports. * * @since Moodle 2.9 * @package core_user * @copyright 2015 onwards Ankit Agarwal <ankit.agrr@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends \plugin_renderer_base { /** * Render the whole tree. * * @param tree $tree * * @return string */ public function render_tree(tree $tree) { $return = \html_writer::start_tag('div', array('class' => 'profile_tree')); $categories = $tree->categories; foreach ($categories as $category) { $return .= $this->render($category); } $return .= \html_writer::end_tag('div'); return $return; } /** * Render a category. * * @param category $category * * @return string */ public function render_category(category $category) { $classes = $category->classes; if (empty($classes)) { $return = \html_writer::start_tag('section', array('class' => 'node_category card d-inline-block w-100 mb-3')); $return .= \html_writer::start_tag('div', array('class' => 'card-body')); } else { $return = \html_writer::start_tag('section', array('class' => 'node_category card d-inline-block w-100 mb-3' . $classes)); $return .= \html_writer::start_tag('div', array('class' => 'card-body')); } $return .= \html_writer::tag('h3', $category->title, array('class' => 'lead')); $nodes = $category->nodes; if (empty($nodes)) { // No nodes, nothing to render. return ''; } $return .= \html_writer::start_tag('ul'); foreach ($nodes as $node) { $return .= $this->render($node); } $return .= \html_writer::end_tag('ul'); $return .= \html_writer::end_tag('div'); $return .= \html_writer::end_tag('section'); return $return; } /** * Render a node. * * @param node $node * * @return string */ public function render_node(node $node) { $return = ''; if (is_object($node->url)) { $header = \html_writer::link($node->url, $node->title); } else { $header = $node->title; } $icon = $node->icon; if (!empty($icon)) { $header .= $this->render($icon); } $content = $node->content; $classes = $node->classes; if (!empty($content)) { if ($header) { // There is some content to display below this make this a header. $return = \html_writer::tag('dt', $header); $return .= \html_writer::tag('dd', $content); $return = \html_writer::tag('dl', $return); } else { $return = \html_writer::span($content); } if ($classes) { $return = \html_writer::tag('li', $return, array('class' => 'contentnode ' . $classes)); } else { $return = \html_writer::tag('li', $return, array('class' => 'contentnode')); } } else { $return = \html_writer::span($header); $return = \html_writer::tag('li', $return, array('class' => $classes)); } return $return; } } classes/output/myprofile/node.php 0000644 00000007005 15151162244 0013210 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines a node in my profile page navigation. * * @package core_user * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\output\myprofile; defined('MOODLE_INTERNAL') || die(); /** * Defines a node in my profile page navigation. * * @since Moodle 2.9 * @package core_user * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class node implements \renderable { /** * @var string Name of parent category. */ private $parentcat; /** * @var string Name of this node. */ private $name; /** * @var string Name of the node after which this node should appear. */ private $after; /** * @var string Title of this node. */ private $title; /** * @var string|\moodle_url Url that this node should link to. */ private $url; /** * @var string Content to display under this node. */ private $content; /** * @var string|\pix_icon Icon for this node. */ private $icon; /** * @var string HTML class attribute for this node. Classes should be separated by a space, e.g. 'class1 class2' */ private $classes; /** * @var array list of properties accessible via __get. */ private $properties = array('parentcat', 'after', 'name', 'title', 'url', 'content', 'icon', 'classes'); /** * Constructor for the node. * * @param string $parentcat Name of parent category. * @param string $name Name of this node. * @param string $title Title of this node. * @param null|string $after Name of the node after which this node should appear. * @param null|string|\moodle_url $url Url that this node should link to. * @param null|string $content Content to display under this node. * @param null|string|\pix_icon $icon Icon for this node. * @param null|string $classes a list of css classes. */ public function __construct($parentcat, $name, $title, $after = null, $url = null, $content = null, $icon = null, $classes = null) { $this->parentcat = $parentcat; $this->after = $after; $this->name = $name; $this->title = $title; $this->url = is_null($url) ? null : new \moodle_url($url); $this->content = $content; $this->icon = $icon; $this->classes = $classes; } /** * Magic get method. * * @param string $prop property to get. * * @return mixed * @throws \coding_exception */ public function __get($prop) { if (in_array($prop, $this->properties)) { return $this->$prop; } throw new \coding_exception('Property "' . $prop . '" doesn\'t exist'); } } classes/output/myprofile/category.php 0000644 00000015074 15151162244 0014105 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines a category in my profile page navigation. * * @package core_user * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\output\myprofile; defined('MOODLE_INTERNAL') || die(); /** * Defines a category in my profile page navigation. * * @since Moodle 2.9 * @package core_user * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category implements \renderable { /** * @var string Name of the category after which this category should appear. */ private $after; /** * @var string Name of the category. */ private $name; /** * @var string Title of the category. */ private $title; /** * @var node[] Array of nodes associated with this category. */ private $nodes = array(); /** * @var string HTML class attribute for this category. Classes should be separated by a space, e.g. 'class1 class2' */ private $classes; /** * @var array list of properties publicly accessible via __get. */ private $properties = array('after', 'name', 'title', 'nodes', 'classes'); /** * Constructor for category class. * * @param string $name Category name. * @param string $title category title. * @param null|string $after Name of category after which this category should appear. * @param null|string $classes a list of css classes. */ public function __construct($name, $title, $after = null, $classes = null) { $this->after = $after; $this->name = $name; $this->title = $title; $this->classes = $classes; } /** * Add a node to this category. * * @param node $node node object. * @see \core_user\output\myprofile\tree::add_node() * * @throws \coding_exception */ public function add_node(node $node) { $name = $node->name; if (isset($this->nodes[$name])) { throw new \coding_exception("Node with name $name already exists"); } if ($node->parentcat !== $this->name) { throw new \coding_exception("Node parent must match with the category it is added to"); } $this->nodes[$node->name] = $node; } /** * Sort nodes of the category in the order in which they should be displayed. * * @see \core_user\output\myprofile\tree::sort_categories() * @throws \coding_exception */ public function sort_nodes() { $tempnodes = array(); $this->validate_after_order(); // First content noes. foreach ($this->nodes as $node) { $after = $node->after; $content = $node->content; if (($after == null && !empty($content)) || $node->name === 'editprofile') { // Can go anywhere in the cat. Also show content nodes first. $tempnodes = array_merge($tempnodes, array($node->name => $node), $this->find_nodes_after($node)); } } // Now nodes with no content. foreach ($this->nodes as $node) { $after = $node->after; $content = $node->content; if ($after == null && empty($content)) { // Can go anywhere in the cat. Also show content nodes first. $tempnodes = array_merge($tempnodes, array($node->name => $node), $this->find_nodes_after($node)); } } if (count($tempnodes) !== count($this->nodes)) { // Orphan nodes found. throw new \coding_exception('Some of the nodes specified contains invalid \'after\' property'); } $this->nodes = $tempnodes; } /** * Verifies that node with content can come after node with content only . Also verifies the same thing for nodes without * content. * @throws \coding_exception */ protected function validate_after_order() { $nodearray = $this->nodes; foreach ($this->nodes as $node) { $after = $node->after; if (!empty($after)) { if (empty($nodearray[$after])) { throw new \coding_exception('node {$node->name} specified contains invalid \'after\' property'); } else { // Valid node found. $afternode = $nodearray[$after]; $beforecontent = $node->content; $aftercontent = $afternode->content; if ((empty($beforecontent) && !empty($aftercontent)) || (!empty($beforecontent) && empty($aftercontent))) { // Only node with content are allowed after content nodes. Same goes for no content nodes. throw new \coding_exception('node {$node->name} specified contains invalid \'after\' property'); } } } } } /** * Given a node object find all node objects that should appear after it. * * @param node $node node object * * @return array */ protected function find_nodes_after($node) { $return = array(); $nodearray = $this->nodes; foreach ($nodearray as $nodeelement) { if ($nodeelement->after === $node->name) { // Find all nodes that comes after this node as well. $return = array_merge($return, array($nodeelement->name => $nodeelement), $this->find_nodes_after($nodeelement)); } } return $return; } /** * Magic get method. * * @param string $prop property to get. * * @return mixed * @throws \coding_exception */ public function __get($prop) { if (in_array($prop, $this->properties)) { return $this->$prop; } throw new \coding_exception('Property "' . $prop . '" doesn\'t exist'); } } classes/output/myprofile/tree.php 0000644 00000011674 15151162244 0013231 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines profile page navigation tree. * * @package core_user * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\output\myprofile; defined('MOODLE_INTERNAL') || die(); /** * Defines my profile page navigation tree. * * @since Moodle 2.9 * @package core_user * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tree implements \renderable { /** * @var category[] Array of categories in the tree. */ private $categories = array(); /** * @var node[] Array of nodes in the tree that were directly added to the tree. */ private $nodes = array(); /** * @var array List of properties accessible via __get. */ private $properties = array('categories', 'nodes'); /** * Add a node to the tree. * * @param node $node node object. * * @throws \coding_exception */ public function add_node(node $node) { $name = $node->name; if (isset($this->nodes[$name])) { throw new \coding_exception("Node name $name already used"); } $this->nodes[$node->name] = $node; } /** * Add a category to the tree. * * @param category $cat category object. * * @throws \coding_exception */ public function add_category(category $cat) { $name = $cat->name; if (isset($this->categories[$name])) { throw new \coding_exception("Category name $name already used"); } $this->categories[$cat->name] = $cat; } /** * Sort categories and nodes. Builds the tree structure that would be displayed to the user. * * @throws \coding_exception */ public function sort_categories() { $this->attach_nodes_to_categories(); $tempcategories = array(); foreach ($this->categories as $category) { $after = $category->after; if ($after == null) { // Can go anywhere in the tree. $category->sort_nodes(); $tempcategories = array_merge($tempcategories, array($category->name => $category), $this->find_categories_after($category)); } } if (count($tempcategories) !== count($this->categories)) { // Orphan categories found. throw new \coding_exception('Some of the categories specified contains invalid \'after\' property'); } $this->categories = $tempcategories; } /** * Attach various nodes to their respective categories. * * @throws \coding_exception */ protected function attach_nodes_to_categories() { foreach ($this->nodes as $node) { $parentcat = $node->parentcat; if (!isset($this->categories[$parentcat])) { throw new \coding_exception("Category $parentcat doesn't exist"); } else { $this->categories[$parentcat]->add_node($node); } } } /** * Find all category nodes that should be displayed after a given a category node. * * @param category $category category object * * @return category[] array of category objects * @throws \coding_exception */ protected function find_categories_after($category) { $return = array(); $categoryarray = $this->categories; foreach ($categoryarray as $categoryelement) { if ($categoryelement->after == $category->name) { // Find all categories that comes after this category as well. $categoryelement->sort_nodes(); $return = array_merge($return, array($categoryelement->name => $categoryelement), $this->find_categories_after($categoryelement)); } } return $return; } /** * Magic get method. * * @param string $prop property to get. * * @return mixed * @throws \coding_exception */ public function __get($prop) { if (in_array($prop, $this->properties)) { return $this->$prop; } throw new \coding_exception('Property "' . $prop . '" doesn\'t exist'); } } classes/output/myprofile/manager.php 0000644 00000005427 15151162244 0013703 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines Manager class for my profile navigation tree. * * @package core_user * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\output\myprofile; defined('MOODLE_INTERNAL') || die(); /** * Defines MAnager class for myprofile navigation tree. * * @since Moodle 2.9 * @package core_user * @copyright 2015 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** * Parse all callbacks and builds the tree. * * @param integer $user ID of the user for which the profile is displayed. * @param bool $iscurrentuser true if the profile being viewed is of current user, else false. * @param \stdClass $course Course object * * @return tree Fully build tree to be rendered on my profile page. */ public static function build_tree($user, $iscurrentuser, $course = null) { global $CFG; $tree = new tree(); // Add core nodes. require_once($CFG->libdir . "/myprofilelib.php"); core_myprofile_navigation($tree, $user, $iscurrentuser, $course); // Core components. $components = \core_component::get_core_subsystems(); foreach ($components as $component => $directory) { if (empty($directory)) { continue; } $file = $directory . "/lib.php"; if (is_readable($file)) { require_once($file); $function = "core_" . $component . "_myprofile_navigation"; if (function_exists($function)) { $function($tree, $user, $iscurrentuser, $course); } } } // Plugins. $pluginswithfunction = get_plugins_with_function('myprofile_navigation', 'lib.php'); foreach ($pluginswithfunction as $plugins) { foreach ($plugins as $function) { $function($tree, $user, $iscurrentuser, $course); } } $tree->sort_categories(); return $tree; } } classes/output/status_field.php 0000644 00000014450 15151162244 0012745 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class containing the data necessary for rendering the status field in the course participants page. * * @package core_user * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use user_enrolment_action; /** * Class containing the data for the status field. * * @package core_user * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class status_field implements renderable, templatable { /** Active user enrolment status constant. */ const STATUS_ACTIVE = 0; /** Suspended user enrolment status constant. */ const STATUS_SUSPENDED = 1; /** Not current user enrolment status constant. */ const STATUS_NOT_CURRENT = 2; /** @var string $enrolinstancename The enrolment instance name. */ protected $enrolinstancename; /** @var string $coursename The course's full name. */ protected $coursename; /** @var string $fullname The user's full name. */ protected $fullname; /** @var string $status The user enrolment status. */ protected $status; /** @var int $timestart The timestamp when the user's enrolment starts. */ protected $timestart; /** @var int $timeend The timestamp when the user's enrolment ends. */ protected $timeend; /** @var int $timeenrolled The timestamp when the user was enrolled. */ protected $timeenrolled; /** @var user_enrolment_action[] $enrolactions Array of enrol action objects for the given enrolment method. */ protected $enrolactions; /** @var bool $statusactive Indicates whether a user enrolment status should be rendered as active. */ protected $statusactive = false; /** @var bool $statusactive Indicates whether a user enrolment status should be rendered as suspended. */ protected $statussuspended = false; /** @var bool $statusactive Indicates whether a user enrolment status should be rendered as not current. */ protected $statusnotcurrent = false; /** * status_field constructor. * * @param string $enrolinstancename The enrolment instance name. * @param string $coursename The course's full name. * @param string $fullname The user's full name. * @param string $status The user enrolment status. * @param int|null $timestart The timestamp when the user's enrolment starts. * @param int|null $timeend The timestamp when the user's enrolment ends. * @param user_enrolment_action[] $enrolactions Array of enrol action objects for the given enrolment method. * @param int|null $timeenrolled The timestamp when the user was enrolled. */ public function __construct($enrolinstancename, $coursename, $fullname, $status, $timestart = null, $timeend = null, $enrolactions = [], $timeenrolled = null) { $this->enrolinstancename = $enrolinstancename; $this->coursename = $coursename; $this->fullname = $fullname; $this->status = $status; $this->timestart = $timestart; $this->timeend = $timeend; $this->enrolactions = $enrolactions; $this->timeenrolled = $timeenrolled; } /** * Function to export the renderer data in a format that is suitable for a * mustache template. This means: * 1. No complex types - only stdClass, array, int, string, float, bool * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks). * * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. * @return stdClass|array */ public function export_for_template(renderer_base $output) { $data = new stdClass(); $data->enrolinstancename = $this->enrolinstancename; $data->coursename = $this->coursename; $data->fullname = $this->fullname; $data->status = $this->status; $data->active = $this->statusactive; $data->suspended = $this->statussuspended; $data->notcurrent = $this->statusnotcurrent; if ($this->timestart) { $data->timestart = userdate($this->timestart); } if ($this->timeend) { $data->timeend = userdate($this->timeend); } if ($this->timeenrolled) { $data->timeenrolled = userdate($this->timeenrolled); } $data->enrolactions = []; foreach ($this->enrolactions as $enrolaction) { $action = new stdClass(); $action->url = $enrolaction->get_url()->out(false); $action->icon = $output->render($enrolaction->get_icon()); $action->attributes = []; foreach ($enrolaction->get_attributes() as $name => $value) { $attribute = (object) [ 'name' => $name, 'value' => $value ]; $action->attributes[] = $attribute; } $data->enrolactions[] = $action; } return $data; } /** * Status setter. * * @param int $status The user enrolment status representing one of this class' STATUS_* constants. * @return status_field This class' instance. Useful for chaining. */ public function set_status($status = self::STATUS_ACTIVE) { $this->statusactive = $status == static::STATUS_ACTIVE; $this->statussuspended = $status == static::STATUS_SUSPENDED; $this->statusnotcurrent = $status == static::STATUS_NOT_CURRENT; return $this; } } classes/output/participants_filter.php 0000644 00000026260 15151162244 0014327 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for rendering user filters on the course participants page. * * @package core_user * @copyright 2020 Michael Hawkins <michaelh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\output; use core_user\fields; use renderer_base; use stdClass; /** * Class for rendering user filters on the course participants page. * * @copyright 2020 Michael Hawkins <michaelh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class participants_filter extends \core\output\datafilter { /** * Get data for all filter types. * * @return array */ protected function get_filtertypes(): array { $filtertypes = []; $filtertypes[] = $this->get_keyword_filter(); if ($filtertype = $this->get_enrolmentstatus_filter()) { $filtertypes[] = $filtertype; } if ($filtertype = $this->get_roles_filter()) { $filtertypes[] = $filtertype; } if ($filtertype = $this->get_enrolments_filter()) { $filtertypes[] = $filtertype; } if ($filtertype = $this->get_groups_filter()) { $filtertypes[] = $filtertype; } if ($filtertype = $this->get_accesssince_filter()) { $filtertypes[] = $filtertype; } if ($filtertype = $this->get_country_filter()) { $filtertypes[] = $filtertype; } return $filtertypes; } /** * Get data for the enrolment status filter. * * @return stdClass|null */ protected function get_enrolmentstatus_filter(): ?stdClass { if (!has_capability('moodle/course:enrolreview', $this->context)) { return null; } return $this->get_filter_object( 'status', get_string('participationstatus', 'core_enrol'), false, true, null, [ (object) [ 'value' => ENROL_USER_ACTIVE, 'title' => get_string('active'), ], (object) [ 'value' => ENROL_USER_SUSPENDED, 'title' => get_string('inactive'), ], ] ); } /** * Get data for the roles filter. * * @return stdClass|null */ protected function get_roles_filter(): ?stdClass { $roles = []; $roles += [-1 => get_string('noroles', 'role')]; $roles += get_viewable_roles($this->context, null, ROLENAME_BOTH); if (has_capability('moodle/role:assign', $this->context)) { $roles += get_assignable_roles($this->context, ROLENAME_BOTH); } return $this->get_filter_object( 'roles', get_string('roles', 'core_role'), false, true, null, array_map(function($id, $title) { return (object) [ 'value' => $id, 'title' => $title, ]; }, array_keys($roles), array_values($roles)) ); } /** * Get data for the roles filter. * * @return stdClass|null */ protected function get_enrolments_filter(): ?stdClass { if (!has_capability('moodle/course:enrolreview', $this->context)) { return null; } if ($this->course->id == SITEID) { // No enrolment methods for the site. return null; } $instances = enrol_get_instances($this->course->id, true); $plugins = enrol_get_plugins(false); return $this->get_filter_object( 'enrolments', get_string('enrolmentinstances', 'core_enrol'), false, true, null, array_filter(array_map(function($instance) use ($plugins): ?stdClass { if (!array_key_exists($instance->enrol, $plugins)) { return null; } return (object) [ 'value' => $instance->id, 'title' => $plugins[$instance->enrol]->get_instance_name($instance), ]; }, array_values($instances))) ); } /** * Get data for the groups filter. * * @return stdClass|null */ protected function get_groups_filter(): ?stdClass { global $USER; // Filter options for groups, if available. $seeallgroups = has_capability('moodle/site:accessallgroups', $this->context); $seeallgroups = $seeallgroups || ($this->course->groupmode != SEPARATEGROUPS); if ($seeallgroups) { $groups = []; $groups += [USERSWITHOUTGROUP => (object) [ 'id' => USERSWITHOUTGROUP, 'name' => get_string('nogroup', 'group'), ]]; $groups += groups_get_all_groups($this->course->id); } else { // Otherwise, just list the groups the user belongs to. $groups = groups_get_all_groups($this->course->id, $USER->id); } // Return no data if no groups found (which includes if the only value is 'No group'). if (empty($groups) || (count($groups) === 1 && array_key_exists(-1, $groups))) { return null; } return $this->get_filter_object( 'groups', get_string('groups', 'core_group'), false, true, null, array_map(function($group) { return (object) [ 'value' => $group->id, 'title' => format_string($group->name, true, ['context' => $this->context]), ]; }, array_values($groups)) ); } /** * Get data for the accesssince filter. * * @return stdClass|null */ protected function get_accesssince_filter(): ?stdClass { global $CFG, $DB; $hiddenfields = []; if (!has_capability('moodle/course:viewhiddenuserfields', $this->context)) { $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); } if (array_key_exists('lastaccess', $hiddenfields)) { return null; } // Get minimum lastaccess for this course and display a dropbox to filter by lastaccess going back this far. // We need to make it diferently for normal courses and site course. if (!$this->course->id == SITEID) { // Regular course. $params = [ 'courseid' => $this->course->id, 'timeaccess' => 0, ]; $select = 'courseid = :courseid AND timeaccess != :timeaccess'; $minlastaccess = $DB->get_field_select('user_lastaccess', 'MIN(timeaccess)', $select, $params); $lastaccess0exists = $DB->record_exists('user_lastaccess', $params); } else { // Front page. $params = ['lastaccess' => 0]; $select = 'lastaccess != :lastaccess'; $minlastaccess = $DB->get_field_select('user', 'MIN(lastaccess)', $select, $params); $lastaccess0exists = $DB->record_exists('user', $params); } $now = usergetmidnight(time()); $timeoptions = []; $criteria = get_string('usersnoaccesssince'); $getoptions = function(int $count, string $singletype, string $type) use ($now, $minlastaccess): array { $values = []; for ($i = 1; $i <= $count; $i++) { $timestamp = strtotime("-{$i} {$type}", $now); if ($timestamp < $minlastaccess) { break; } if ($i === 1) { $title = get_string("num{$singletype}", 'moodle', $i); } else { $title = get_string("num{$type}", 'moodle', $i); } $values[] = [ 'value' => $timestamp, 'title' => $title, ]; } return $values; }; $values = array_merge( $getoptions(6, 'day', 'days'), $getoptions(10, 'week', 'weeks'), $getoptions(11, 'month', 'months'), $getoptions(1, 'year', 'years') ); if ($lastaccess0exists) { $values[] = [ 'value' => time(), 'title' => get_string('never', 'moodle'), ]; } if (count($values) <= 1) { // Nothing to show. return null; } return $this->get_filter_object( 'accesssince', get_string('usersnoaccesssince'), false, false, null, $values ); } /** * Get data for the country filter * * @return stdClass|null */ protected function get_country_filter(): ?stdClass { $extrauserfields = fields::get_identity_fields($this->context, false); if (array_search('country', $extrauserfields) === false) { return null; } $countries = get_string_manager()->get_list_of_countries(true); return $this->get_filter_object( 'country', get_string('country'), false, true, 'core/datafilter/filtertypes/country', array_map(function(string $code, string $name): stdClass { return (object) [ 'value' => $code, 'title' => $name, ]; }, array_keys($countries), array_values($countries)) ); } /** * Get data for the keywords filter. * * @return stdClass|null */ protected function get_keyword_filter(): ?stdClass { return $this->get_filter_object( 'keywords', get_string('filterbykeyword', 'core_user'), true, true, 'core/datafilter/filtertypes/keyword', [], true ); } /** * Export the renderer data in a mustache template friendly format. * * @param renderer_base $output Unused. * @return stdClass Data in a format compatible with a mustache template. */ public function export_for_template(renderer_base $output): stdClass { return (object) [ 'tableregionid' => $this->tableregionid, 'courseid' => $this->context->instanceid, 'filtertypes' => $this->get_filtertypes(), 'rownumber' => 1, ]; return $data; } } classes/privacy/provider.php 0000644 00000060700 15151162244 0012225 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy class for requesting user data. * * @package core_user * @copyright 2018 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\transform; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\writer; use core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; /** * Privacy class for requesting user data. * * @package core_comment * @copyright 2018 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\subsystem\provider, \core_privacy\local\request\user_preference_provider { /** * Returns information about the user data stored in this component. * * @param collection $collection A list of information about this component * @return collection The collection object filled out with information about this component. */ public static function get_metadata(collection $collection) : collection { $userfields = [ 'id' => 'privacy:metadata:id', 'auth' => 'privacy:metadata:auth', 'confirmed' => 'privacy:metadata:confirmed', 'policyagreed' => 'privacy:metadata:policyagreed', 'deleted' => 'privacy:metadata:deleted', 'suspended' => 'privacy:metadata:suspended', 'mnethostid' => 'privacy:metadata:mnethostid', 'username' => 'privacy:metadata:username', 'password' => 'privacy:metadata:password', 'idnumber' => 'privacy:metadata:idnumber', 'firstname' => 'privacy:metadata:firstname', 'lastname' => 'privacy:metadata:lastname', 'email' => 'privacy:metadata:email', 'emailstop' => 'privacy:metadata:emailstop', 'phone1' => 'privacy:metadata:phone', 'phone2' => 'privacy:metadata:phone', 'institution' => 'privacy:metadata:institution', 'department' => 'privacy:metadata:department', 'address' => 'privacy:metadata:address', 'city' => 'privacy:metadata:city', 'country' => 'privacy:metadata:country', 'lang' => 'privacy:metadata:lang', 'calendartype' => 'privacy:metadata:calendartype', 'theme' => 'privacy:metadata:theme', 'timezone' => 'privacy:metadata:timezone', 'firstaccess' => 'privacy:metadata:firstaccess', 'lastaccess' => 'privacy:metadata:lastaccess', 'lastlogin' => 'privacy:metadata:lastlogin', 'currentlogin' => 'privacy:metadata:currentlogin', 'lastip' => 'privacy:metadata:lastip', 'secret' => 'privacy:metadata:secret', 'picture' => 'privacy:metadata:picture', 'description' => 'privacy:metadata:description', 'maildigest' => 'privacy:metadata:maildigest', 'maildisplay' => 'privacy:metadata:maildisplay', 'autosubscribe' => 'privacy:metadata:autosubscribe', 'trackforums' => 'privacy:metadata:trackforums', 'timecreated' => 'privacy:metadata:timecreated', 'timemodified' => 'privacy:metadata:timemodified', 'trustbitmask' => 'privacy:metadata:trustbitmask', 'imagealt' => 'privacy:metadata:imagealt', 'lastnamephonetic' => 'privacy:metadata:lastnamephonetic', 'firstnamephonetic' => 'privacy:metadata:firstnamephonetic', 'middlename' => 'privacy:metadata:middlename', 'alternatename' => 'privacy:metadata:alternatename', 'moodlenetprofile' => 'privacy:metadata:moodlenetprofile' ]; $passwordhistory = [ 'userid' => 'privacy:metadata:userid', 'hash' => 'privacy:metadata:hash', 'timecreated' => 'privacy:metadata:timecreated' ]; $lastaccess = [ 'userid' => 'privacy:metadata:userid', 'courseid' => 'privacy:metadata:courseid', 'timeaccess' => 'privacy:metadata:timeaccess' ]; $userpasswordresets = [ 'userid' => 'privacy:metadata:userid', 'timerequested' => 'privacy:metadata:timerequested', 'timererequested' => 'privacy:metadata:timererequested', 'token' => 'privacy:metadata:token' ]; $userdevices = [ 'userid' => 'privacy:metadata:userid', 'appid' => 'privacy:metadata:appid', 'name' => 'privacy:metadata:devicename', 'model' => 'privacy:metadata:model', 'platform' => 'privacy:metadata:platform', 'version' => 'privacy:metadata:version', 'pushid' => 'privacy:metadata:pushid', 'uuid' => 'privacy:metadata:uuid', 'timecreated' => 'privacy:metadata:timecreated', 'timemodified' => 'privacy:metadata:timemodified' ]; $usersessions = [ 'state' => 'privacy:metadata:state', 'sid' => 'privacy:metadata:sid', 'userid' => 'privacy:metadata:userid', 'sessdata' => 'privacy:metadata:sessdata', 'timecreated' => 'privacy:metadata:timecreated', 'timemodified' => 'privacy:metadata:timemodified', 'firstip' => 'privacy:metadata:firstip', 'lastip' => 'privacy:metadata:lastip' ]; $courserequest = [ 'fullname' => 'privacy:metadata:fullname', 'shortname' => 'privacy:metadata:shortname', 'summary' => 'privacy:metadata:summary', 'category' => 'privacy:metadata:category', 'reason' => 'privacy:metadata:reason', 'requester' => 'privacy:metadata:requester' ]; $mypages = [ 'userid' => 'privacy:metadata:my_pages:userid', 'name' => 'privacy:metadata:my_pages:name', 'private' => 'privacy:metadata:my_pages:private', ]; $userpreferences = [ 'userid' => 'privacy:metadata:user_preferences:userid', 'name' => 'privacy:metadata:user_preferences:name', 'value' => 'privacy:metadata:user_preferences:value' ]; $collection->add_database_table('user', $userfields, 'privacy:metadata:usertablesummary'); $collection->add_database_table('user_password_history', $passwordhistory, 'privacy:metadata:passwordtablesummary'); $collection->add_database_table('user_password_resets', $userpasswordresets, 'privacy:metadata:passwordresettablesummary'); $collection->add_database_table('user_lastaccess', $lastaccess, 'privacy:metadata:lastaccesstablesummary'); $collection->add_database_table('user_devices', $userdevices, 'privacy:metadata:devicetablesummary'); $collection->add_database_table('course_request', $courserequest, 'privacy:metadata:requestsummary'); $collection->add_database_table('sessions', $usersessions, 'privacy:metadata:sessiontablesummary'); $collection->add_database_table('my_pages', $mypages, 'privacy:metadata:my_pages'); $collection->add_database_table('user_preferences', $userpreferences, 'privacy:metadata:user_preferences'); $collection->add_subsystem_link('core_files', [], 'privacy:metadata:filelink'); $collection->add_user_preference( 'core_user_welcome', 'privacy:metadata:user_preference:core_user_welcome' ); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : contextlist { $params = ['userid' => $userid, 'contextuser' => CONTEXT_USER]; $sql = "SELECT id FROM {context} WHERE instanceid = :userid and contextlevel = :contextuser"; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $userlist->add_user($context->instanceid); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { $context = $contextlist->current(); $user = \core_user::get_user($contextlist->get_user()->id); static::export_user($user, $context); static::export_password_history($user->id, $context); static::export_password_resets($user->id, $context); static::export_lastaccess($user->id, $context); static::export_course_requests($user->id, $context); static::export_user_devices($user->id, $context); static::export_user_session_data($user->id, $context); } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { // Only delete data for a user context. if ($context->contextlevel == CONTEXT_USER) { static::delete_user_data($context->instanceid, $context); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_user_data($context->instanceid, $context); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { foreach ($contextlist as $context) { // Let's be super certain that we have the right information for this user here. if ($context->contextlevel == CONTEXT_USER && $contextlist->get_user()->id == $context->instanceid) { static::delete_user_data($contextlist->get_user()->id, $contextlist->current()); } } } /** * Deletes non vital information about a user. * * @param int $userid The user ID to delete * @param \context $context The user context */ protected static function delete_user_data(int $userid, \context $context) { global $DB; // Delete password history. $DB->delete_records('user_password_history', ['userid' => $userid]); // Delete last access. $DB->delete_records('user_lastaccess', ['userid' => $userid]); // Delete password resets. $DB->delete_records('user_password_resets', ['userid' => $userid]); // Delete user devices. $DB->delete_records('user_devices', ['userid' => $userid]); // Delete user course requests. $DB->delete_records('course_request', ['requester' => $userid]); // Delete sessions. $DB->delete_records('sessions', ['userid' => $userid]); // Do I delete user preferences? Seems like the right place to do it. $DB->delete_records('user_preferences', ['userid' => $userid]); // Delete all of the files for this user. $fs = get_file_storage(); $fs->delete_area_files($context->id, 'user'); // For the user record itself we only want to remove unnecessary data. We still need the core data to keep as a record // that we actually did follow the request to be forgotten. $user = \core_user::get_user($userid); // Update fields we wish to change to nothing. $user->deleted = 1; $user->idnumber = ''; $user->emailstop = 0; $user->phone1 = ''; $user->phone2 = ''; $user->institution = ''; $user->department = ''; $user->address = ''; $user->city = ''; $user->country = ''; $user->lang = ''; $user->calendartype = ''; $user->theme = ''; $user->timezone = ''; $user->firstaccess = 0; $user->lastaccess = 0; $user->lastlogin = 0; $user->currentlogin = 0; $user->lastip = 0; $user->secret = ''; $user->picture = ''; $user->description = ''; $user->descriptionformat = 0; $user->mailformat = 0; $user->maildigest = 0; $user->maildisplay = 0; $user->autosubscribe = 0; $user->trackforums = 0; $user->timecreated = 0; $user->timemodified = 0; $user->trustbitmask = 0; $user->imagealt = ''; $user->lastnamephonetic = ''; $user->firstnamephonetic = ''; $user->middlename = ''; $user->alternatename = ''; $DB->update_record('user', $user); } /** * Export core user data. * * @param \stdClass $user The user object. * @param \context $context The user context. */ protected static function export_user(\stdClass $user, \context $context) { $data = (object) [ 'auth' => $user->auth, 'confirmed' => transform::yesno($user->confirmed), 'policyagreed' => transform::yesno($user->policyagreed), 'deleted' => transform::yesno($user->deleted), 'suspended' => transform::yesno($user->suspended), 'username' => $user->username, 'idnumber' => $user->idnumber, 'firstname' => format_string($user->firstname, true, ['context' => $context]), 'lastname' => format_string($user->lastname, true, ['context' => $context]), 'email' => $user->email, 'emailstop' => transform::yesno($user->emailstop), 'phone1' => format_string($user->phone1, true, ['context' => $context]), 'phone2' => format_string($user->phone2, true, ['context' => $context]), 'institution' => format_string($user->institution, true, ['context' => $context]), 'department' => format_string($user->department, true, ['context' => $context]), 'address' => format_string($user->address, true, ['context' => $context]), 'city' => format_string($user->city, true, ['context' => $context]), 'country' => format_string($user->country, true, ['context' => $context]), 'lang' => $user->lang, 'calendartype' => $user->calendartype, 'theme' => $user->theme, 'timezone' => $user->timezone, 'firstaccess' => $user->firstaccess ? transform::datetime($user->firstaccess) : null, 'lastaccess' => $user->lastaccess ? transform::datetime($user->lastaccess) : null, 'lastlogin' => $user->lastlogin ? transform::datetime($user->lastlogin) : null, 'currentlogin' => $user->currentlogin ? transform::datetime($user->currentlogin) : null, 'lastip' => $user->lastip, 'secret' => $user->secret, 'picture' => $user->picture, 'description' => format_text( writer::with_context($context)->rewrite_pluginfile_urls( [], 'user', 'profile', '', $user->description ), $user->descriptionformat, ['context' => $context]), 'maildigest' => transform::yesno($user->maildigest), 'maildisplay' => $user->maildisplay, 'autosubscribe' => transform::yesno($user->autosubscribe), 'trackforums' => transform::yesno($user->trackforums), 'timecreated' => transform::datetime($user->timecreated), 'timemodified' => transform::datetime($user->timemodified), 'imagealt' => format_string($user->imagealt, true, ['context' => $context]), 'lastnamephonetic' => format_string($user->lastnamephonetic, true, ['context' => $context]), 'firstnamephonetic' => format_string($user->firstnamephonetic, true, ['context' => $context]), 'middlename' => format_string($user->middlename, true, ['context' => $context]), 'alternatename' => format_string($user->alternatename, true, ['context' => $context]) ]; writer::with_context($context)->export_area_files([], 'user', 'profile', 0) ->export_data([], $data); // Export profile images. writer::with_context($context)->export_area_files([get_string('privacy:profileimagespath', 'user')], 'user', 'icon', 0); // Export private files. writer::with_context($context)->export_area_files([get_string('privacy:privatefilespath', 'user')], 'user', 'private', 0); // Export draft files. writer::with_context($context)->export_area_files([get_string('privacy:draftfilespath', 'user')], 'user', 'draft', false); } /** * Export information about the last time a user accessed a course. * * @param int $userid The user ID. * @param \context $context The user context. */ protected static function export_lastaccess(int $userid, \context $context) { global $DB; $sql = "SELECT c.id, c.fullname, ul.timeaccess FROM {user_lastaccess} ul JOIN {course} c ON c.id = ul.courseid WHERE ul.userid = :userid"; $params = ['userid' => $userid]; $records = $DB->get_records_sql($sql, $params); if (!empty($records)) { $lastaccess = (object) array_map(function($record) use ($context) { return [ 'course_name' => format_string($record->fullname, true, ['context' => $context]), 'timeaccess' => transform::datetime($record->timeaccess) ]; }, $records); writer::with_context($context)->export_data([get_string('privacy:lastaccesspath', 'user')], $lastaccess); } } /** * Exports information about password resets. * * @param int $userid The user ID * @param \context $context Context for this user. */ protected static function export_password_resets(int $userid, \context $context) { global $DB; $records = $DB->get_records('user_password_resets', ['userid' => $userid]); if (!empty($records)) { $passwordresets = (object) array_map(function($record) { return [ 'timerequested' => transform::datetime($record->timerequested), 'timererequested' => transform::datetime($record->timererequested) ]; }, $records); writer::with_context($context)->export_data([get_string('privacy:passwordresetpath', 'user')], $passwordresets); } } /** * Exports information about the user's mobile devices. * * @param int $userid The user ID. * @param \context $context Context for this user. */ protected static function export_user_devices(int $userid, \context $context) { global $DB; $records = $DB->get_records('user_devices', ['userid' => $userid]); if (!empty($records)) { $userdevices = (object) array_map(function($record) { return [ 'appid' => $record->appid, 'name' => $record->name, 'model' => $record->model, 'platform' => $record->platform, 'version' => $record->version, 'timecreated' => transform::datetime($record->timecreated), 'timemodified' => transform::datetime($record->timemodified) ]; }, $records); writer::with_context($context)->export_data([get_string('privacy:devicespath', 'user')], $userdevices); } } /** * Exports information about course requests this user made. * * @param int $userid The user ID. * @param \context $context The context object */ protected static function export_course_requests(int $userid, \context $context) { global $DB; $sql = "SELECT cr.shortname, cr.fullname, cr.summary, cc.name AS category, cr.reason FROM {course_request} cr JOIN {course_categories} cc ON cr.category = cc.id WHERE cr.requester = :userid"; $params = ['userid' => $userid]; $records = $DB->get_records_sql($sql, $params); if ($records) { writer::with_context($context)->export_data([get_string('privacy:courserequestpath', 'user')], (object) $records); } } /** * Get details about the user's password history. * * @param int $userid The user ID that we are getting the password history for. * @param \context $context the user context. */ protected static function export_password_history(int $userid, \context $context) { global $DB; // Just provide a count of how many entries we have. $recordcount = $DB->count_records('user_password_history', ['userid' => $userid]); if ($recordcount) { $passwordhistory = (object) ['password_history_count' => $recordcount]; writer::with_context($context)->export_data([get_string('privacy:passwordhistorypath', 'user')], $passwordhistory); } } /** * Exports information about the user's session. * * @param int $userid The user ID. * @param \context $context The context for this user. */ protected static function export_user_session_data(int $userid, \context $context) { global $DB, $SESSION; $records = $DB->get_records('sessions', ['userid' => $userid]); if (!empty($records)) { $sessiondata = (object) array_map(function($record) { return [ 'state' => $record->state, 'sessdata' => base64_decode($record->sessdata), 'timecreated' => transform::datetime($record->timecreated), 'timemodified' => transform::datetime($record->timemodified), 'firstip' => $record->firstip, 'lastip' => $record->lastip ]; }, $records); writer::with_context($context)->export_data([get_string('privacy:sessionpath', 'user')], $sessiondata); } } /** * Export all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { $userwelcomepreference = get_user_preferences('core_user_welcome', null, $userid); if ($userwelcomepreference !== null) { writer::export_user_preference( 'core_user', 'core_user_welcome', $userwelcomepreference, get_string('privacy:metadata:user_preference:core_user_welcome', 'core_user') ); } } } classes/form/defaulthomepage_form.php 0000644 00000004003 15151162244 0014030 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form to allow user to set their default home page * * @package core_user * @copyright 2019 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\form; use lang_string; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . '/lib/formslib.php'); /** * Form class * * @copyright 2019 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class defaulthomepage_form extends \moodleform { /** * Define the form. */ public function definition () { global $CFG; $mform = $this->_form; $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $options = [HOMEPAGE_SITE => new lang_string('home')]; if (!empty($CFG->enabledashboard)) { $options[HOMEPAGE_MY] = new lang_string('mymoodle', 'admin'); } $options[HOMEPAGE_MYCOURSES] = new lang_string('mycourses', 'admin'); $mform->addElement('select', 'defaulthomepage', get_string('defaulthomepageuser'), $options); $mform->addHelpButton('defaulthomepage', 'defaulthomepageuser'); $mform->setDefault('defaulthomepage', get_default_home_page()); $this->add_action_buttons(true, get_string('savechanges')); } } classes/form/profile_field_form.php 0000644 00000014122 15151162244 0013504 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user\form; use context; use core_form\dynamic_form; use moodle_url; use profile_define_base; /** * Class field_form used for profile fields. * * @package core_user * @copyright 2007 onwards Shane Elliot {@link http://pukunui.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_field_form extends dynamic_form { /** @var profile_define_base $field */ public $field; /** @var \stdClass */ protected $fieldrecord; /** * Define the form */ public function definition () { global $CFG; require_once($CFG->dirroot.'/user/profile/definelib.php'); $mform = $this->_form; // Everything else is dependant on the data type. $datatype = $this->get_field_record()->datatype; require_once($CFG->dirroot.'/user/profile/field/'.$datatype.'/define.class.php'); $newfield = 'profile_define_'.$datatype; $this->field = new $newfield(); // Add some extra hidden fields. $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'action', 'editfield'); $mform->setType('action', PARAM_ALPHANUMEXT); $mform->addElement('hidden', 'datatype', $datatype); $mform->setType('datatype', PARAM_ALPHA); $this->field->define_form($mform); } /** * Alter definition based on existing or submitted data */ public function definition_after_data () { $mform = $this->_form; $this->field->define_after_data($mform); } /** * Perform some moodle validation. * @param array $data * @param array $files * @return array */ public function validation($data, $files) { return $this->field->define_validate($data, $files); } /** * Returns the defined editors for the field. * @return array */ public function editors(): array { $editors = $this->field->define_editors(); return is_array($editors) ? $editors : []; } /** * Returns context where this form is used * * @return context */ protected function get_context_for_dynamic_submission(): context { return \context_system::instance(); } /** * Checks if current user has access to this form, otherwise throws exception */ protected function check_access_for_dynamic_submission(): void { require_capability('moodle/site:config', $this->get_context_for_dynamic_submission()); } /** * Process the form submission, used if form was submitted via AJAX */ public function process_dynamic_submission() { global $CFG; require_once($CFG->dirroot.'/user/profile/definelib.php'); profile_save_field($this->get_data(), $this->editors()); } /** * Load in existing data as form defaults */ public function set_data_for_dynamic_submission(): void { $field = $this->get_field_record(); // Clean and prepare description for the editor. $description = clean_text($field->description, $field->descriptionformat); $field->description = ['text' => $description, 'format' => $field->descriptionformat, 'itemid' => 0]; // Convert the data format for. if (is_array($this->editors())) { foreach ($this->editors() as $editor) { if (isset($field->$editor)) { $editordesc = clean_text($field->$editor, $field->{$editor.'format'}); $field->$editor = ['text' => $editordesc, 'format' => $field->{$editor.'format'}, 'itemid' => 0]; } } } $this->set_data($field); } /** * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX * * @return moodle_url */ protected function get_page_url_for_dynamic_submission(): moodle_url { $id = $this->optional_param('id', 0, PARAM_INT); $datatype = $this->optional_param('datatype', 'text', PARAM_PLUGIN); return new moodle_url('/user/profile/index.php', ['action' => 'editfield', 'id' => $id, 'datatype' => $id ? null : $datatype]); } /** * Record for the field from the database (or generic record for a new field) * * @return false|mixed|\stdClass * @throws \coding_exception * @throws \dml_exception */ public function get_field_record() { global $DB; if (!$this->fieldrecord) { $id = $this->optional_param('id', 0, PARAM_INT); if (!$id || !($this->fieldrecord = $DB->get_record('user_info_field', ['id' => $id]))) { $datatype = $this->optional_param('datatype', 'text', PARAM_PLUGIN); $this->fieldrecord = new \stdClass(); $this->fieldrecord->datatype = $datatype; $this->fieldrecord->description = ''; $this->fieldrecord->descriptionformat = FORMAT_HTML; $this->fieldrecord->defaultdata = ''; $this->fieldrecord->defaultdataformat = FORMAT_HTML; $this->fieldrecord->categoryid = $this->optional_param('categoryid', 0, PARAM_INT); } if (!\core_component::get_component_directory('profilefield_'.$this->fieldrecord->datatype)) { throw new \moodle_exception('fieldnotfound', 'customfield'); } } return $this->fieldrecord; } } classes/form/calendar_form.php 0000644 00000014327 15151162244 0012461 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form to edit a users preferred language * * @copyright 2015 Shamim Rezaie http://foodle.org * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ namespace core_user\form; if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. } require_once($CFG->dirroot.'/lib/formslib.php'); /** * Class user_edit_calendar_form. * * @copyright 2015 Shamim Rezaie http://foodle.org * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class calendar_form extends \moodleform { /** * Define the form. */ public function definition () { global $CFG, $USER; $mform = $this->_form; $userid = $USER->id; if (is_array($this->_customdata)) { if (array_key_exists('userid', $this->_customdata)) { $userid = $this->_customdata['userid']; } } // Add some extra hidden fields. $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); // We do not want to show this option unless there is more than one calendar type to display. if (count(\core_calendar\type_factory::get_list_of_calendar_types()) > 1) { $calendartypes = \core_calendar\type_factory::get_list_of_calendar_types(); $mform->addElement('select', 'calendartype', get_string('preferredcalendar', 'calendar'), $calendartypes); $mform->setType('calendartype', PARAM_ALPHANUM); $mform->setDefault('calendartype', $CFG->calendartype); } else { $mform->addElement('hidden', 'calendartype', $CFG->calendartype); $mform->setType('calendartype', PARAM_ALPHANUM); } // Date / Time settings. $options = array( '0' => get_string('default', 'calendar'), CALENDAR_TF_12 => get_string('timeformat_12', 'calendar'), CALENDAR_TF_24 => get_string('timeformat_24', 'calendar') ); $mform->addElement('select', 'timeformat', get_string('pref_timeformat', 'calendar'), $options); $mform->addHelpButton('timeformat', 'pref_timeformat', 'calendar'); // First day of week. $options = array( 0 => get_string('sunday', 'calendar'), 1 => get_string('monday', 'calendar'), 2 => get_string('tuesday', 'calendar'), 3 => get_string('wednesday', 'calendar'), 4 => get_string('thursday', 'calendar'), 5 => get_string('friday', 'calendar'), 6 => get_string('saturday', 'calendar') ); $mform->addElement('select', 'startwday', get_string('pref_startwday', 'calendar'), $options); $mform->addHelpButton('startwday', 'pref_startwday', 'calendar'); // Maximum events to display. $options = array(); for ($i = 1; $i <= 20; $i++) { $options[$i] = $i; } $mform->addElement('select', 'maxevents', get_string('pref_maxevents', 'calendar'), $options); $mform->addHelpButton('maxevents', 'pref_maxevents', 'calendar'); // Calendar lookahead. $options = array(365 => new \lang_string('numyear', '', 1), 270 => get_string('nummonths', '', 9), 180 => get_string('nummonths', '', 6), 150 => get_string('nummonths', '', 5), 120 => get_string('nummonths', '', 4), 90 => get_string('nummonths', '', 3), 60 => get_string('nummonths', '', 2), 30 => get_string('nummonth', '', 1), 21 => get_string('numweeks', '', 3), 14 => get_string('numweeks', '', 2), 7 => get_string('numweek', '', 1), 6 => get_string('numdays', '', 6), 5 => get_string('numdays', '', 5), 4 => get_string('numdays', '', 4), 3 => get_string('numdays', '', 3), 2 => get_string('numdays', '', 2), 1 => get_string('numday', '', 1)); $mform->addElement('select', 'lookahead', get_string('pref_lookahead', 'calendar'), $options); $mform->addHelpButton('lookahead', 'pref_lookahead', 'calendar'); // Remember event filtering. $options = array( 0 => get_string('no'), 1 => get_string('yes') ); $mform->addElement('select', 'persistflt', get_string('pref_persistflt', 'calendar'), $options); $mform->addHelpButton('persistflt', 'pref_persistflt', 'calendar'); $this->add_action_buttons(true, get_string('savechanges')); } /** * Extend the form definition after the data has been parsed. */ public function definition_after_data() { global $CFG; $mform = $this->_form; // If calendar type does not exist, use site default calendar type. if ($calendarselected = $mform->getElementValue('calendartype')) { if (is_array($calendarselected)) { // There are multiple calendar types available. $calendar = reset($calendarselected); } else { // There is only one calendar type available. $calendar = $calendarselected; } // Check calendar type exists. if (!array_key_exists($calendar, \core_calendar\type_factory::get_list_of_calendar_types())) { $calendartypeel = $mform->getElement('calendartype'); $calendartypeel->setValue($CFG->calendartype); } } } } classes/form/contentbank_user_preferences_form.php 0000644 00000003467 15151162244 0016640 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user\form; use \core_contentbank\content; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot.'/lib/formslib.php'); /** * Form to edit a user's preferences concerning the content bank. * * @package core_user * @copyright 2020 François Moreau * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contentbank_user_preferences_form extends \moodleform { /** * Define the form. */ public function definition () { global $CFG, $USER; $mform = $this->_form; $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $options = [ content::VISIBILITY_PUBLIC => get_string('visibilitychoicepublic', 'core_contentbank'), content::VISIBILITY_UNLISTED => get_string('visibilitychoiceunlisted', 'core_contentbank') ]; $mform->addElement('select', 'contentvisibility', get_string('visibilitypref', 'core_contentbank'), $options); $mform->addHelpButton('contentvisibility', 'visibilitypref', 'core_contentbank'); $this->add_action_buttons(true, get_string('savechanges')); } } classes/form/profile_category_form.php 0000644 00000010023 15151162244 0014232 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user\form; use context; use core_form\dynamic_form; use moodle_url; /** * Modal form to edit profile category * * @package core_user * @copyright 2021 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class profile_category_form extends dynamic_form { /** * Form definition */ protected function definition() { $mform = $this->_form; $strrequired = get_string('required'); // Add some extra hidden fields. $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'action', 'editcategory'); $mform->setType('action', PARAM_ALPHANUMEXT); $mform->addElement('text', 'name', get_string('profilecategoryname', 'admin'), 'maxlength="255" size="30"'); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', $strrequired, 'required', null, 'client'); } /** * Perform some moodle validation. * * @param array $data * @param array $files * @return array */ public function validation($data, $files) { global $DB; $errors = parent::validation($data, $files); $duplicate = $DB->get_field('user_info_category', 'id', ['name' => $data['name']]); // Check the name is unique. if (!empty($data['id'])) { // We are editing an existing record. $olddata = $DB->get_record('user_info_category', ['id' => $data['id']]); // Name has changed, new name in use, new name in use by another record. $dupfound = (($olddata->name !== $data['name']) && $duplicate && ($data['id'] != $duplicate)); } else { // New profile category. $dupfound = $duplicate; } if ($dupfound ) { $errors['name'] = get_string('profilecategorynamenotunique', 'admin'); } return $errors; } /** * Returns context where this form is used * * @return context */ protected function get_context_for_dynamic_submission(): context { return \context_system::instance(); } /** * Checks if current user has access to this form, otherwise throws exception */ protected function check_access_for_dynamic_submission(): void { require_capability('moodle/site:config', $this->get_context_for_dynamic_submission()); } /** * Process the form submission, used if form was submitted via AJAX */ public function process_dynamic_submission() { global $CFG; require_once($CFG->dirroot.'/user/profile/definelib.php'); profile_save_category($this->get_data()); } /** * Load in existing data as form defaults */ public function set_data_for_dynamic_submission(): void { global $DB; if ($id = $this->optional_param('id', 0, PARAM_INT)) { $this->set_data($DB->get_record('user_info_category', ['id' => $id], '*', MUST_EXIST)); } } /** * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX * * @return moodle_url */ protected function get_page_url_for_dynamic_submission(): moodle_url { $id = $this->optional_param('id', 0, PARAM_INT); return new moodle_url('/user/profile/index.php', ['action' => 'editcategory', 'id' => $id]); } } classes/form/private_files.php 0000644 00000015577 15151162244 0012531 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user\form; use html_writer; use moodle_url; /** * Manage user private area files form * * @package core_user * @copyright 2010 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class private_files extends \core_form\dynamic_form { /** * Add elements to this form. */ public function definition() { global $OUTPUT; $mform = $this->_form; $options = $this->get_options(); // Show file area space usage. $maxareabytes = $options['areamaxbytes']; if ($maxareabytes != FILE_AREA_MAX_BYTES_UNLIMITED) { $fileareainfo = file_get_file_area_info($this->get_context_for_dynamic_submission()->id, 'user', 'private'); // Display message only if we have files. if ($fileareainfo['filecount']) { $a = (object) [ 'used' => display_size($fileareainfo['filesize_without_references']), 'total' => display_size($maxareabytes, 0) ]; $quotamsg = get_string('quotausage', 'moodle', $a); $notification = new \core\output\notification($quotamsg, \core\output\notification::NOTIFY_INFO); $mform->addElement('static', 'areabytes', '', $OUTPUT->render($notification)); } } $mform->addElement('filemanager', 'files_filemanager', get_string('files'), null, $options); if ($link = $this->get_emaillink()) { $emaillink = html_writer::link(new moodle_url('mailto:' . $link), $link); $mform->addElement('static', 'emailaddress', '', get_string('emailtoprivatefiles', 'moodle', $emaillink)); } $mform->setType('returnurl', PARAM_LOCALURL); // The 'nosubmit' param (default false) determines whether we should show the standard form action buttons (save/cancel). // This value is set when the form is displayed within a modal, which adds the action buttons itself. if (!$this->optional_param('nosubmit', false, PARAM_BOOL)) { $this->add_action_buttons(); } } /** * Validate incoming data. * * @param array $data * @param array $files * @return array */ public function validation($data, $files) { $errors = array(); $draftitemid = $data['files_filemanager']; $options = $this->get_options(); if (file_is_draft_area_limit_reached($draftitemid, $options['areamaxbytes'])) { $errors['files_filemanager'] = get_string('userquotalimit', 'error'); } return $errors; } /** * Link to email private files * * @return string|null * @throws \coding_exception */ protected function get_emaillink() { global $USER; // Attempt to generate an inbound message address to support e-mail to private files. $generator = new \core\message\inbound\address_manager(); $generator->set_handler('\core\message\inbound\private_files_handler'); $generator->set_data(-1); return $generator->generate($USER->id); } /** * Check if current user has access to this form, otherwise throw exception * * Sometimes permission check may depend on the action and/or id of the entity. * If necessary, form data is available in $this->_ajaxformdata or * by calling $this->optional_param() */ protected function check_access_for_dynamic_submission(): void { require_capability('moodle/user:manageownfiles', $this->get_context_for_dynamic_submission()); } /** * Returns form context * * If context depends on the form data, it is available in $this->_ajaxformdata or * by calling $this->optional_param() * * @return \context */ protected function get_context_for_dynamic_submission(): \context { global $USER; return \context_user::instance($USER->id); } /** * File upload options * * @return array * @throws \coding_exception */ protected function get_options(): array { global $CFG; $maxbytes = $CFG->userquota; $maxareabytes = $CFG->userquota; if (has_capability('moodle/user:ignoreuserquota', $this->get_context_for_dynamic_submission())) { $maxbytes = USER_CAN_IGNORE_FILE_SIZE_LIMITS; $maxareabytes = FILE_AREA_MAX_BYTES_UNLIMITED; } return ['subdirs' => 1, 'maxbytes' => $maxbytes, 'maxfiles' => -1, 'accepted_types' => '*', 'areamaxbytes' => $maxareabytes]; } /** * Process the form submission, used if form was submitted via AJAX * * This method can return scalar values or arrays that can be json-encoded, they will be passed to the caller JS. * * Submission data can be accessed as: $this->get_data() * * @return mixed */ public function process_dynamic_submission() { file_postupdate_standard_filemanager($this->get_data(), 'files', $this->get_options(), $this->get_context_for_dynamic_submission(), 'user', 'private', 0); return null; } /** * Load in existing data as form defaults * * Can be overridden to retrieve existing values from db by entity id and also * to preprocess editor and filemanager elements * * Example: * $this->set_data(get_entity($this->_ajaxformdata['id'])); */ public function set_data_for_dynamic_submission(): void { $data = new \stdClass(); file_prepare_standard_filemanager($data, 'files', $this->get_options(), $this->get_context_for_dynamic_submission(), 'user', 'private', 0); $this->set_data($data); } /** * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX * * This is used in the form elements sensitive to the page url, such as Atto autosave in 'editor' * * If the form has arguments (such as 'id' of the element being edited), the URL should * also have respective argument. * * @return \moodle_url */ protected function get_page_url_for_dynamic_submission(): \moodle_url { return new moodle_url('/user/files.php'); } } classes/form/contactsitesupport_form.php 0000644 00000010143 15151162244 0014655 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user\form; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot.'/lib/formslib.php'); /** * Contact site support form. * * @package core_user * @copyright 2022 Simey Lameze <simey@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contactsitesupport_form extends \moodleform { /** * Define the contact site support form. */ public function definition(): void { global $CFG; $mform = $this->_form; $user = $this->_customdata; $strrequired = get_string('required'); // Name. $mform->addElement('text', 'name', get_string('name')); $mform->addRule('name', $strrequired, 'required', null, 'client'); $mform->setType('name', PARAM_TEXT); // Email. $mform->addElement('text', 'email', get_string('email')); $mform->addRule('email', get_string('missingemail'), 'required', null, 'client'); $mform->setType('email', PARAM_EMAIL); // Subject. $mform->addElement('text', 'subject', get_string('subject')); $mform->addRule('subject', $strrequired, 'required', null, 'client'); $mform->setType('subject', PARAM_TEXT); // Message. $mform->addElement('textarea', 'message', get_string('message')); $mform->addRule('message', $strrequired, 'required', null, 'client'); $mform->setType('message', PARAM_TEXT); // If the user is logged in set name and email fields to the current user info. if (isloggedin() && !isguestuser()) { $mform->setDefault('name', fullname($user)); $mform->hardFreeze('name'); $mform->setDefault('email', $user->email); $mform->hardFreeze('email'); } if (!empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey)) { $mform->addElement('recaptcha', 'recaptcha_element', get_string('security_question', 'auth')); $mform->addHelpButton('recaptcha_element', 'recaptcha', 'auth'); $mform->closeHeaderBefore('recaptcha_element'); } $this->add_action_buttons(true, get_string('submit')); } /** * Validate user supplied data on the contact site support form. * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array of "element_name"=>"error_description" if there are errors, * or an empty array if everything is OK (true allowed for backwards compatibility too). */ public function validation($data, $files): array { $errors = parent::validation($data, $files); if (!validate_email($data['email'])) { $errors['email'] = get_string('invalidemail'); } if ($this->_form->elementExists('recaptcha_element')) { $recaptchaelement = $this->_form->getElement('recaptcha_element'); if (!empty($this->_form->_submitValues['g-recaptcha-response'])) { $response = $this->_form->_submitValues['g-recaptcha-response']; if (!$recaptchaelement->verify($response)) { $errors['recaptcha_element'] = get_string('incorrectpleasetryagain', 'auth'); } } else { $errors['recaptcha_element'] = get_string('missingrecaptchachallengefield'); } } return $errors; } } classes/reportbuilder/datasource/users.php 0000644 00000010062 15151162244 0015067 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_user\reportbuilder\datasource; use lang_string; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\user; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\helpers\database; use core_tag\reportbuilder\local\entities\tag; use core_reportbuilder\manager; use core_reportbuilder\local\helpers\report; /** * Users datasource * * @package core_user * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class users extends datasource { /** * Return user friendly name of the datasource * * @return string */ public static function get_name(): string { return get_string('users'); } /** * Initialise report */ protected function initialise(): void { global $CFG; $userentity = new user(); $usertablealias = $userentity->get_table_alias('user'); $this->set_main_table('user', $usertablealias); $userparamguest = database::generate_param_name(); $this->add_base_condition_sql("{$usertablealias}.id != :{$userparamguest} AND {$usertablealias}.deleted = 0", [ $userparamguest => $CFG->siteguest, ]); $this->add_entity($userentity); // Join the tag entity. $tagentity = (new tag()) ->set_table_alias('tag', $userentity->get_table_alias('tag')) ->set_entity_title(new lang_string('interests')); $this->add_entity($tagentity ->add_joins($userentity->get_tag_joins())); // Add all columns/filters/conditions from entities to be available in custom reports. $this->add_all_from_entity($userentity->get_entity_name()); // Add specific tag entity elements. $this->add_columns_from_entity($tagentity->get_entity_name(), ['name', 'namewithlink']); $this->add_filter($tagentity->get_filter('name')); $this->add_condition($tagentity->get_condition('name')); } /** * Return the columns that will be added to the report once is created * * @return string[] */ public function get_default_columns(): array { return ['user:fullname', 'user:username', 'user:email']; } /** * Return the filters that will be added to the report once is created * * @return string[] */ public function get_default_filters(): array { return ['user:fullname', 'user:username', 'user:email']; } /** * Return the conditions that will be added to the report once is created * * @return string[] */ public function get_default_conditions(): array { return [ 'user:fullname', 'user:username', 'user:email', 'user:suspended', ]; } /** * Return the conditions values that will be added to the report once is created * * @return array */ public function get_default_condition_values(): array { return [ 'user:suspended_operator' => boolean_select::NOT_CHECKED, ]; } /** * Return the default sorting that will be added to the report once it is created * * @return array|int[] */ public function get_default_column_sorting(): array { return [ 'user:fullname' => SORT_ASC, ]; } } classes/table/participants.php 0000644 00000040507 15151162244 0012511 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains the class used for the displaying the participants table. * * @package core_user * @copyright 2017 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace core_user\table; use DateTime; use context; use core_table\dynamic as dynamic_table; use core_table\local\filter\filterset; use core_user\output\status_field; use core_user\table\participants_search; use moodle_url; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->libdir . '/tablelib.php'); require_once($CFG->dirroot . '/user/lib.php'); /** * Class for the displaying the participants table. * * @package core_user * @copyright 2017 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class participants extends \table_sql implements dynamic_table { /** * @var int $courseid The course id */ protected $courseid; /** * @var string[] The list of countries. */ protected $countries; /** * @var \stdClass[] The list of groups with membership info for the course. */ protected $groups; /** * @var string[] Extra fields to display. */ protected $extrafields; /** * @var \stdClass $course The course details. */ protected $course; /** * @var context $context The course context. */ protected $context; /** * @var \stdClass[] List of roles indexed by roleid. */ protected $allroles; /** * @var \stdClass[] List of roles indexed by roleid. */ protected $allroleassignments; /** * @var \stdClass[] Assignable roles in this course. */ protected $assignableroles; /** * @var \stdClass[] Profile roles in this course. */ protected $profileroles; /** * @var filterset Filterset describing which participants to include. */ protected $filterset; /** @var \stdClass[] $viewableroles */ private $viewableroles; /** @var moodle_url $baseurl The base URL for the report. */ public $baseurl; /** * Render the participants table. * * @param int $pagesize Size of page for paginated displayed table. * @param bool $useinitialsbar Whether to use the initials bar which will only be used if there is a fullname column defined. * @param string $downloadhelpbutton */ public function out($pagesize, $useinitialsbar, $downloadhelpbutton = '') { global $CFG, $OUTPUT, $PAGE; // Define the headers and columns. $headers = []; $columns = []; // At the very least, the user viewing this table will be able to use bulk actions to export it, so add 'select' column. $mastercheckbox = new \core\output\checkbox_toggleall('participants-table', true, [ 'id' => 'select-all-participants', 'name' => 'select-all-participants', 'label' => get_string('selectall'), 'labelclasses' => 'sr-only', 'classes' => 'm-1', 'checked' => false, ]); $headers[] = $OUTPUT->render($mastercheckbox); $columns[] = 'select'; $headers[] = get_string('fullname'); $columns[] = 'fullname'; $extrafields = \core_user\fields::get_identity_fields($this->context); foreach ($extrafields as $field) { $headers[] = \core_user\fields::get_display_name($field); $columns[] = $field; } $headers[] = get_string('roles'); $columns[] = 'roles'; // Get the list of fields we have to hide. $hiddenfields = array(); if (!has_capability('moodle/course:viewhiddenuserfields', $this->context)) { $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); } // Add column for groups if the user can view them. $canseegroups = !isset($hiddenfields['groups']); if ($canseegroups) { $headers[] = get_string('groups'); $columns[] = 'groups'; } // Do not show the columns if it exists in the hiddenfields array. if (!isset($hiddenfields['lastaccess'])) { if ($this->courseid == SITEID) { $headers[] = get_string('lastsiteaccess'); } else { $headers[] = get_string('lastcourseaccess'); } $columns[] = 'lastaccess'; } $canreviewenrol = has_capability('moodle/course:enrolreview', $this->context); if ($canreviewenrol && $this->courseid != SITEID) { $columns[] = 'status'; $headers[] = get_string('participationstatus', 'enrol'); $this->no_sorting('status'); }; $this->define_columns($columns); $this->define_headers($headers); // The name column is a header. $this->define_header_column('fullname'); // Make this table sorted by last name by default. $this->sortable(true, 'lastname'); $this->no_sorting('select'); $this->no_sorting('roles'); if ($canseegroups) { $this->no_sorting('groups'); } $this->set_default_per_page(20); $this->set_attribute('id', 'participants'); $this->countries = get_string_manager()->get_list_of_countries(true); $this->extrafields = $extrafields; if ($canseegroups) { $this->groups = groups_get_all_groups($this->courseid, 0, 0, 'g.*', true); } // If user has capability to review enrol, show them both role names. $allrolesnamedisplay = ($canreviewenrol ? ROLENAME_BOTH : ROLENAME_ALIAS); $this->allroles = role_fix_names(get_all_roles($this->context), $this->context, $allrolesnamedisplay); $this->assignableroles = get_assignable_roles($this->context, ROLENAME_BOTH, false); $this->profileroles = get_profile_roles($this->context); $this->viewableroles = get_viewable_roles($this->context); parent::out($pagesize, $useinitialsbar, $downloadhelpbutton); if (has_capability('moodle/course:enrolreview', $this->context)) { $params = [ 'contextid' => $this->context->id, 'uniqueid' => $this->uniqueid, ]; $PAGE->requires->js_call_amd('core_user/status_field', 'init', [$params]); } } /** * Generate the select column. * * @param \stdClass $data * @return string */ public function col_select($data) { global $OUTPUT; $checkbox = new \core\output\checkbox_toggleall('participants-table', false, [ 'classes' => 'usercheckbox m-1', 'id' => 'user' . $data->id, 'name' => 'user' . $data->id, 'checked' => false, 'label' => get_string('selectitem', 'moodle', fullname($data)), 'labelclasses' => 'accesshide', ]); return $OUTPUT->render($checkbox); } /** * Generate the fullname column. * * @param \stdClass $data * @return string */ public function col_fullname($data) { global $OUTPUT; return $OUTPUT->user_picture($data, array('size' => 35, 'courseid' => $this->course->id, 'includefullname' => true)); } /** * User roles column. * * @param \stdClass $data * @return string */ public function col_roles($data) { global $OUTPUT; $roles = isset($this->allroleassignments[$data->id]) ? $this->allroleassignments[$data->id] : []; $editable = new \core_user\output\user_roles_editable($this->course, $this->context, $data, $this->allroles, $this->assignableroles, $this->profileroles, $roles, $this->viewableroles); return $OUTPUT->render_from_template('core/inplace_editable', $editable->export_for_template($OUTPUT)); } /** * Generate the groups column. * * @param \stdClass $data * @return string */ public function col_groups($data) { global $OUTPUT; $usergroups = []; foreach ($this->groups as $coursegroup) { if (isset($coursegroup->members[$data->id])) { $usergroups[] = $coursegroup->id; } } $editable = new \core_group\output\user_groups_editable($this->course, $this->context, $data, $this->groups, $usergroups); return $OUTPUT->render_from_template('core/inplace_editable', $editable->export_for_template($OUTPUT)); } /** * Generate the country column. * * @param \stdClass $data * @return string */ public function col_country($data) { if (!empty($this->countries[$data->country])) { return $this->countries[$data->country]; } return ''; } /** * Generate the last access column. * * @param \stdClass $data * @return string */ public function col_lastaccess($data) { if ($data->lastaccess) { return format_time(time() - $data->lastaccess); } return get_string('never'); } /** * Generate the status column. * * @param \stdClass $data The data object. * @return string */ public function col_status($data) { global $CFG, $OUTPUT, $PAGE; $enrolstatusoutput = ''; $canreviewenrol = has_capability('moodle/course:enrolreview', $this->context); if ($canreviewenrol) { $canviewfullnames = has_capability('moodle/site:viewfullnames', $this->context); $fullname = fullname($data, $canviewfullnames); $coursename = format_string($this->course->fullname, true, array('context' => $this->context)); require_once($CFG->dirroot . '/enrol/locallib.php'); $manager = new \course_enrolment_manager($PAGE, $this->course); $userenrolments = $manager->get_user_enrolments($data->id); foreach ($userenrolments as $ue) { $timestart = $ue->timestart; $timeend = $ue->timeend; $timeenrolled = $ue->timecreated; $actions = $ue->enrolmentplugin->get_user_enrolment_actions($manager, $ue); $instancename = $ue->enrolmentinstancename; // Default status field label and value. $status = get_string('participationactive', 'enrol'); $statusval = status_field::STATUS_ACTIVE; switch ($ue->status) { case ENROL_USER_ACTIVE: $currentdate = new DateTime(); $now = $currentdate->getTimestamp(); $isexpired = $timestart > $now || ($timeend > 0 && $timeend < $now); $enrolmentdisabled = $ue->enrolmentinstance->status == ENROL_INSTANCE_DISABLED; // If user enrolment status has not yet started/already ended or the enrolment instance is disabled. if ($isexpired || $enrolmentdisabled) { $status = get_string('participationnotcurrent', 'enrol'); $statusval = status_field::STATUS_NOT_CURRENT; } break; case ENROL_USER_SUSPENDED: $status = get_string('participationsuspended', 'enrol'); $statusval = status_field::STATUS_SUSPENDED; break; } $statusfield = new status_field($instancename, $coursename, $fullname, $status, $timestart, $timeend, $actions, $timeenrolled); $statusfielddata = $statusfield->set_status($statusval)->export_for_template($OUTPUT); $enrolstatusoutput .= $OUTPUT->render_from_template('core_user/status_field', $statusfielddata); } } return $enrolstatusoutput; } /** * This function is used for the extra user fields. * * These are being dynamically added to the table so there are no functions 'col_<userfieldname>' as * the list has the potential to increase in the future and we don't want to have to remember to add * a new method to this class. We also don't want to pollute this class with unnecessary methods. * * @param string $colname The column name * @param \stdClass $data * @return string */ public function other_cols($colname, $data) { // Do not process if it is not a part of the extra fields. if (!in_array($colname, $this->extrafields)) { return ''; } return s($data->{$colname}); } /** * Query the database for results to display in the table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. */ public function query_db($pagesize, $useinitialsbar = true) { list($twhere, $tparams) = $this->get_sql_where(); $psearch = new participants_search($this->course, $this->context, $this->filterset); $total = $psearch->get_total_participants_count($twhere, $tparams); $this->pagesize($pagesize, $total); $sort = $this->get_sql_sort(); if ($sort) { $sort = 'ORDER BY ' . $sort; } $rawdata = $psearch->get_participants($twhere, $tparams, $sort, $this->get_page_start(), $this->get_page_size()); $this->rawdata = []; foreach ($rawdata as $user) { $this->rawdata[$user->id] = $user; } $rawdata->close(); if ($this->rawdata) { $this->allroleassignments = get_users_roles($this->context, array_keys($this->rawdata), true, 'c.contextlevel DESC, r.sortorder ASC'); } else { $this->allroleassignments = []; } // Set initial bars. if ($useinitialsbar) { $this->initialbars(true); } } /** * Override the table show_hide_link to not show for select column. * * @param string $column the column name, index into various names. * @param int $index numerical index of the column. * @return string HTML fragment. */ protected function show_hide_link($column, $index) { if ($index > 0) { return parent::show_hide_link($column, $index); } return ''; } /** * Set filters and build table structure. * * @param filterset $filterset The filterset object to get the filters from. */ public function set_filterset(filterset $filterset): void { // Get the context. $this->courseid = $filterset->get_filter('courseid')->current(); $this->course = get_course($this->courseid); $this->context = \context_course::instance($this->courseid, MUST_EXIST); // Process the filterset. parent::set_filterset($filterset); } /** * Guess the base url for the participants table. */ public function guess_base_url(): void { $this->baseurl = new moodle_url('/user/index.php', ['id' => $this->courseid]); } /** * Get the context of the current table. * * Note: This function should not be called until after the filterset has been provided. * * @return context */ public function get_context(): context { return $this->context; } } classes/table/participants_search.php 0000644 00000130714 15151162244 0014036 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class used to fetch participants based on a filterset. * * @package core_user * @copyright 2020 Michael Hawkins <michaelh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\table; use context; use context_helper; use core_table\local\filter\filterset; use core_user; use moodle_recordset; use stdClass; use core_user\fields; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . '/user/lib.php'); /** * Class used to fetch participants based on a filterset. * * @package core_user * @copyright 2020 Michael Hawkins <michaelh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class participants_search { /** * @var filterset $filterset The filterset describing which participants to include in the search. */ protected $filterset; /** * @var stdClass $course The course being searched. */ protected $course; /** * @var context_course $context The course context being searched. */ protected $context; /** * @var string[] $userfields Names of any extra user fields to be shown when listing users. */ protected $userfields; /** * Class constructor. * * @param stdClass $course The course being searched. * @param context $context The context of the search. * @param filterset $filterset The filterset used to filter the participants in a course. */ public function __construct(stdClass $course, context $context, filterset $filterset) { $this->course = $course; $this->context = $context; $this->filterset = $filterset; $this->userfields = fields::get_identity_fields($this->context); } /** * Fetch participants matching the filterset. * * @param string $additionalwhere Any additional SQL to add to where. * @param array $additionalparams The additional params used by $additionalwhere. * @param string $sort Optional SQL sort. * @param int $limitfrom Return a subset of records, starting at this point (optional). * @param int $limitnum Return a subset comprising this many records (optional, required if $limitfrom is set). * @return moodle_recordset */ public function get_participants(string $additionalwhere = '', array $additionalparams = [], string $sort = '', int $limitfrom = 0, int $limitnum = 0): moodle_recordset { global $DB; [ 'subqueryalias' => $subqueryalias, 'outerselect' => $outerselect, 'innerselect' => $innerselect, 'outerjoins' => $outerjoins, 'innerjoins' => $innerjoins, 'outerwhere' => $outerwhere, 'innerwhere' => $innerwhere, 'params' => $params, ] = $this->get_participants_sql($additionalwhere, $additionalparams); $sql = "{$outerselect} FROM ({$innerselect} FROM {$innerjoins} {$innerwhere} ) {$subqueryalias} {$outerjoins} {$outerwhere} {$sort}"; return $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum); } /** * Returns the total number of participants for a given course. * * @param string $additionalwhere Any additional SQL to add to where. * @param array $additionalparams The additional params used by $additionalwhere. * @return int */ public function get_total_participants_count(string $additionalwhere = '', array $additionalparams = []): int { global $DB; [ 'subqueryalias' => $subqueryalias, 'innerselect' => $innerselect, 'outerjoins' => $outerjoins, 'innerjoins' => $innerjoins, 'outerwhere' => $outerwhere, 'innerwhere' => $innerwhere, 'params' => $params, ] = $this->get_participants_sql($additionalwhere, $additionalparams); $sql = "SELECT COUNT(u.id) FROM ({$innerselect} FROM {$innerjoins} {$innerwhere} ) {$subqueryalias} {$outerjoins} {$outerwhere}"; return $DB->count_records_sql($sql, $params); } /** * Generate the SQL used to fetch filtered data for the participants table. * * @param string $additionalwhere Any additional SQL to add to where * @param array $additionalparams The additional params * @return array */ protected function get_participants_sql(string $additionalwhere, array $additionalparams): array { global $CFG; $isfrontpage = ($this->course->id == SITEID); $accesssince = 0; // Whether to match on users who HAVE accessed since the given time (ie false is 'inactive for more than x'). $matchaccesssince = false; // The alias for the subquery that fetches all distinct course users. $usersubqueryalias = 'targetusers'; // The alias for {user} within the distinct user subquery. $inneruseralias = 'udistinct'; // Inner query that selects distinct users in a course who are not deleted. // Note: This ensures the outer (filtering) query joins on distinct users, avoiding the need for GROUP BY. $innerselect = "SELECT DISTINCT {$inneruseralias}.id"; $innerjoins = ["{user} {$inneruseralias}"]; $innerwhere = "WHERE {$inneruseralias}.deleted = 0 AND {$inneruseralias}.id <> :siteguest"; $params = ['siteguest' => $CFG->siteguest]; $outerjoins = ["JOIN {user} u ON u.id = {$usersubqueryalias}.id"]; $wheres = []; if ($this->filterset->has_filter('accesssince')) { $accesssince = $this->filterset->get_filter('accesssince')->current(); // Last access filtering only supports matching or not matching, not any/all/none. $jointypenone = $this->filterset->get_filter('accesssince')::JOINTYPE_NONE; if ($this->filterset->get_filter('accesssince')->get_join_type() === $jointypenone) { $matchaccesssince = true; } } [ // SQL that forms part of the filter. 'sql' => $esql, // SQL for enrolment filtering that must always be applied (eg due to capability restrictions). 'forcedsql' => $esqlforced, 'params' => $eparams, ] = $this->get_enrolled_sql(); $params = array_merge($params, $eparams); // Get the fields for all contexts because there is a special case later where it allows // matches of fields you can't access if they are on your own account. $userfields = fields::for_identity(null)->with_userpic(); ['selects' => $userfieldssql, 'joins' => $userfieldsjoin, 'params' => $userfieldsparams, 'mappings' => $mappings] = (array)$userfields->get_sql('u', true); if ($userfieldsjoin) { $outerjoins[] = $userfieldsjoin; $params = array_merge($params, $userfieldsparams); } // Include any compulsory enrolment SQL (eg capability related filtering that must be applied). if (!empty($esqlforced)) { $outerjoins[] = "JOIN ({$esqlforced}) fef ON fef.id = u.id"; } // Include any enrolment related filtering. if (!empty($esql)) { $outerjoins[] = "LEFT JOIN ({$esql}) ef ON ef.id = u.id"; $wheres[] = 'ef.id IS NOT NULL'; } if ($isfrontpage) { $outerselect = "SELECT u.lastaccess $userfieldssql"; if ($accesssince) { $wheres[] = user_get_user_lastaccess_sql($accesssince, 'u', $matchaccesssince); } } else { $outerselect = "SELECT COALESCE(ul.timeaccess, 0) AS lastaccess $userfieldssql"; // Not everybody has accessed the course yet. $outerjoins[] = 'LEFT JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = :courseid2)'; $params['courseid2'] = $this->course->id; if ($accesssince) { $wheres[] = user_get_course_lastaccess_sql($accesssince, 'ul', $matchaccesssince); } // Make sure we only ever fetch users in the course (regardless of enrolment filters). $innerjoins[] = "JOIN {user_enrolments} ue ON ue.userid = {$inneruseralias}.id"; $innerjoins[] = 'JOIN {enrol} e ON e.id = ue.enrolid AND e.courseid = :courseid1'; $params['courseid1'] = $this->course->id; } // Performance hacks - we preload user contexts together with accounts. $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $ccjoin = 'LEFT JOIN {context} ctx ON (ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel)'; $params['contextlevel'] = CONTEXT_USER; $outerselect .= $ccselect; $outerjoins[] = $ccjoin; // Apply any role filtering. if ($this->filterset->has_filter('roles')) { [ 'where' => $roleswhere, 'params' => $rolesparams, ] = $this->get_roles_sql(); if (!empty($roleswhere)) { $wheres[] = "({$roleswhere})"; } if (!empty($rolesparams)) { $params = array_merge($params, $rolesparams); } } // Apply any country filtering. if ($this->filterset->has_filter('country')) { [ 'where' => $countrywhere, 'params' => $countryparams, ] = $this->get_country_sql(); if (!empty($countrywhere)) { $wheres[] = "($countrywhere)"; } if (!empty($countryparams)) { $params = array_merge($params, $countryparams); } } // Apply any keyword text searches. if ($this->filterset->has_filter('keywords')) { [ 'where' => $keywordswhere, 'params' => $keywordsparams, ] = $this->get_keywords_search_sql($mappings); if (!empty($keywordswhere)) { $wheres[] = "({$keywordswhere})"; } if (!empty($keywordsparams)) { $params = array_merge($params, $keywordsparams); } } // Add any supplied additional forced WHERE clauses. if (!empty($additionalwhere)) { $innerwhere .= " AND ({$additionalwhere})"; $params = array_merge($params, $additionalparams); } // Prepare final values. $outerjoinsstring = implode("\n", $outerjoins); $innerjoinsstring = implode("\n", $innerjoins); if ($wheres) { switch ($this->filterset->get_join_type()) { case $this->filterset::JOINTYPE_ALL: $wherenot = ''; $wheresjoin = ' AND '; break; case $this->filterset::JOINTYPE_NONE: $wherenot = ' NOT '; $wheresjoin = ' AND NOT '; // Some of the $where conditions may begin with `NOT` which results in `AND NOT NOT ...`. // To prevent this from breaking on Oracle the inner WHERE clause is wrapped in brackets, making it // `AND NOT (NOT ...)` which is valid in all DBs. $wheres = array_map(function($where) { return "({$where})"; }, $wheres); break; default: // Default to 'Any' jointype. $wherenot = ''; $wheresjoin = ' OR '; break; } $outerwhere = 'WHERE ' . $wherenot . implode($wheresjoin, $wheres); } else { $outerwhere = ''; } return [ 'subqueryalias' => $usersubqueryalias, 'outerselect' => $outerselect, 'innerselect' => $innerselect, 'outerjoins' => $outerjoinsstring, 'innerjoins' => $innerjoinsstring, 'outerwhere' => $outerwhere, 'innerwhere' => $innerwhere, 'params' => $params, ]; } /** * Prepare SQL and associated parameters for users enrolled in the course. * * @return array SQL query data in the format ['sql' => '', 'forcedsql' => '', 'params' => []]. */ protected function get_enrolled_sql(): array { global $USER; $isfrontpage = ($this->context->instanceid == SITEID); $prefix = 'eu_'; $filteruid = "{$prefix}u.id"; $sql = ''; $joins = []; $wheres = []; $params = []; // It is possible some statements must always be included (in addition to any filtering). $forcedprefix = "f{$prefix}"; $forceduid = "{$forcedprefix}u.id"; $forcedsql = ''; $forcedjoins = []; $forcedwhere = "{$forcedprefix}u.deleted = 0"; if (!$isfrontpage) { // Prepare any enrolment method filtering. [ 'joins' => $methodjoins, 'where' => $wheres[], 'params' => $methodparams, ] = $this->get_enrol_method_sql($filteruid); // Prepare any status filtering. [ 'joins' => $statusjoins, 'where' => $statuswhere, 'params' => $statusparams, 'forcestatus' => $forcestatus, ] = $this->get_status_sql($filteruid, $forceduid, $forcedprefix); if ($forcestatus) { // Force filtering by active participants if user does not have capability to view suspended. $forcedjoins = array_merge($forcedjoins, $statusjoins); $statusjoins = []; $forcedwhere .= " AND ({$statuswhere})"; } else { $wheres[] = $statuswhere; } $joins = array_merge($joins, $methodjoins, $statusjoins); $params = array_merge($params, $methodparams, $statusparams); } $groupids = []; if ($this->filterset->has_filter('groups')) { $groupids = $this->filterset->get_filter('groups')->get_filter_values(); } // Force additional groups filtering if required due to lack of capabilities. // Note: This means results will always be limited to allowed groups, even if the user applies their own groups filtering. $canaccessallgroups = has_capability('moodle/site:accessallgroups', $this->context); $forcegroups = ($this->course->groupmode == SEPARATEGROUPS && !$canaccessallgroups); if ($forcegroups) { $allowedgroupids = array_keys(groups_get_all_groups($this->course->id, $USER->id)); // Users not in any group in a course with separate groups mode should not be able to access the participants filter. if (empty($allowedgroupids)) { // The UI does not support this, so it should not be reachable unless someone is trying to bypass the restriction. throw new \coding_exception('User must be part of a group to filter by participants.'); } $forceduid = "{$forcedprefix}u.id"; $forcedjointype = $this->get_groups_jointype(\core_table\local\filter\filter::JOINTYPE_ANY); $forcedgroupjoin = groups_get_members_join($allowedgroupids, $forceduid, $this->context, $forcedjointype); $forcedjoins[] = $forcedgroupjoin->joins; $forcedwhere .= " AND ({$forcedgroupjoin->wheres})"; $params = array_merge($params, $forcedgroupjoin->params); // Remove any filtered groups the user does not have access to. $groupids = array_intersect($allowedgroupids, $groupids); } // Prepare any user defined groups filtering. if ($groupids) { $groupjoin = groups_get_members_join($groupids, $filteruid, $this->context, $this->get_groups_jointype()); $joins[] = $groupjoin->joins; $params = array_merge($params, $groupjoin->params); if (!empty($groupjoin->wheres)) { $wheres[] = $groupjoin->wheres; } } // Combine the relevant filters and prepare the query. $joins = array_filter($joins); if (!empty($joins)) { $joinsql = implode("\n", $joins); $sql = "SELECT DISTINCT {$prefix}u.id FROM {user} {$prefix}u {$joinsql} WHERE {$prefix}u.deleted = 0"; } $wheres = array_filter($wheres); if (!empty($wheres)) { if ($this->filterset->get_join_type() === $this->filterset::JOINTYPE_ALL) { $wheresql = '(' . implode(') AND (', $wheres) . ')'; } else { $wheresql = '(' . implode(') OR (', $wheres) . ')'; } $sql .= " AND ({$wheresql})"; } // Prepare any SQL that must be applied. if (!empty($forcedjoins)) { $forcedjoinsql = implode("\n", $forcedjoins); $forcedsql = "SELECT DISTINCT {$forcedprefix}u.id FROM {user} {$forcedprefix}u {$forcedjoinsql} WHERE {$forcedwhere}"; } return [ 'sql' => $sql, 'forcedsql' => $forcedsql, 'params' => $params, ]; } /** * Prepare the enrolment methods filter SQL content. * * @param string $useridcolumn User ID column used in the calling query, e.g. u.id * @return array SQL query data in the format ['joins' => [], 'where' => '', 'params' => []]. */ protected function get_enrol_method_sql($useridcolumn): array { global $DB; $prefix = 'ejm_'; $joins = []; $where = ''; $params = []; $enrolids = []; if ($this->filterset->has_filter('enrolments')) { $enrolids = $this->filterset->get_filter('enrolments')->get_filter_values(); } if (!empty($enrolids)) { $jointype = $this->filterset->get_filter('enrolments')->get_join_type(); // Handle 'All' join type. if ($jointype === $this->filterset->get_filter('enrolments')::JOINTYPE_ALL || $jointype === $this->filterset->get_filter('enrolments')::JOINTYPE_NONE) { $allwheres = []; foreach ($enrolids as $i => $enrolid) { $thisprefix = "{$prefix}{$i}"; list($enrolidsql, $enrolidparam) = $DB->get_in_or_equal($enrolid, SQL_PARAMS_NAMED, $thisprefix); $joins[] = "LEFT JOIN {enrol} {$thisprefix}e ON ({$thisprefix}e.id {$enrolidsql} AND {$thisprefix}e.courseid = :{$thisprefix}courseid)"; $joins[] = "LEFT JOIN {user_enrolments} {$thisprefix}ue ON {$thisprefix}ue.userid = {$useridcolumn} AND {$thisprefix}ue.enrolid = {$thisprefix}e.id"; if ($jointype === $this->filterset->get_filter('enrolments')::JOINTYPE_ALL) { $allwheres[] = "{$thisprefix}ue.id IS NOT NULL"; } else { // Ensure participants do not match any of the filtered methods when joining by 'None'. $allwheres[] = "{$thisprefix}ue.id IS NULL"; } $params["{$thisprefix}courseid"] = $this->course->id; $params = array_merge($params, $enrolidparam); } if (!empty($allwheres)) { $where = implode(' AND ', $allwheres); } } else { // Handle the 'Any'join type. list($enrolidssql, $enrolidsparams) = $DB->get_in_or_equal($enrolids, SQL_PARAMS_NAMED, $prefix); $joins[] = "LEFT JOIN {enrol} {$prefix}e ON ({$prefix}e.id {$enrolidssql} AND {$prefix}e.courseid = :{$prefix}courseid)"; $joins[] = "LEFT JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$useridcolumn} AND {$prefix}ue.enrolid = {$prefix}e.id"; $where = "{$prefix}ue.id IS NOT NULL"; $params["{$prefix}courseid"] = $this->course->id; $params = array_merge($params, $enrolidsparams); } } return [ 'joins' => $joins, 'where' => $where, 'params' => $params, ]; } /** * Prepare the status filter SQL content. * Note: Users who cannot view suspended users will always have their results filtered to only show active participants. * * @param string $filteruidcolumn User ID column used in the calling query, e.g. eu_u.id * @param string $forceduidcolumn User ID column used in any forced query, e.g. feu_u.id * @param string $forcedprefix The prefix to use if forced filtering is required * @return array SQL query data in the format ['joins' => [], 'where' => '', 'params' => [], 'forcestatus' => true] */ protected function get_status_sql($filteruidcolumn, $forceduidcolumn, $forcedprefix): array { $prefix = $forcedprefix; $useridcolumn = $forceduidcolumn; $joins = []; $where = ''; $params = []; $forcestatus = true; // By default we filter to show users with active status only. $statusids = [ENROL_USER_ACTIVE]; $statusjointype = $this->filterset::JOINTYPE_DEFAULT; // Allow optional status filtering if the user has relevant capabilities. if (has_capability('moodle/course:enrolreview', $this->context) && (has_capability('moodle/course:viewsuspendedusers', $this->context))) { $forcestatus = false; $prefix = 'ejs_'; $useridcolumn = $filteruidcolumn; // Default to no filtering if capabilities allow for it. $statusids = []; if ($this->filterset->has_filter('status')) { $statusjointype = $this->filterset->get_filter('status')->get_join_type(); $statusfiltervalues = $this->filterset->get_filter('status')->get_filter_values(); // If values are set for the status filter, use them. if (!empty($statusfiltervalues)) { $statusids = $statusfiltervalues; } } } if (!empty($statusids)) { $enroljoin = 'JOIN {enrol} %1$se ON %1$se.id = %1$sue.enrolid AND %1$se.courseid = :%1$scourseid'; $whereactive = '(%1$sue.status = :%2$sactive AND %1$se.status = :%2$senabled AND %1$sue.timestart < :%2$snow1 AND (%1$sue.timeend = 0 OR %1$sue.timeend > :%2$snow2))'; $wheresuspended = '(%1$sue.status = :%2$ssuspended OR %1$se.status != :%2$senabled OR %1$sue.timestart >= :%2$snow1 OR (%1$sue.timeend > 0 AND %1$sue.timeend <= :%2$snow2))'; // Round 'now' time to help DB caching. $now = round(time(), -2); switch ($statusjointype) { case $this->filterset::JOINTYPE_ALL: $joinwheres = []; foreach ($statusids as $i => $statusid) { $joinprefix = "{$prefix}{$i}"; $joins[] = "JOIN {user_enrolments} {$joinprefix}ue ON {$joinprefix}ue.userid = {$useridcolumn}"; if ($statusid === ENROL_USER_ACTIVE) { // Conditions to be met if user filtering by active. $joinwheres[] = sprintf($whereactive, $joinprefix, $joinprefix); $activeparams = [ "{$joinprefix}active" => ENROL_USER_ACTIVE, "{$joinprefix}enabled" => ENROL_INSTANCE_ENABLED, "{$joinprefix}now1" => $now, "{$joinprefix}now2" => $now, "{$joinprefix}courseid" => $this->course->id, ]; $params = array_merge($params, $activeparams); } else { // Conditions to be met if filtering by suspended (currently the only other status). $joinwheres[] = sprintf($wheresuspended, $joinprefix, $joinprefix); $suspendedparams = [ "{$joinprefix}suspended" => ENROL_USER_SUSPENDED, "{$joinprefix}enabled" => ENROL_INSTANCE_ENABLED, "{$joinprefix}now1" => $now, "{$joinprefix}now2" => $now, "{$joinprefix}courseid" => $this->course->id, ]; $params = array_merge($params, $suspendedparams); } $joins[] = sprintf($enroljoin, $joinprefix); } $where = implode(' AND ', $joinwheres); break; case $this->filterset::JOINTYPE_NONE: // Should always be enrolled, just not in any of the filtered statuses. $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$useridcolumn}"; $joins[] = sprintf($enroljoin, $prefix); $joinwheres = []; $params["{$prefix}courseid"] = $this->course->id; foreach ($statusids as $i => $statusid) { $paramprefix = "{$prefix}{$i}"; if ($statusid === ENROL_USER_ACTIVE) { // Conditions to be met if user filtering by active. $joinwheres[] = sprintf("NOT {$whereactive}", $prefix, $paramprefix); $activeparams = [ "{$paramprefix}active" => ENROL_USER_ACTIVE, "{$paramprefix}enabled" => ENROL_INSTANCE_ENABLED, "{$paramprefix}now1" => $now, "{$paramprefix}now2" => $now, ]; $params = array_merge($params, $activeparams); } else { // Conditions to be met if filtering by suspended (currently the only other status). $joinwheres[] = sprintf("NOT {$wheresuspended}", $prefix, $paramprefix); $suspendedparams = [ "{$paramprefix}suspended" => ENROL_USER_SUSPENDED, "{$paramprefix}enabled" => ENROL_INSTANCE_ENABLED, "{$paramprefix}now1" => $now, "{$paramprefix}now2" => $now, ]; $params = array_merge($params, $suspendedparams); } } $where = '(' . implode(' AND ', $joinwheres) . ')'; break; default: // Handle the 'Any' join type. $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$useridcolumn}"; $joins[] = sprintf($enroljoin, $prefix); $joinwheres = []; $params["{$prefix}courseid"] = $this->course->id; foreach ($statusids as $i => $statusid) { $paramprefix = "{$prefix}{$i}"; if ($statusid === ENROL_USER_ACTIVE) { // Conditions to be met if user filtering by active. $joinwheres[] = sprintf($whereactive, $prefix, $paramprefix); $activeparams = [ "{$paramprefix}active" => ENROL_USER_ACTIVE, "{$paramprefix}enabled" => ENROL_INSTANCE_ENABLED, "{$paramprefix}now1" => $now, "{$paramprefix}now2" => $now, ]; $params = array_merge($params, $activeparams); } else { // Conditions to be met if filtering by suspended (currently the only other status). $joinwheres[] = sprintf($wheresuspended, $prefix, $paramprefix); $suspendedparams = [ "{$paramprefix}suspended" => ENROL_USER_SUSPENDED, "{$paramprefix}enabled" => ENROL_INSTANCE_ENABLED, "{$paramprefix}now1" => $now, "{$paramprefix}now2" => $now, ]; $params = array_merge($params, $suspendedparams); } } $where = '(' . implode(' OR ', $joinwheres) . ')'; break; } } return [ 'joins' => $joins, 'where' => $where, 'params' => $params, 'forcestatus' => $forcestatus, ]; } /** * Fetch the groups filter's grouplib jointype, based on its filterset jointype. * This mapping is to ensure compatibility between the two, should their values ever differ. * * @param int|null $forcedjointype If set, specifies the join type to fetch mapping for (used when applying forced filtering). * If null, then user defined filter join type is used. * @return int */ protected function get_groups_jointype(?int $forcedjointype = null): int { // If applying forced groups filter and no manual groups filtering is applied, add an empty filter so we can map the join. if (!is_null($forcedjointype) && !$this->filterset->has_filter('groups')) { $this->filterset->add_filter(new \core_table\local\filter\integer_filter('groups')); } $groupsfilter = $this->filterset->get_filter('groups'); if (is_null($forcedjointype)) { // Fetch join type mapping for a user supplied groups filtering. $filterjointype = $groupsfilter->get_join_type(); } else { // Fetch join type mapping for forced groups filtering. $filterjointype = $forcedjointype; } switch ($filterjointype) { case $groupsfilter::JOINTYPE_NONE: $groupsjoin = GROUPS_JOIN_NONE; break; case $groupsfilter::JOINTYPE_ALL: $groupsjoin = GROUPS_JOIN_ALL; break; default: // Default to ANY jointype. $groupsjoin = GROUPS_JOIN_ANY; break; } return $groupsjoin; } /** * Prepare SQL where clause and associated parameters for any roles filtering being performed. * * @return array SQL query data in the format ['where' => '', 'params' => []]. */ protected function get_roles_sql(): array { global $DB; $where = ''; $params = []; // Limit list to users with some role only. if ($this->filterset->has_filter('roles')) { $rolesfilter = $this->filterset->get_filter('roles'); $roleids = $rolesfilter->get_filter_values(); $jointype = $rolesfilter->get_join_type(); // Determine how to match values in the query. $matchinsql = 'IN'; switch ($jointype) { case $rolesfilter::JOINTYPE_ALL: $wherejoin = ' AND '; break; case $rolesfilter::JOINTYPE_NONE: $wherejoin = ' AND NOT '; $matchinsql = 'NOT IN'; break; default: // Default to 'Any' jointype. $wherejoin = ' OR '; break; } // We want to query both the current context and parent contexts. $rolecontextids = $this->context->get_parent_context_ids(true); // Get users without any role, if needed. if (($withoutkey = array_search(-1, $roleids)) !== false) { list($relatedctxsql1, $norolectxparams) = $DB->get_in_or_equal($rolecontextids, SQL_PARAMS_NAMED, 'relatedctx'); if ($jointype === $rolesfilter::JOINTYPE_NONE) { $where .= "(u.id IN (SELECT userid FROM {role_assignments} WHERE contextid {$relatedctxsql1}))"; } else { $where .= "(u.id NOT IN (SELECT userid FROM {role_assignments} WHERE contextid {$relatedctxsql1}))"; } $params = array_merge($params, $norolectxparams); if ($withoutkey !== false) { unset($roleids[$withoutkey]); } // Join if any roles will be included. if (!empty($roleids)) { // The NOT case is replaced with AND to prevent a double negative. $where .= $jointype === $rolesfilter::JOINTYPE_NONE ? ' AND ' : $wherejoin; } } // Get users with specified roles, if needed. if (!empty($roleids)) { // All case - need one WHERE per filtered role. if ($rolesfilter::JOINTYPE_ALL === $jointype) { $numroles = count($roleids); $rolecount = 1; foreach ($roleids as $roleid) { list($relatedctxsql, $relctxparams) = $DB->get_in_or_equal($rolecontextids, SQL_PARAMS_NAMED, 'relatedctx'); list($roleidssql, $roleidparams) = $DB->get_in_or_equal($roleid, SQL_PARAMS_NAMED, 'roleids'); $where .= "(u.id IN ( SELECT userid FROM {role_assignments} WHERE roleid {$roleidssql} AND contextid {$relatedctxsql}) )"; if ($rolecount < $numroles) { $where .= $wherejoin; $rolecount++; } $params = array_merge($params, $roleidparams, $relctxparams); } } else { // Any / None cases - need one WHERE to cover all filtered roles. list($relatedctxsql, $relctxparams) = $DB->get_in_or_equal($rolecontextids, SQL_PARAMS_NAMED, 'relatedctx'); list($roleidssql, $roleidsparams) = $DB->get_in_or_equal($roleids, SQL_PARAMS_NAMED, 'roleids'); $where .= "(u.id {$matchinsql} ( SELECT userid FROM {role_assignments} WHERE roleid {$roleidssql} AND contextid {$relatedctxsql}) )"; $params = array_merge($params, $roleidsparams, $relctxparams); } } } return [ 'where' => $where, 'params' => $params, ]; } /** * Prepare SQL where clause and associated parameters for country filtering * * @return array SQL query data in the format ['where' => '', 'params' => []]. */ protected function get_country_sql(): array { global $DB; $where = ''; $params = []; $countryfilter = $this->filterset->get_filter('country'); if ($countrycodes = $countryfilter->get_filter_values()) { // If filter type is "None", then we negate the comparison. [$countrywhere, $params] = $DB->get_in_or_equal($countrycodes, SQL_PARAMS_NAMED, 'country', $countryfilter->get_join_type() !== $countryfilter::JOINTYPE_NONE); $where = "(u.country {$countrywhere})"; } return [ 'where' => $where, 'params' => $params, ]; } /** * Prepare SQL where clause and associated parameters for any keyword searches being performed. * * @param array $mappings Array of field mappings (fieldname => SQL code for the value) * @return array SQL query data in the format ['where' => '', 'params' => []]. */ protected function get_keywords_search_sql(array $mappings): array { global $CFG, $DB, $USER; $keywords = []; $where = ''; $params = []; $keywordsfilter = $this->filterset->get_filter('keywords'); $jointype = $keywordsfilter->get_join_type(); // None join types in both filter row and filterset require additional 'not null' handling for accurate keywords matches. $notjoin = false; // Determine how to match values in the query. switch ($jointype) { case $keywordsfilter::JOINTYPE_ALL: $wherejoin = ' AND '; break; case $keywordsfilter::JOINTYPE_NONE: $wherejoin = ' AND NOT '; $notjoin = true; break; default: // Default to 'Any' jointype. $wherejoin = ' OR '; break; } // Handle filterset None join type. if ($this->filterset->get_join_type() === $this->filterset::JOINTYPE_NONE) { $notjoin = true; } if ($this->filterset->has_filter('keywords')) { $keywords = $keywordsfilter->get_filter_values(); } $canviewfullnames = has_capability('moodle/site:viewfullnames', $this->context); foreach ($keywords as $index => $keyword) { $searchkey1 = 'search' . $index . '1'; $searchkey2 = 'search' . $index . '2'; $searchkey3 = 'search' . $index . '3'; $searchkey4 = 'search' . $index . '4'; $searchkey5 = 'search' . $index . '5'; $searchkey6 = 'search' . $index . '6'; $searchkey7 = 'search' . $index . '7'; $conditions = []; // Search by fullname. [$fullname, $fullnameparams] = fields::get_sql_fullname('u', $canviewfullnames); $conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false); $params = array_merge($params, $fullnameparams); // Search by email. $email = $DB->sql_like('email', ':' . $searchkey2, false, false); if ($notjoin) { $email = "(email IS NOT NULL AND {$email})"; } if (!in_array('email', $this->userfields)) { $maildisplay = 'maildisplay' . $index; $userid1 = 'userid' . $index . '1'; // Prevent users who hide their email address from being found by others // who aren't allowed to see hidden email addresses. $email = "(". $email ." AND (" . "u.maildisplay <> :$maildisplay " . "OR u.id = :$userid1". // Users can always find themselves. "))"; $params[$maildisplay] = core_user::MAILDISPLAY_HIDE; $params[$userid1] = $USER->id; } $conditions[] = $email; // Search by idnumber. $idnumber = $DB->sql_like('idnumber', ':' . $searchkey3, false, false); if ($notjoin) { $idnumber = "(idnumber IS NOT NULL AND {$idnumber})"; } if (!in_array('idnumber', $this->userfields)) { $userid2 = 'userid' . $index . '2'; // Users who aren't allowed to see idnumbers should at most find themselves // when searching for an idnumber. $idnumber = "(". $idnumber . " AND u.id = :$userid2)"; $params[$userid2] = $USER->id; } $conditions[] = $idnumber; // Search all user identify fields. $extrasearchfields = fields::get_identity_fields(null); foreach ($extrasearchfields as $fieldindex => $extrasearchfield) { if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) { // Already covered above. continue; } // The param must be short (max 32 characters) so don't include field name. $param = $searchkey3 . '_ident' . $fieldindex; $fieldsql = $mappings[$extrasearchfield]; $condition = $DB->sql_like($fieldsql, ':' . $param, false, false); $params[$param] = "%$keyword%"; if ($notjoin) { $condition = "($fieldsql IS NOT NULL AND {$condition})"; } if (!in_array($extrasearchfield, $this->userfields)) { // User cannot see this field, but allow match if their own account. $userid3 = 'userid' . $index . '3_ident' . $fieldindex; $condition = "(". $condition . " AND u.id = :$userid3)"; $params[$userid3] = $USER->id; } $conditions[] = $condition; } // Search by middlename. $middlename = $DB->sql_like('middlename', ':' . $searchkey4, false, false); if ($notjoin) { $middlename = "(middlename IS NOT NULL AND {$middlename})"; } $conditions[] = $middlename; // Search by alternatename. $alternatename = $DB->sql_like('alternatename', ':' . $searchkey5, false, false); if ($notjoin) { $alternatename = "(alternatename IS NOT NULL AND {$alternatename})"; } $conditions[] = $alternatename; // Search by firstnamephonetic. $firstnamephonetic = $DB->sql_like('firstnamephonetic', ':' . $searchkey6, false, false); if ($notjoin) { $firstnamephonetic = "(firstnamephonetic IS NOT NULL AND {$firstnamephonetic})"; } $conditions[] = $firstnamephonetic; // Search by lastnamephonetic. $lastnamephonetic = $DB->sql_like('lastnamephonetic', ':' . $searchkey7, false, false); if ($notjoin) { $lastnamephonetic = "(lastnamephonetic IS NOT NULL AND {$lastnamephonetic})"; } $conditions[] = $lastnamephonetic; if (!empty($where)) { $where .= $wherejoin; } else if ($jointype === $keywordsfilter::JOINTYPE_NONE) { // Join type 'None' requires the WHERE to begin with NOT. $where .= ' NOT '; } $where .= "(". implode(" OR ", $conditions) .") "; $params[$searchkey1] = "%$keyword%"; $params[$searchkey2] = "%$keyword%"; $params[$searchkey3] = "%$keyword%"; $params[$searchkey4] = "%$keyword%"; $params[$searchkey5] = "%$keyword%"; $params[$searchkey6] = "%$keyword%"; $params[$searchkey7] = "%$keyword%"; } return [ 'where' => $where, 'params' => $params, ]; } } classes/table/participants_filterset.php 0000644 00000004331 15151162244 0014565 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Participants table filterset. * * @package core * @category table * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace core_user\table; use core_table\local\filter\filterset; use core_table\local\filter\integer_filter; use core_table\local\filter\string_filter; /** * Participants table filterset. * * @package core * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class participants_filterset extends filterset { /** * Get the required filters. * * The only required filter is the courseid filter. * * @return array. */ public function get_required_filters(): array { return [ 'courseid' => integer_filter::class, ]; } /** * Get the optional filters. * * These are: * - accesssince; * - enrolments; * - groups; * - keywords; * - country; * - roles; and * - status. * * @return array */ public function get_optional_filters(): array { return [ 'accesssince' => integer_filter::class, 'enrolments' => integer_filter::class, 'groups' => integer_filter::class, 'keywords' => string_filter::class, 'country' => string_filter::class, 'roles' => integer_filter::class, 'status' => integer_filter::class, ]; } } classes/search/user.php 0000644 00000015454 15151162244 0011147 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Search area for Users for whom I have authority to view profile. * * @package core_user * @copyright 2016 Devang Gaur {@link http://www.devanggaur.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\search; require_once($CFG->dirroot . '/user/lib.php'); defined('MOODLE_INTERNAL') || die(); /** * Search area for Users for whom I have access to view profile. * * @package core_user * @copyright 2016 Devang Gaur {@link http://www.devanggaur.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user extends \core_search\base { /** * Returns recordset containing required data attributes for indexing. * * @param number $modifiedfrom * @param \context|null $context Optional context to restrict scope of returned results * @return \moodle_recordset|null Recordset (or null if no results) */ public function get_document_recordset($modifiedfrom = 0, \context $context = null) { global $DB; // Prepare query conditions. $where = 'timemodified >= ? AND deleted = ? AND confirmed = ?'; $params = [$modifiedfrom, 0, 1]; // Handle context types. if (!$context) { $context = \context_system::instance(); } switch ($context->contextlevel) { case CONTEXT_MODULE: case CONTEXT_BLOCK: case CONTEXT_COURSE: case CONTEXT_COURSECAT: // These contexts cannot contain any users. return null; case CONTEXT_USER: // Restrict to specific user. $where .= ' AND id = ?'; $params[] = $context->instanceid; break; case CONTEXT_SYSTEM: break; default: throw new \coding_exception('Unexpected contextlevel: ' . $context->contextlevel); } return $DB->get_recordset_select('user', $where, $params); } /** * Returns document instances for each record in the recordset. * * @param \stdClass $record * @param array $options * @return \core_search\document */ public function get_document($record, $options = array()) { $context = \context_system::instance(); // Prepare associative array with data from DB. $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname); // Include all alternate names in title. $array = []; foreach (\core_user\fields::get_name_fields(true) as $field) { $array[$field] = $record->$field; } $fullusername = join(' ', $array); // Assigning properties to our document. $doc->set('title', content_to_text($fullusername, false)); $doc->set('contextid', $context->id); $doc->set('courseid', SITEID); $doc->set('itemid', $record->id); $doc->set('modified', $record->timemodified); $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('content', content_to_text($record->description, $record->descriptionformat)); // Check if this document should be considered new. if (isset($options['lastindexedtime']) && $options['lastindexedtime'] < $record->timecreated) { // If the document was created after the last index time, it must be new. $doc->set_is_new(true); } return $doc; } /** * Returns the user fullname to display as document title * * @param \core_search\document $doc * @return string User fullname */ public function get_document_display_title(\core_search\document $doc) { $user = \core_user::get_user($doc->get('itemid')); return fullname($user); } /** * Checking whether I can access a document * * @param int $id user id * @return int */ public function check_access($id) { global $DB, $USER; $user = $DB->get_record('user', array('id' => $id)); if (!$user || $user->deleted) { return \core_search\manager::ACCESS_DELETED; } if (user_can_view_profile($user)) { return \core_search\manager::ACCESS_GRANTED; } return \core_search\manager::ACCESS_DENIED; } /** * Returns a url to the profile page of user. * * @param \core_search\document $doc * @return \moodle_url */ public function get_doc_url(\core_search\document $doc) { return $this->get_context_url($doc); } /** * Returns a url to the document context. * * @param \core_search\document $doc * @return \moodle_url */ public function get_context_url(\core_search\document $doc) { return new \moodle_url('/user/profile.php', array('id' => $doc->get('itemid'))); } /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } /** * Return the context info required to index files for * this search area. * * Should be onerridden by each search area. * * @return array */ public function get_search_fileareas() { $fileareas = array( 'profile' // Fileareas. ); return $fileareas; } /** * Returns the moodle component name. * * It might be the plugin name (whole frankenstyle name) or the core subsystem name. * * @return string */ public function get_component_name() { return 'user'; } /** * Returns an icon instance for the document. * * @param \core_search\document $doc * * @return \core_search\document_icon */ public function get_doc_icon(\core_search\document $doc) : \core_search\document_icon { return new \core_search\document_icon('i/user'); } /** * Returns a list of category names associated with the area. * * @return array */ public function get_category_names() { return [\core_search\manager::SEARCH_AREA_CATEGORY_USERS]; } } classes/search/course_teacher.php 0000644 00000015751 15151162244 0013164 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Index teachers in a course * * @package core_user * @author Nathan Nguyen <nathannguyen@catalyst-au.net> * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\search; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/user/lib.php'); /** * Search for user role assignment in a course * * @package core_user * @author Nathan Nguyen <nathannguyen@catalyst-au.net> * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_teacher extends \core_search\base { /** * The context levels the search implementation is working on. * * @var array */ protected static $levels = [CONTEXT_COURSE]; /** * Returns the moodle component name. * * It might be the plugin name (whole frankenstyle name) or the core subsystem name. * * @return string */ public function get_component_name() { return 'course_teacher'; } /** * Returns recordset containing required data attributes for indexing. * * @param number $modifiedfrom * @param \context|null $context Optional context to restrict scope of returned results * @return \moodle_recordset|null Recordset (or null if no results) */ public function get_document_recordset($modifiedfrom = 0, \context $context = null) { global $DB; $teacherroleids = get_config('core', 'searchteacherroles'); // Only index teacher roles. if (!empty($teacherroleids)) { $teacherroleids = explode(',', $teacherroleids); list($insql, $inparams) = $DB->get_in_or_equal($teacherroleids, SQL_PARAMS_NAMED); } else { // Do not index at all. list($insql, $inparams) = [' = :roleid', ['roleid' => 0]]; } $params = [ 'coursecontext' => CONTEXT_COURSE, 'modifiedfrom' => $modifiedfrom ]; $params = array_merge($params, $inparams); $recordset = $DB->get_recordset_sql(" SELECT u.*, ra.contextid, r.shortname as roleshortname, ra.id as itemid, ra.timemodified as timeassigned FROM {role_assignments} ra JOIN {context} ctx ON ctx.id = ra.contextid AND ctx.contextlevel = :coursecontext JOIN {user} u ON u.id = ra.userid JOIN {role} r ON r.id = ra.roleid WHERE ra.timemodified >= :modifiedfrom AND r.id $insql ORDER BY ra.timemodified ASC", $params); return $recordset; } /** * Returns document instances for each record in the recordset. * * @param \stdClass $record * @param array $options * @return \core_search\document */ public function get_document($record, $options = array()) { $context = \context::instance_by_id($record->contextid); // Content. if ($context->contextlevel == CONTEXT_COURSE) { $course = get_course($context->instanceid); $contentdata = new \stdClass(); $contentdata->role = ucfirst($record->roleshortname); $contentdata->course = $course->fullname; $content = get_string('content:courserole', 'core_search', $contentdata); } else { return false; } $doc = \core_search\document_factory::instance($record->itemid, $this->componentname, $this->areaname); // Assigning properties to our document. $doc->set('title', content_to_text(fullname($record), false)); $doc->set('contextid', $context->id); $doc->set('courseid', $context->instanceid); $doc->set('itemid', $record->itemid); $doc->set('modified', $record->timeassigned); $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('userid', $record->id); $doc->set('content', $content); // Check if this document should be considered new. if (isset($options['lastindexedtime']) && $options['lastindexedtime'] < $record->timeassigned) { $doc->set_is_new(true); } return $doc; } /** * Checking whether I can access a document * * @param int $id user id * @return int */ public function check_access($id) { $user = $this->get_user($id); if (!$user || $user->deleted) { return \core_search\manager::ACCESS_DELETED; } if (user_can_view_profile($user)) { return \core_search\manager::ACCESS_GRANTED; } return \core_search\manager::ACCESS_DENIED; } /** * Returns a url to the document context. * * @param \core_search\document $doc * @return \moodle_url */ public function get_context_url(\core_search\document $doc) { $user = $this->get_user($doc->get('itemid')); $courseid = $doc->get('courseid'); return new \moodle_url('/user/view.php', array('id' => $user->id, 'course' => $courseid)); } /** * Returns the user fullname to display as document title * * @param \core_search\document $doc * @return string User fullname */ public function get_document_display_title(\core_search\document $doc) { $user = $this->get_user($doc->get('itemid')); return fullname($user); } /** * Get user based on role assignment id * * @param int $itemid role assignment id * @return mixed */ private function get_user($itemid) { global $DB; $sql = "SELECT u.* FROM {user} u JOIN {role_assignments} ra ON ra.userid = u.id WHERE ra.id = :raid"; return $DB->get_record_sql($sql, array('raid' => $itemid)); } /** * Returns a list of category names associated with the area. * * @return array */ public function get_category_names() { return [\core_search\manager::SEARCH_AREA_CATEGORY_ALL, \core_search\manager::SEARCH_AREA_CATEGORY_USERS]; } /** * Link to the teacher in the course * * @param \core_search\document $doc the document * @return \moodle_url */ public function get_doc_url(\core_search\document $doc) { return $this->get_context_url($doc); } } classes/fields.php 0000644 00000072060 15151162244 0010166 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_user; use core_text; /** * Class for retrieving information about user fields that are needed for displaying user identity. * * @package core_user */ class fields { /** @var string Prefix used to identify custom profile fields */ const PROFILE_FIELD_PREFIX = 'profile_field_'; /** @var string Regular expression used to match a field name against the prefix */ const PROFILE_FIELD_REGEX = '~^' . self::PROFILE_FIELD_PREFIX . '(.*)$~'; /** @var int All fields required to display user's identity, based on server configuration */ const PURPOSE_IDENTITY = 0; /** @var int All fields required to display a user picture */ const PURPOSE_USERPIC = 1; /** @var int All fields required for somebody's name */ const PURPOSE_NAME = 2; /** @var int Field required by custom include list */ const CUSTOM_INCLUDE = 3; /** @var \context|null Context in use */ protected $context; /** @var bool True to allow custom user fields */ protected $allowcustom; /** @var bool[] Array of purposes (from PURPOSE_xx to true/false) */ protected $purposes; /** @var string[] List of extra fields to include */ protected $include; /** @var string[] List of fields to exclude */ protected $exclude; /** @var int Unique identifier for different queries generated in same request */ protected static $uniqueidentifier = 1; /** @var array|null Associative array from field => array of purposes it was used for => true */ protected $fields = null; /** * Protected constructor - use one of the for_xx methods to create an object. * * @param int $purpose Initial purpose for object or -1 for none */ protected function __construct(int $purpose = -1) { $this->purposes = [ self::PURPOSE_IDENTITY => false, self::PURPOSE_USERPIC => false, self::PURPOSE_NAME => false, ]; if ($purpose != -1) { $this->purposes[$purpose] = true; } $this->include = []; $this->exclude = []; $this->context = null; $this->allowcustom = true; } /** * Constructs an empty user fields object to get arbitrary user fields. * * You can add fields to retrieve with the including() function. * * @return fields User fields object ready for use */ public static function empty(): fields { return new fields(); } /** * Constructs a user fields object to get identity information for display. * * The function does all the required capability checks to see if the current user is allowed * to see them in the specified context. You can pass context null to get all the fields without * checking permissions. * * If the code can only handle fields in the main user table, and not custom profile fields, * then set $allowcustom to false. * * Note: After constructing the object you can use the ->with_xx, ->including, and ->excluding * functions to control the required fields in more detail. For example: * * $fields = fields::for_identity($context)->with_userpic()->excluding('email'); * * @param \context|null $context Context; if supplied, includes only fields the current user should see * @param bool $allowcustom If true, custom profile fields may be included * @return fields User fields object ready for use */ public static function for_identity(?\context $context, bool $allowcustom = true): fields { $fields = new fields(self::PURPOSE_IDENTITY); $fields->context = $context; $fields->allowcustom = $allowcustom; return $fields; } /** * Constructs a user fields object to get information required for displaying a user picture. * * Note: After constructing the object you can use the ->with_xx, ->including, and ->excluding * functions to control the required fields in more detail. For example: * * $fields = fields::for_userpic()->with_name()->excluding('email'); * * @return fields User fields object ready for use */ public static function for_userpic(): fields { return new fields(self::PURPOSE_USERPIC); } /** * Constructs a user fields object to get information required for displaying a user full name. * * Note: After constructing the object you can use the ->with_xx, ->including, and ->excluding * functions to control the required fields in more detail. For example: * * $fields = fields::for_name()->with_userpic()->excluding('email'); * * @return fields User fields object ready for use */ public static function for_name(): fields { return new fields(self::PURPOSE_NAME); } /** * On an existing fields object, adds the fields required for displaying user pictures. * * @return $this Same object for chaining function calls */ public function with_userpic(): fields { $this->purposes[self::PURPOSE_USERPIC] = true; return $this; } /** * On an existing fields object, adds the fields required for displaying user full names. * * @return $this Same object for chaining function calls */ public function with_name(): fields { $this->purposes[self::PURPOSE_NAME] = true; return $this; } /** * On an existing fields object, adds the fields required for displaying user identity. * * The function does all the required capability checks to see if the current user is allowed * to see them in the specified context. You can pass context null to get all the fields without * checking permissions. * * If the code can only handle fields in the main user table, and not custom profile fields, * then set $allowcustom to false. * * @param \context|null Context; if supplied, includes only fields the current user should see * @param bool $allowcustom If true, custom profile fields may be included * @return $this Same object for chaining function calls */ public function with_identity(?\context $context, bool $allowcustom = true): fields { $this->context = $context; $this->allowcustom = $allowcustom; $this->purposes[self::PURPOSE_IDENTITY] = true; return $this; } /** * On an existing fields object, adds extra fields to be retrieved. You can specify either * fields from the user table e.g. 'email', or profile fields e.g. 'profile_field_height'. * * @param string ...$include One or more fields to add * @return $this Same object for chaining function calls */ public function including(string ...$include): fields { $this->include = array_merge($this->include, $include); return $this; } /** * On an existing fields object, excludes fields from retrieval. You can specify either * fields from the user table e.g. 'email', or profile fields e.g. 'profile_field_height'. * * This is useful when constructing queries where your query already explicitly references * certain fields, so you don't want to retrieve them twice. * * @param string ...$exclude One or more fields to exclude * @return $this Same object for chaining function calls */ public function excluding(...$exclude): fields { $this->exclude = array_merge($this->exclude, $exclude); return $this; } /** * Gets an array of all fields that are required for the specified purposes, also taking * into account the $includes and $excludes settings. * * The results may include basic field names (columns from the 'user' database table) and, * unless turned off, custom profile field names in the format 'profile_field_myfield'. * * You should not rely on the order of fields, with one exception: if there is an id field * it will be returned first. This is in case it is used with get_records calls. * * The $limitpurposes parameter is useful if you want to get a different set of fields than the * purposes in the constructor. For example, if you want to get SQL for identity + user picture * fields, but you then want to only get the identity fields as a list. (You can only specify * purposes that were also passed to the constructor i.e. it can only be used to restrict the * list, not add to it.) * * @param array $limitpurposes If specified, gets fields only for these purposes * @return string[] Array of required fields * @throws \coding_exception If any unknown purpose is listed */ public function get_required_fields(array $limitpurposes = []): array { // The first time this is called, actually work out the list. There is no way to 'un-cache' // it, but these objects are designed to be short-lived so it doesn't need one. if ($this->fields === null) { // Add all the fields as array keys so that there are no duplicates. $this->fields = []; if ($this->purposes[self::PURPOSE_IDENTITY]) { foreach (self::get_identity_fields($this->context, $this->allowcustom) as $field) { $this->fields[$field] = [self::PURPOSE_IDENTITY => true]; } } if ($this->purposes[self::PURPOSE_USERPIC]) { foreach (self::get_picture_fields() as $field) { if (!array_key_exists($field, $this->fields)) { $this->fields[$field] = []; } $this->fields[$field][self::PURPOSE_USERPIC] = true; } } if ($this->purposes[self::PURPOSE_NAME]) { foreach (self::get_name_fields() as $field) { if (!array_key_exists($field, $this->fields)) { $this->fields[$field] = []; } $this->fields[$field][self::PURPOSE_NAME] = true; } } foreach ($this->include as $field) { if ($this->allowcustom || !preg_match(self::PROFILE_FIELD_REGEX, $field)) { if (!array_key_exists($field, $this->fields)) { $this->fields[$field] = []; } $this->fields[$field][self::CUSTOM_INCLUDE] = true; } } foreach ($this->exclude as $field) { unset($this->fields[$field]); } // If the id field is included, make sure it's first in the list. if (array_key_exists('id', $this->fields)) { $newfields = ['id' => $this->fields['id']]; foreach ($this->fields as $field => $purposes) { if ($field !== 'id') { $newfields[$field] = $purposes; } } $this->fields = $newfields; } } if ($limitpurposes) { // Check the value was legitimate. foreach ($limitpurposes as $purpose) { if ($purpose != self::CUSTOM_INCLUDE && empty($this->purposes[$purpose])) { throw new \coding_exception('$limitpurposes can only include purposes defined in object'); } } // Filter the fields to include only those matching the purposes. $result = []; foreach ($this->fields as $key => $purposes) { foreach ($limitpurposes as $purpose) { if (array_key_exists($purpose, $purposes)) { $result[] = $key; break; } } } return $result; } else { return array_keys($this->fields); } } /** * Gets fields required for user pictures. * * The results include only basic field names (columns from the 'user' database table). * * @return string[] All fields required for user pictures */ public static function get_picture_fields(): array { return ['id', 'picture', 'firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename', 'alternatename', 'imagealt', 'email']; } /** * Gets fields required for user names. * * The results include only basic field names (columns from the 'user' database table). * * Fields are usually returned in a specific order, which the fullname() function depends on. * If you specify 'true' to the $strangeorder flag, then the firstname and lastname fields * are moved to the front; this is useful in a few places in existing code. New code should * avoid requiring a particular order. * * @param bool $differentorder In a few places, a different order of fields is required * @return string[] All fields used to display user names */ public static function get_name_fields(bool $differentorder = false): array { $fields = ['firstnamephonetic', 'lastnamephonetic', 'middlename', 'alternatename', 'firstname', 'lastname']; if ($differentorder) { return array_merge(array_slice($fields, -2), array_slice($fields, 0, -2)); } else { return $fields; } } /** * Gets all fields required for user identity. These fields should be included in tables * showing lists of users (in addition to the user's name which is included as standard). * * The results include basic field names (columns from the 'user' database table) and, unless * turned off, custom profile field names in the format 'profile_field_myfield', note these * fields will always be returned lower cased to match how they are returned by the DML library. * * This function does all the required capability checks to see if the current user is allowed * to see them in the specified context. You can pass context null to get all the fields * without checking permissions. * * @param \context|null $context Context; if not supplied, all fields will be included without checks * @param bool $allowcustom If true, custom profile fields will be included * @return string[] Array of required fields * @throws \coding_exception */ public static function get_identity_fields(?\context $context, bool $allowcustom = true): array { global $CFG; // Only users with permission get the extra fields. if ($context && !has_capability('moodle/site:viewuseridentity', $context)) { return []; } // Split showuseridentity on comma (filter needed in case the showuseridentity is empty). $extra = array_filter(explode(',', $CFG->showuseridentity)); // If there are any custom fields, remove them if necessary (either if allowcustom is false, // or if the user doesn't have access to see them). foreach ($extra as $key => $field) { if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) { $allowed = false; if ($allowcustom) { require_once($CFG->dirroot . '/user/profile/lib.php'); $field = profile_get_custom_field_data_by_shortname($matches[1], false); $fieldinstance = profile_get_user_field($field->datatype, $field->id, 0, $field); $allowed = $fieldinstance->is_visible($context); } if (!$allowed) { unset($extra[$key]); } } } // For standard user fields, access is controlled by the hiddenuserfields option and // some different capabilities. Check and remove these if the user can't access them. $hiddenfields = array_filter(explode(',', $CFG->hiddenuserfields)); $hiddenidentifiers = array_intersect($extra, $hiddenfields); if ($hiddenidentifiers) { if (!$context) { $canviewhiddenuserfields = true; } else if ($context->get_course_context(false)) { // We are somewhere inside a course. $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context); } else { // We are not inside a course. $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context); } if (!$canviewhiddenuserfields) { // Remove hidden identifiers from the list. $extra = array_diff($extra, $hiddenidentifiers); } } // Re-index the entries and return. $extra = array_values($extra); return array_map([core_text::class, 'strtolower'], $extra); } /** * Gets SQL that can be used in a query to get the necessary fields. * * The result of this function is an object with fields 'selects', 'joins', 'params', and * 'mappings'. * * If not empty, the list of selects will begin with a comma and the list of joins will begin * and end with a space. You can include the result in your existing query like this: * * SELECT (your existing fields) * $selects * FROM {user} u * JOIN (your existing joins) * $joins * * When there are no custom fields then the 'joins' result will always be an empty string, and * 'params' will be an empty array. * * The $fieldmappings value is often not needed. It is an associative array from each field * name to an SQL expression for the value of that field, e.g.: * 'profile_field_frog' => 'uf1d_3.data' * 'city' => 'u.city' * This is helpful if you want to use the profile fields in a WHERE clause, becuase you can't * refer to the aliases used in the SELECT list there. * * The leading comma is included because this makes it work in the pattern above even if there * are no fields from the get_sql() data (which can happen if doing identity fields and none * are selected). If you want the result without a leading comma, set $leadingcomma to false. * * If the 'id' field is included then it will always be first in the list. Otherwise, you * should not rely on the field order. * * For identity fields, the function does all the required capability checks to see if the * current user is allowed to see them in the specified context. You can pass context null * to get all the fields without checking permissions. * * If your code for any reason cannot cope with custom fields then you can turn them off. * * You can have either named or ? params. If you use named params, they are of the form * uf1s_2; the first number increments in each call using a static variable in this class and * the second number refers to the field being queried. A similar pattern is used to make * join aliases unique. * * If your query refers to the user table by an alias e.g. 'u' then specify this in the $alias * parameter; otherwise it will use {user} (if there are any joins for custom profile fields) * or simply refer to the field by name only (if there aren't). * * If you need to use a prefix on the field names (for example in case they might coincide with * existing result columns from your query, or if you want a convenient way to split out all * the user data into a separate object) then you can specify one here. For example, if you * include name fields and the prefix is 'u_' then the results will include 'u_firstname'. * * If you don't want to prefix all the field names but only change the id field name, use * the $renameid parameter. (When you use this parameter, it takes precedence over any prefix; * the id field will not be prefixed, while all others will.) * * @param string $alias Optional (but recommended) alias for user table in query, e.g. 'u' * @param bool $namedparams If true, uses named :parameters instead of indexed ? parameters * @param string $prefix Optional prefix for all field names in result, e.g. 'u_' * @param string $renameid Renames the 'id' field if specified, e.g. 'userid' * @param bool $leadingcomma If true the 'selects' list will start with a comma * @return \stdClass Object with necessary SQL components */ public function get_sql(string $alias = '', bool $namedparams = false, string $prefix = '', string $renameid = '', bool $leadingcomma = true): \stdClass { global $DB; $fields = $this->get_required_fields(); $selects = ''; $joins = ''; $params = []; $mappings = []; $unique = self::$uniqueidentifier++; $fieldcount = 0; if ($alias) { $usertable = $alias . '.'; } else { // If there is no alias, we still need to use {user} to identify the table when there // are joins with other tables. When there are no customfields then there are no joins // so we can refer to the fields by name alone. $gotcustomfields = false; foreach ($fields as $field) { if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) { $gotcustomfields = true; break; } } if ($gotcustomfields) { $usertable = '{user}.'; } else { $usertable = ''; } } foreach ($fields as $field) { if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) { // Custom profile field. $shortname = $matches[1]; $fieldcount++; $fieldalias = 'uf' . $unique . 'f_' . $fieldcount; $dataalias = 'uf' . $unique . 'd_' . $fieldcount; if ($namedparams) { $withoutcolon = 'uf' . $unique . 's' . $fieldcount; $placeholder = ':' . $withoutcolon; $params[$withoutcolon] = $shortname; } else { $placeholder = '?'; $params[] = $shortname; } $joins .= " JOIN {user_info_field} $fieldalias ON " . $DB->sql_equal($fieldalias . '.shortname', $placeholder, false) . " LEFT JOIN {user_info_data} $dataalias ON $dataalias.fieldid = $fieldalias.id AND $dataalias.userid = {$usertable}id"; // For Oracle we need to convert the field into a usable format. $fieldsql = $DB->sql_compare_text($dataalias . '.data', 255); $selects .= ", $fieldsql AS $prefix$field"; $mappings[$field] = $fieldsql; } else { // Standard user table field. $selects .= ", $usertable$field"; if ($field === 'id' && $renameid && $renameid !== 'id') { $selects .= " AS $renameid"; } else if ($prefix) { $selects .= " AS $prefix$field"; } $mappings[$field] = "$usertable$field"; } } // Add a space to the end of the joins list; this means it can be appended directly into // any existing query without worrying about whether the developer has remembered to add // whitespace after it. if ($joins) { $joins .= ' '; } // Optionally remove the leading comma. if (!$leadingcomma) { $selects = ltrim($selects, ' ,'); } return (object)['selects' => $selects, 'joins' => $joins, 'params' => $params, 'mappings' => $mappings]; } /** * Similar to {@see \moodle_database::sql_fullname} except it returns all user name fields as defined by site config, in a * single select statement suitable for inclusion in a query/filter for a users fullname, e.g. * * [$select, $params] = fields::get_sql_fullname('u'); * $users = $DB->get_records_sql_menu("SELECT u.id, {$select} FROM {user} u", $params); * * @param string|null $tablealias User table alias, if set elsewhere in the query, null if not required * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used * @return array SQL select snippet and parameters */ public static function get_sql_fullname(?string $tablealias = 'u', bool $override = false): array { global $DB; $unique = self::$uniqueidentifier++; $namefields = self::get_name_fields(); // Create a dummy user object containing all name fields. $dummyuser = (object) array_combine($namefields, $namefields); $dummyfullname = fullname($dummyuser, $override); // Extract any name fields from the fullname format in the order that they appear. $matchednames = array_values(order_in_string($namefields, $dummyfullname)); $namelookup = $namepattern = $elements = $params = []; foreach ($namefields as $index => $namefield) { $namefieldwithalias = $tablealias ? "{$tablealias}.{$namefield}" : $namefield; // Coalesce the name fields to ensure we don't return null. $emptyparam = "uf{$unique}ep_{$index}"; $namelookup[$namefield] = "COALESCE({$namefieldwithalias}, :{$emptyparam})"; $params[$emptyparam] = ''; $namepattern[] = '\b' . preg_quote($namefield) . '\b'; } // Grab any content between the name fields, inserting them after each name field. $chunks = preg_split('/(' . implode('|', $namepattern) . ')/', $dummyfullname); foreach ($chunks as $index => $chunk) { if ($index > 0) { $elements[] = $namelookup[$matchednames[$index - 1]]; } if (core_text::strlen($chunk) > 0) { // If content is just whitespace, add to elements directly (also Oracle doesn't support passing ' ' as param). if (preg_match('/^\s+$/', $chunk)) { $elements[] = "'$chunk'"; } else { $elementparam = "uf{$unique}fp_{$index}"; $elements[] = ":{$elementparam}"; $params[$elementparam] = $chunk; } } } return [$DB->sql_concat(...$elements), $params]; } /** * Gets the display name of a given user field. * * Supports field names from the 'user' database table, and custom profile fields supplied in * the format 'profile_field_xx'. * * @param string $field Field name in database * @return string Field name for display to user * @throws \coding_exception */ public static function get_display_name(string $field): string { global $CFG; // Custom fields have special handling. if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) { require_once($CFG->dirroot . '/user/profile/lib.php'); $fieldinfo = profile_get_custom_field_data_by_shortname($matches[1], false); // Use format_string so it can be translated with multilang filter if necessary. return $fieldinfo ? format_string($fieldinfo->name) : $field; } // Some fields have language strings which are not the same as field name. switch ($field) { case 'picture' : { return get_string('pictureofuser'); } } // Otherwise just use the same lang string. return get_string($field); } /** * Resets the unique identifier used to ensure that multiple SQL fragments generated in the * same request will have different identifiers for parameters and table aliases. * * This is intended only for use in unit testing. */ public static function reset_unique_identifier() { self::$uniqueidentifier = 1; } /** * Checks if a field name looks like a custom profile field i.e. it begins with profile_field_ * (does not check if that profile field actually exists). * * @param string $fieldname Field name * @return string Empty string if not a profile field, or profile field name (without profile_field_) */ public static function match_custom_field(string $fieldname): string { if (preg_match(self::PROFILE_FIELD_REGEX, $fieldname, $matches)) { return $matches[1]; } else { return ''; } } } classes/external/user_summary_exporter.php 0000644 00000011241 15151162244 0015217 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for exporting a user summary from an stdClass. * * @package core_user * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_user\external; defined('MOODLE_INTERNAL') || die(); use context_system; use renderer_base; use moodle_url; /** * Class for exporting a user summary from an stdClass. * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_summary_exporter extends \core\external\exporter { protected function get_other_values(renderer_base $output) { global $PAGE, $CFG; // Add user picture. $userpicture = new \user_picture($this->data); $userpicture->size = 1; // Size f1. $profileimageurl = $userpicture->get_url($PAGE)->out(false); $userpicture->size = 0; // Size f2. $profileimageurlsmall = $userpicture->get_url($PAGE)->out(false); $profileurl = (new moodle_url('/user/profile.php', array('id' => $this->data->id)))->out(false); // TODO Does not support custom user profile fields (MDL-70456). $identityfields = array_flip(\core_user\fields::get_identity_fields(null, false)); $data = $this->data; foreach ($identityfields as $field => $index) { if (!empty($data->$field)) { $identityfields[$field] = $data->$field; } else { unset($identityfields[$field]); } } $identity = implode(', ', $identityfields); return array( 'fullname' => fullname($this->data), 'profileimageurl' => $profileimageurl, 'profileimageurlsmall' => $profileimageurlsmall, 'profileurl' => $profileurl, 'identity' => $identity ); } /** * Get the format parameters for department. * * @return array */ protected function get_format_parameters_for_department() { return [ 'context' => context_system::instance(), // The system context is cached, so we can get it right away. ]; } /** * Get the format parameters for institution. * * @return array */ protected function get_format_parameters_for_institution() { return [ 'context' => context_system::instance(), // The system context is cached, so we can get it right away. ]; } public static function define_properties() { return array( 'id' => array( 'type' => \core_user::get_property_type('id'), ), 'email' => array( 'type' => \core_user::get_property_type('email'), 'default' => '' ), 'idnumber' => array( 'type' => \core_user::get_property_type('idnumber'), 'default' => '' ), 'phone1' => array( 'type' => \core_user::get_property_type('phone1'), 'default' => '' ), 'phone2' => array( 'type' => \core_user::get_property_type('phone2'), 'default' => '' ), 'department' => array( 'type' => \core_user::get_property_type('department'), 'default' => '' ), 'institution' => array( 'type' => \core_user::get_property_type('institution'), 'default' => '' ) ); } public static function define_other_properties() { return array( 'fullname' => array( 'type' => PARAM_RAW ), 'identity' => array( 'type' => PARAM_RAW ), 'profileurl' => array( 'type' => PARAM_URL ), 'profileimageurl' => array( 'type' => PARAM_URL ), 'profileimageurlsmall' => array( 'type' => PARAM_URL ), ); } } classes/external/search_identity.php 0000644 00000011565 15151162244 0013723 0 ustar 00 <?php // This file is part of Moodle - https://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <https://www.gnu.org/licenses/>. namespace core_user\external; /** * Provides the core_user_search_identity external function. * * @package core_user * @category external * @copyright 2021 David Mudrák <david@moodle.com> * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class search_identity extends \external_api { /** * Describes the external function parameters. * * @return \external_function_parameters */ public static function execute_parameters(): \external_function_parameters { return new \external_function_parameters([ 'query' => new \external_value(PARAM_RAW, 'The search query', VALUE_REQUIRED), ]); } /** * Finds users with the identity matching the given query. * * @param string $query The search request. * @return array */ public static function execute(string $query): array { global $DB, $CFG; $params = \external_api::validate_parameters(self::execute_parameters(), [ 'query' => $query, ]); $query = clean_param($params['query'], PARAM_TEXT); // Validate context. $context = \context_system::instance(); self::validate_context($context); require_capability('moodle/user:viewalldetails', $context); $hasviewfullnames = has_capability('moodle/site:viewfullnames', $context); $fields = \core_user\fields::for_name()->with_identity($context, false); $extrafields = $fields->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); list($searchsql, $searchparams) = users_search_sql($query, '', true, $extrafields); list($sortsql, $sortparams) = users_order_by_sql('', $query, $context); $params = array_merge($searchparams, $sortparams); $rs = $DB->get_recordset_select('user', $searchsql, $params, $sortsql, 'id' . $fields->get_sql()->selects, 0, $CFG->maxusersperpage + 1); $count = 0; $list = []; foreach ($rs as $record) { $user = (object)[ 'id' => $record->id, 'fullname' => fullname($record, $hasviewfullnames), 'extrafields' => [], ]; foreach ($extrafields as $extrafield) { // Sanitize the extra fields to prevent potential XSS exploit. $user->extrafields[] = (object)[ 'name' => $extrafield, 'value' => s($record->$extrafield) ]; } $count++; if ($count <= $CFG->maxusersperpage) { $list[$record->id] = $user; } } $rs->close(); return [ 'list' => $list, 'maxusersperpage' => $CFG->maxusersperpage, 'overflow' => ($count > $CFG->maxusersperpage), ]; } /** * Describes the external function result value. * * @return \external_description */ public static function execute_returns(): \external_description { return new \external_single_structure([ 'list' => new \external_multiple_structure( new \external_single_structure([ 'id' => new \external_value(\core_user::get_property_type('id'), 'ID of the user'), // The output of the {@see fullname()} can contain formatting HTML such as <ruby> tags. // So we need PARAM_RAW here and the caller is supposed to render it appropriately. 'fullname' => new \external_value(PARAM_RAW, 'The fullname of the user'), 'extrafields' => new \external_multiple_structure( new \external_single_structure([ 'name' => new \external_value(PARAM_TEXT, 'Name of the extrafield.'), 'value' => new \external_value(PARAM_TEXT, 'Value of the extrafield.'), ]), 'List of extra fields', VALUE_OPTIONAL) ]) ), 'maxusersperpage' => new \external_value(PARAM_INT, 'Configured maximum users per page.'), 'overflow' => new \external_value(PARAM_BOOL, 'Were there more records than maxusersperpage found?'), ]); } } calendar.php 0000644 00000007477 15151162244 0007046 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Allows you to edit a users profile * * @copyright 2015 Shamim Rezaie http://foodle.org * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_user */ require_once('../config.php'); require_once($CFG->dirroot.'/calendar/lib.php'); require_once($CFG->dirroot.'/user/editlib.php'); require_once($CFG->dirroot.'/user/lib.php'); $userid = optional_param('id', $USER->id, PARAM_INT); // User id. $PAGE->set_url('/user/calendar.php', array('id' => $userid)); list($user, $course) = useredit_setup_preference_page($userid, SITEID); $defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD; if (isset($CFG->calendar_lookahead)) { $defaultlookahead = intval($CFG->calendar_lookahead); } $defaultmaxevents = CALENDAR_DEFAULT_UPCOMING_MAXEVENTS; if (isset($CFG->calendar_maxevents)) { $defaultmaxevents = intval($CFG->calendar_maxevents); } // Create form. $calendarform = new core_user\form\calendar_form(null, array('userid' => $user->id)); $user->timeformat = get_user_preferences('calendar_timeformat', ''); $user->startwday = calendar_get_starting_weekday(); $user->maxevents = get_user_preferences('calendar_maxevents', $defaultmaxevents); $user->lookahead = get_user_preferences('calendar_lookahead', $defaultlookahead); $user->persistflt = get_user_preferences('calendar_persistflt', 0); $calendarform->set_data($user); $redirect = new moodle_url("/user/preferences.php", array('userid' => $user->id)); if ($calendarform->is_cancelled()) { redirect($redirect); } else if ($calendarform->is_submitted() && $calendarform->is_validated() && confirm_sesskey()) { $data = $calendarform->get_data(); $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; // If the specified calendar type does not exist, use the site default. if (!array_key_exists($calendartype, \core_calendar\type_factory::get_list_of_calendar_types())) { $calendartype = $CFG->calendartype; } $user->calendartype = $calendartype; // Update user with new calendar type. user_update_user($user, false, false); // Trigger event. \core\event\user_updated::create_from_userid($user->id)->trigger(); if ($USER->id == $user->id) { $USER->calendartype = $calendartype; } redirect($redirect, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } // Display page header. $streditmycalendar = get_string('calendarpreferences', 'calendar'); $userfullname = fullname($user, true); $PAGE->navbar->includesettingsbase = true; $PAGE->set_title("$course->shortname: $streditmycalendar"); $PAGE->set_heading($userfullname); echo $OUTPUT->header(); echo $OUTPUT->heading($streditmycalendar); // Finally display THE form. $calendarform->display(); // And proper footer. echo $OUTPUT->footer(); pix.php 0000644 00000003006 15151162244 0006055 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * BC user image location * * @package core_user * @category files * @copyright 2010 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_DEBUG_DISPLAY', true); define('NOMOODLECOOKIE', 1); require('../config.php'); $PAGE->set_url('/user/pix.php'); $PAGE->set_context(null); $relativepath = get_file_argument('pix.php'); $args = explode('/', trim($relativepath, '/')); if (count($args) == 2) { $userid = (integer)$args[0]; if ($args[1] === 'f1.jpg') { $image = 'f1'; } else { $image = 'f2'; } if ($usercontext = context_user::instance($userid, IGNORE_MISSING)) { $url = moodle_url::make_pluginfile_url($usercontext->id, 'user', 'icon', null, '/', $image); redirect($url); } } redirect($OUTPUT->image_url('u/f1')); defaulthomepage.php 0000644 00000004625 15151162244 0010417 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle 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 the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Allow user to set their default home page * * @package core_user * @copyright 2019 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->dirroot . '/user/editlib.php'); require_once($CFG->dirroot . '/user/lib.php'); $userid = optional_param('id', $USER->id, PARAM_INT); $PAGE->set_url('/user/defaulthomepage.php', ['id' => $userid]); list($user, $course) = useredit_setup_preference_page($userid, SITEID); $form = new core_user\form\defaulthomepage_form(); $defaulthomepage = get_default_home_page(); $user->defaulthomepage = get_user_preferences('user_home_page_preference', $defaulthomepage, $user); if (empty($CFG->enabledashboard) && $user->defaulthomepage == HOMEPAGE_MY) { // If the user was using the dashboard but it's disabled, return the default home page. $user->defaulthomepage = $defaulthomepage; } $form->set_data($user); $redirect = new moodle_url('/user/preferences.php', ['userid' => $user->id]); if ($form->is_cancelled()) { redirect($redirect); } else if ($data = $form->get_data()) { $userupdate = [ 'id' => $user->id, 'preference_user_home_page_preference' => $data->defaulthomepage, ]; useredit_update_user_preference($userupdate); \core\event\user_updated::create_from_userid($userupdate['id'])->trigger(); redirect($redirect); } $PAGE->navbar->includesettingsbase = true; $strdefaulthomepageuser = get_string('defaulthomepageuser'); $PAGE->set_title("$course->shortname: $strdefaulthomepageuser"); $PAGE->set_heading(fullname($user, true)); echo $OUTPUT->header(); echo $OUTPUT->heading($strdefaulthomepageuser); $form->display(); echo $OUTPUT->footer();
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 2.78 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�