From 1d7e191310b148d6cb4aa9e8d6c675c86a939275 Mon Sep 17 00:00:00 2001 From: sam marshall Date: Wed, 30 Sep 2020 14:05:43 +0100 Subject: [PATCH] [OU-SPECIFIC] Backport of MDL-45242, MDL-71131, MDL-71099 #390636 OU-specific #407 This is the code about custom user fields support for display on lists using the showuseridentity feature. --- admin/settings/users.php | 33 + admin/tool/lp/classes/external.php | 8 + admin/user.php | 8 + enrol/externallib.php | 29 + enrol/locallib.php | 99 +++ .../build/form-potential-user-selector.min.js | Bin 1214 -> 1385 bytes .../form-potential-user-selector.min.js.map | Bin 7606 -> 9204 bytes .../amd/src/form-potential-user-selector.js | 23 + enrol/manual/classes/enrol_users_form.php | 5 + lang/en/admin.php | 7 + lib/adminlib.php | 21 + lib/classes/user.php | 9 + lib/datalib.php | 31 + lib/deprecatedlib.php | 8 + lib/moodlelib.php | 28 + lib/myprofilelib.php | 11 + mod/assign/classes/output/grading_app.php | 6 + .../external/user_summary_exporter.php | 6 + user/classes/fields.php | 645 ++++++++++++++++++ user/classes/table/participants.php | 10 + user/classes/table/participants_search.php | 72 ++ user/profile/lib.php | 32 + 22 files changed, 1091 insertions(+) create mode 100644 user/classes/fields.php diff --git a/admin/settings/users.php b/admin/settings/users.php index c54a169ce34..0ce756b4c0f 100644 --- a/admin/settings/users.php +++ b/admin/settings/users.php @@ -210,6 +210,8 @@ if ($hassiteconfig // Custom user profile fields are not currently supported. $temp->add(new admin_setting_configmulticheckbox('showuseridentity', new lang_string('showuseridentity', 'admin'), +// ou-specific begins #407 (until 3.11) +/* new lang_string('showuseridentity_desc', 'admin'), array('email' => 1), array( 'username' => new lang_string('username'), 'idnumber' => new lang_string('idnumber'), @@ -221,6 +223,37 @@ if ($hassiteconfig 'city' => new lang_string('city'), 'country' => new lang_string('country'), ))); +*/ + new lang_string('showuseridentity_desc', 'admin'), ['email' => 1], + function() { + global $DB; + + // Basic fields available in user table. + $fields = [ + 'username' => new lang_string('username'), + 'idnumber' => new lang_string('idnumber'), + 'email' => new lang_string('email'), + 'phone1' => new lang_string('phone1'), + 'phone2' => new lang_string('phone2'), + 'department' => new lang_string('department'), + 'institution' => new lang_string('institution'), + 'city' => new lang_string('city'), + 'country' => new lang_string('country'), + ]; + + // Custom profile fields. + $profilefields = $DB->get_records('user_info_field', ['datatype' => 'text'], 'sortorder ASC'); + foreach ($profilefields as $key => $field) { + // Only reasonable-length fields can be used as identity fields. + if ($field->param2 > 255) { + continue; + } + $fields['profile_field_' . $field->shortname] = $field->name . ' *'; + } + + return $fields; + })); +// ou-specific ends #407 (until 3.11) $setting = new admin_setting_configtext('fullnamedisplay', new lang_string('fullnamedisplay', 'admin'), new lang_string('configfullnamedisplay', 'admin'), 'language', PARAM_TEXT, 50); $setting->set_force_ltr(true); diff --git a/admin/tool/lp/classes/external.php b/admin/tool/lp/classes/external.php index ada3e887e66..a74e58e4e39 100644 --- a/admin/tool/lp/classes/external.php +++ b/admin/tool/lp/classes/external.php @@ -878,11 +878,19 @@ class external extends external_api { list($filtercapsql, $filtercapparams) = api::filter_users_with_capability_on_user_context_sql($cap, $USER->id, SQL_PARAMS_NAMED); +// ou-specific begins #407 (until 3.11) +/* $extrasearchfields = array(); if (!empty($CFG->showuseridentity) && has_capability('moodle/site:viewuseridentity', $context)) { $extrasearchfields = explode(',', $CFG->showuseridentity); } $fields = \user_picture::fields('u', $extrasearchfields); +*/ + // TODO Does not support custom user profile fields (MDL-70456). + $userfieldsapi = \core_user\fields::for_identity($context, false)->with_userpic(); + $fields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; + $extrasearchfields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); +// ou-specific ends #407 (until 3.11) list($wheresql, $whereparams) = users_search_sql($query, 'u', true, $extrasearchfields); list($sortsql, $sortparams) = users_order_by_sql('u', $query, $context); diff --git a/admin/user.php b/admin/user.php index a06b2edc047..ae2e0dbedd9 100644 --- a/admin/user.php +++ b/admin/user.php @@ -183,9 +183,17 @@ // These columns are always shown in the users list. $requiredcolumns = array('city', 'country', 'lastaccess'); // Extra columns containing the extra user fields, excluding the required columns (city and country, to be specific). +// ou-specific begins #407 (until 3.11) +/* $extracolumns = get_extra_user_fields($context, $requiredcolumns); // Get all user name fields as an array. $allusernamefields = get_all_user_name_fields(false, null, null, null, true); +*/ + $userfields = \core_user\fields::for_identity($context, true)->excluding(...$requiredcolumns); + $extracolumns = $userfields->get_required_fields(); + // Get all user name fields as an array, but with firstname and lastname first. + $allusernamefields = \core_user\fields::get_name_fields(true); +// ou-specific ends #407 (until 3.11) $columns = array_merge($allusernamefields, $extracolumns, $requiredcolumns); foreach ($columns as $column) { diff --git a/enrol/externallib.php b/enrol/externallib.php index 161764ae5cc..7422d2be868 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -558,9 +558,27 @@ class core_enrol_external extends external_api { $results = array(); // Add also extra user fields. +// ou-specific begins #407 (until 3.11) + $identityfields = \core_user\fields::get_identity_fields($context, true); + $customprofilefields = []; + foreach ($identityfields as $key => $value) { + if ($fieldname = \core_user\fields::match_custom_field($value)) { + unset($identityfields[$key]); + $customprofilefields[$fieldname] = true; + } + } + if ($customprofilefields) { + $identityfields[] = 'customfields'; + } +// ou-specific ends #407 (until 3.11) $requiredfields = array_merge( ['id', 'fullname', 'profileimageurl', 'profileimageurlsmall'], +// ou-specific begins #407 (until 3.11) +/* get_extra_user_fields($context) +*/ + $identityfields +// ou-specific ends #407 (until 3.11) ); foreach ($users['users'] as $id => $user) { // Note: We pass the course here to validate that the current user can at least view user details in this course. @@ -568,6 +586,17 @@ class core_enrol_external extends external_api { // user records, and the user has been validated to have course:enrolreview in this course. Otherwise // there is no way to find users who aren't in the course in order to enrol them. if ($userdetails = user_get_user_details($user, $course, $requiredfields)) { +// ou-specific begins #407 (until 3.11) + // For custom fields, only return the ones we actually need. + if ($customprofilefields && array_key_exists('customfields', $userdetails)) { + foreach ($userdetails['customfields'] as $key => $data) { + if (!array_key_exists($data['shortname'], $customprofilefields)) { + unset($userdetails['customfields'][$key]); + } + } + $userdetails['customfields'] = array_values($userdetails['customfields']); + } +// ou-specific ends #407 (until 3.11) $results[] = $userdetails; } } diff --git a/enrol/locallib.php b/enrol/locallib.php index 0a46f0f02ef..ae5bdadfb3b 100644 --- a/enrol/locallib.php +++ b/enrol/locallib.php @@ -23,6 +23,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +// ou-specific begins #407 (until 3.11) +use core_user\fields; + +// ou-specific ends #407 (until 3.11) defined('MOODLE_INTERNAL') || die(); /** @@ -238,14 +242,28 @@ class course_enrolment_manager { list($instancessql, $params, $filter) = $this->get_instance_sql(); list($filtersql, $moreparams) = $this->get_filter_sql(); $params += $moreparams; +// ou-specific begins #407 (until 3.11) +/* $extrafields = get_extra_user_fields($this->get_context()); $extrafields[] = 'lastaccess'; $ufields = user_picture::fields('u', $extrafields); $sql = "SELECT DISTINCT $ufields, COALESCE(ul.timeaccess, 0) AS lastcourseaccess + FROM {user} u + JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid $instancessql) + JOIN {enrol} e ON (e.id = ue.enrolid) + LEFT JOIN {user_lastaccess} ul ON (ul.courseid = e.courseid AND ul.userid = u.id)"; +*/ + $userfields = fields::for_identity($this->get_context())->with_userpic()->excluding('lastaccess'); + ['selects' => $fieldselect, 'joins' => $fieldjoin, 'params' => $fieldjoinparams] = + (array)$userfields->get_sql('u', true, '', '', false); + $params += $fieldjoinparams; + $sql = "SELECT DISTINCT $fieldselect, COALESCE(ul.timeaccess, 0) AS lastcourseaccess FROM {user} u JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid $instancessql) JOIN {enrol} e ON (e.id = ue.enrolid) + $fieldjoin LEFT JOIN {user_lastaccess} ul ON (ul.courseid = e.courseid AND ul.userid = u.id)"; +// ou-specific ends #407 (until 3.11) if ($this->groupfilter) { $sql .= " LEFT JOIN ({groups_members} gm JOIN {groups} g ON (g.id = gm.groupid)) ON (u.id = gm.userid AND g.courseid = e.courseid)"; @@ -341,6 +359,8 @@ class course_enrolment_manager { list($ctxcondition, $params) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'ctx'); $params['courseid'] = $this->course->id; $params['cid'] = $this->course->id; +// ou-specific begins #407 (until 3.11) +/* $extrafields = get_extra_user_fields($this->get_context()); $ufields = user_picture::fields('u', $extrafields); $sql = "SELECT ra.id as raid, ra.contextid, ra.component, ctx.contextlevel, ra.roleid, $ufields, @@ -357,6 +377,28 @@ class course_enrolment_manager { WHERE ctx.id $ctxcondition AND ue.id IS NULL ORDER BY $sort $direction, ctx.depth DESC"; +*/ + $userfields = fields::for_identity($this->get_context())->with_userpic(); + ['selects' => $fieldselect, 'joins' => $fieldjoin, 'params' => $fieldjoinparams] = + (array)$userfields->get_sql('u', true); + $params += $fieldjoinparams; + $sql = "SELECT ra.id as raid, ra.contextid, ra.component, ctx.contextlevel, ra.roleid, + coalesce(u.lastaccess,0) AS lastaccess + $fieldselect + FROM {role_assignments} ra + JOIN {user} u ON u.id = ra.userid + JOIN {context} ctx ON ra.contextid = ctx.id + $fieldjoin + LEFT JOIN ( + SELECT ue.id, ue.userid + FROM {user_enrolments} ue + JOIN {enrol} e ON e.id = ue.enrolid + WHERE e.courseid = :courseid + ) ue ON ue.userid=u.id + WHERE ctx.id $ctxcondition AND + ue.id IS NULL + ORDER BY $sort $direction, ctx.depth DESC"; +// ou-specific ends #407 (until 3.11) $this->otherusers[$key] = $DB->get_records_sql($sql, $params, $page*$perpage, $perpage); } return $this->otherusers[$key]; @@ -375,13 +417,40 @@ class course_enrolment_manager { protected function get_basic_search_conditions($search, $searchanywhere) { global $DB, $CFG; +// ou-specific begins #407 (until 3.11) + // Get custom user field SQL used for querying all the fields we need (identity, name, and + // user picture). + $userfields = fields::for_identity($this->context)->with_name()->with_userpic() + ->excluding('username', 'lastaccess', 'maildisplay'); + ['selects' => $fieldselects, 'joins' => $fieldjoins, 'params' => $params, 'mappings' => $mappings] = + (array)$userfields->get_sql('u', true, '', '', false); + + // Searchable fields are only the identity and name ones (not userpic). + $searchable = array_fill_keys($userfields->get_required_fields( + [fields::PURPOSE_IDENTITY, fields::PURPOSE_NAME]), true); + +// ou-specific ends #407 (until 3.11) // Add some additional sensible conditions $tests = array("u.id <> :guestid", 'u.deleted = 0', 'u.confirmed = 1'); +// ou-specific begins #407 (until 3.11) +/* $params = array('guestid' => $CFG->siteguest); +*/ + $params['guestid'] = $CFG->siteguest; +// ou-specific ends #407 (until 3.11) if (!empty($search)) { +// ou-specific begins #407 (until 3.11) +/* $conditions = get_extra_user_fields($this->get_context()); foreach (get_all_user_name_fields() as $field) { $conditions[] = 'u.'.$field; +*/ + // Include identity and name fields as conditions. + foreach ($mappings as $fieldname => $fieldsql) { + if (array_key_exists($fieldname, $searchable)) { + $conditions[] = $fieldsql; + } +// ou-specific ends #407 (until 3.11) } $conditions[] = $DB->sql_fullname('u.firstname', 'u.lastname'); if ($searchanywhere) { @@ -399,6 +468,8 @@ class course_enrolment_manager { } $wherecondition = implode(' AND ', $tests); +// ou-specific begins #407 (until 3.11) +/* $extrafields = get_extra_user_fields($this->get_context(), array('username', 'lastaccess')); $extrafields[] = 'username'; $extrafields[] = 'lastaccess'; @@ -406,6 +477,10 @@ class course_enrolment_manager { $ufields = user_picture::fields('u', $extrafields); return array($ufields, $params, $wherecondition); +*/ + $selects = $fieldselects . ', u.username, u.lastaccess, u.maildisplay'; + return [$selects, $fieldjoins, $params, $wherecondition]; +// ou-specific ends #407 (until 3.11) } /** @@ -486,11 +561,19 @@ class course_enrolment_manager { $addedenrollment = 0, $returnexactcount = false) { global $DB; +// ou-specific begins #407 (until 3.11) +/* list($ufields, $params, $wherecondition) = $this->get_basic_search_conditions($search, $searchanywhere); +*/ + [$ufields, $joins, $params, $wherecondition] = $this->get_basic_search_conditions($search, $searchanywhere); +// ou-specific ends #407 (until 3.11) $fields = 'SELECT '.$ufields; $countfields = 'SELECT COUNT(1)'; $sql = " FROM {user} u +-- ou-specific begins #407 (until 3.11) + $joins +-- ou-specific ends #407 (until 3.11) LEFT JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = :enrolid) WHERE $wherecondition AND ue.id IS NULL"; @@ -518,11 +601,19 @@ class course_enrolment_manager { public function search_other_users($search = '', $searchanywhere = false, $page = 0, $perpage = 25, $returnexactcount = false) { global $DB, $CFG; +// ou-specific begins #407 (until 3.11) +/* list($ufields, $params, $wherecondition) = $this->get_basic_search_conditions($search, $searchanywhere); +*/ + [$ufields, $joins, $params, $wherecondition] = $this->get_basic_search_conditions($search, $searchanywhere); +// ou-specific ends #407 (until 3.11) $fields = 'SELECT ' . $ufields; $countfields = 'SELECT COUNT(u.id)'; $sql = " FROM {user} u +-- ou-specific begins #407 (until 3.11) + $joins +-- ou-specific ends #407 (until 3.11) LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid = :contextid) WHERE $wherecondition AND ra.id IS NULL"; @@ -546,11 +637,19 @@ class course_enrolment_manager { */ public function search_users(string $search = '', bool $searchanywhere = false, int $page = 0, int $perpage = 25, bool $returnexactcount = false) { +// ou-specific begins #407 (until 3.11) +/* list($ufields, $params, $wherecondition) = $this->get_basic_search_conditions($search, $searchanywhere); +*/ + [$ufields, $joins, $params, $wherecondition] = $this->get_basic_search_conditions($search, $searchanywhere); +// ou-specific ends #407 (until 3.11) $fields = 'SELECT ' . $ufields; $countfields = 'SELECT COUNT(u.id)'; $sql = " FROM {user} u +-- ou-specific begins #407 (until 3.11) + $joins +-- ou-specific ends #407 (until 3.11) JOIN {user_enrolments} ue ON ue.userid = u.id JOIN {enrol} e ON ue.enrolid = e.id WHERE $wherecondition diff --git a/enrol/manual/amd/build/form-potential-user-selector.min.js b/enrol/manual/amd/build/form-potential-user-selector.min.js index 70944792b30b3c1f3d14a03f4c9e9f9a759a92aa..d3571a35daa9c42066969f1a33e82627631fdaf4 100644 GIT binary patch delta 169 zcmdnT`I2kHIVRb%#3F?ZTm86#qWrYXoYeTV%+#Eecnv)*O%;9X$>*4?6F@4GZ8P*z zD^im+k~FO|(=?JbtAS*SUUF%1Nq#O^OR;7(Dqk-xzsNN)IYT3@G%vX%Ge1uw5v(gw yuQ(&Ws3b2jH`UhGHaXgGvL3UvV4_}GVoqtQW^FAHrREf;PUc|t*zC`o&jmjW0Abq*rT_o{ diff --git a/enrol/manual/amd/build/form-potential-user-selector.min.js.map b/enrol/manual/amd/build/form-potential-user-selector.min.js.map index 3f9f5baa2cda12e817300eff99e8d6b607d7e1a9..36b00035664bf20d7548a2cd7e12a5fe9eda96af 100644 GIT binary patch delta 1596 zcma)6-A)rx5Kf6`TZ+b7q$?zD(MW}*rGbj3!7zKaEhZiCEKbldAKdPwQQld^6LTaycM@rcM7Z4_6OhN z{e$M1u9*Py1x)~cwBDq})I1%arlXyO4I#e-vl()nIZM#4r^%k@7`cYFm>Fh!fpIMh zMhUGfq=?@mKl%Hcb98iz77Ldgaw)qYpp|`|Ommi279!@f8Gwhhujm)(SLGZOSpZ97 z_78?wWU$QW3=zbt&~5@;LAwbJVhcxi{y4u#z7<_)xxE~tx|RpXvnR2^V{%$>25#1M zf`uTzTZE449EPi4oV>`98>`e@X)w$*33#bbpvya>hgVHz?YX_0*Y@*27oNr?)HzDK3*8@B*H`#5;XTKq zhcPyilMJ~+?gV4q*gYIADG~5Ueh$9$mksi=4f(vQzhm8y@45zk6Y_J{@z^>Od1_~P zL}jziDQOrWJRwIy{S#^AXJOnh1%^o*G`DeMfgRHZ-GMWVjhS{9H^b&8{mRqZPeZd^ zgUO`oHb%TPt7w<(qWaKUwjEDBF+Mt>#v4xEuBu~+RO+_RFn61 zf&2VY+>a-QPM=I>?l`|iww$HE#FZa=-wgle{Wl#Lb_lf`lMi}3#WJgL^r)Sj??XMjlPDTBRKTZ<6uVB2?aEf)? zb>eNjU0U}kjM)Y!e!P(C>x<!|4-x?`OEW> RgB-pZ?m>C}?N>_2zVB6s&Zz(Z delta 624 zcmYjOO-mb56lGHTA(^NP10fj*jSCk(NY&Ct3x)gcOvX{CF*Q_i;U@K~h?=OhxUo>^ zAGrHMm!*pek?smzb?ZN{Kcf8sedi8hXL07f^X@tK`a-4L%JeqsN?v<+SABP;IO=9HI9pRoE8MO#!h0>G4iZEW#_lsZ?s1Gh`Z`08+p6#LOP6r0 z+);Bca)-CgGyJBTOAhHp&D4$$PX}8g&-3!3G%ml(_hqGgDIY6=q?NDIsRr_+G%DW$ QUk)dVG8POE{#3o8e>j+^2mk;8 diff --git a/enrol/manual/amd/src/form-potential-user-selector.js b/enrol/manual/amd/src/form-potential-user-selector.js index c2e53d82fd5..6ebd303290f 100644 --- a/enrol/manual/amd/src/form-potential-user-selector.js +++ b/enrol/manual/amd/src/form-potential-user-selector.js @@ -77,13 +77,36 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str'], function($, Ajax, if (results.length <= perpage) { // Render the label. +// ou-specific begins #407 (until 3.11) + const profileRegex = /^profile_field_(.*)$/; +// ou-specific ends #407 (until 3.11) $.each(results, function(index, user) { var ctx = user, identity = []; $.each(userfields, function(i, k) { +// ou-specific begins #407 (until 3.11) +/* if (typeof user[k] !== 'undefined' && user[k] !== '') { ctx.hasidentity = true; identity.push(user[k]); +*/ + const result = profileRegex.exec(k); + if (result) { + if (user.customfields) { + user.customfields.forEach(function(customfield) { + if (customfield.shortname === result[1]) { + ctx.hasidentity = true; + identity.push(customfield.value); + } + + }); + } + } else { + if (typeof user[k] !== 'undefined' && user[k] !== '') { + ctx.hasidentity = true; + identity.push(user[k]); + } +// ou-specific ends #407 (until 3.11) } }); ctx.identity = identity.join(', '); diff --git a/enrol/manual/classes/enrol_users_form.php b/enrol/manual/classes/enrol_users_form.php index 257a5c2a995..e16f04ddf58 100644 --- a/enrol/manual/classes/enrol_users_form.php +++ b/enrol/manual/classes/enrol_users_form.php @@ -93,7 +93,12 @@ class enrol_manual_enrol_users_form extends moodleform { 'courseid' => $course->id, 'enrolid' => $instance->id, 'perpage' => $CFG->maxusersperpage, +// ou-specific begins #407 (until 3.11) +/* 'userfields' => implode(',', get_extra_user_fields($context)) +*/ + 'userfields' => implode(',', \core_user\fields::get_identity_fields($context, true)) +// ou-specific ends #407 (until 3.11) ); $mform->addElement('autocomplete', 'userlist', get_string('selectusers', 'enrol_manual'), array(), $options); diff --git a/lang/en/admin.php b/lang/en/admin.php index 4f5f989095e..5d9260c6f45 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -1204,7 +1204,14 @@ $string['setupsearchengine'] = 'Setup search engine'; $string['showcommentscount'] = 'Show comments count'; $string['showdetails'] = 'Show details'; $string['showuseridentity'] = 'Show user identity'; +// ou-specific begins #407 (until 3.11) +/* $string['showuseridentity_desc'] = 'When selecting or searching for users, and when displaying lists of users, these fields may be shown in addition to their full name. The fields are only shown to users who have the moodle/site:viewuseridentity capability; by default, teachers and managers. (This option makes most sense if you choose one or two fields that are mandatory at your institution.)'; +*/ +$string['showuseridentity_desc'] = 'When selecting or searching for users, and when displaying lists of users, these fields may be shown in addition to their full name. The fields are only shown to users who have the moodle/site:viewuseridentity capability; by default, teachers and managers. (This option makes most sense if you choose one or two fields that are mandatory at your institution.) + +Fields marked * are custom user profile fields. You can select these fields, but there are currently some screens on which they will not appear.'; +// ou-specific ends #407 (until 3.11) $string['simplexmlrequired'] = 'The SimpleXML PHP extension is now required by Moodle.'; $string['sitemenubar'] = 'Site navigation'; $string['sitemailcharset'] = 'Character set'; diff --git a/lib/adminlib.php b/lib/adminlib.php index 2bc85d7f22c..142b363e216 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -3014,6 +3014,10 @@ class admin_setting_configcheckbox extends admin_setting { class admin_setting_configmulticheckbox extends admin_setting { /** @var array Array of choices value=>label */ public $choices; +// ou-specific begins #407 (until 3.11) + /** @var callable|null Loader function for choices */ + protected $choiceloader = null; +// ou-specific ends #407 (until 3.11) /** * Constructor: uses parent::__construct @@ -3025,7 +3029,17 @@ class admin_setting_configmulticheckbox extends admin_setting { * @param array $choices array of $value=>$label for each checkbox */ public function __construct($name, $visiblename, $description, $defaultsetting, $choices) { +// ou-specific begins #407 (until 3.11) +/* $this->choices = $choices; +*/ + if (is_array($choices)) { + $this->choices = $choices; + } + if (is_callable($choices)) { + $this->choiceloader = $choices; + } +// ou-specific ends #407 (until 3.11) parent::__construct($name, $visiblename, $description, $defaultsetting); } @@ -3042,6 +3056,13 @@ class admin_setting_configmulticheckbox extends admin_setting { } .... load choices here */ +// ou-specific begins #407 (until 3.11) + if ($this->choiceloader) { + if (!is_array($this->choices)) { + $this->choices = call_user_func($this->choiceloader); + } + } +// ou-specific ends #407 (until 3.11) return true; } diff --git a/lib/classes/user.php b/lib/classes/user.php index ac2430003cd..1d4fea61a5b 100644 --- a/lib/classes/user.php +++ b/lib/classes/user.php @@ -251,6 +251,8 @@ class core_user { $extrasql = ''; $extraparams = []; +// ou-specific begins #407 (until 3.11) +/* if (empty($CFG->showuseridentity)) { // Explode gives wrong result with empty string. $extra = []; @@ -270,6 +272,13 @@ class core_user { } $selectfields = \user_picture::fields('u', array_merge(get_all_user_name_fields(), $extrafieldlist)); +*/ + // TODO Does not support custom user profile fields (MDL-70456). + $userfieldsapi = \core_user\fields::for_identity(null, false)->with_userpic()->with_name() + ->including('username', 'deleted'); + $selectfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; + $extra = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); +// ou-specific ends #407 (until 3.11) $index = 1; foreach ($extra as $fieldname) { diff --git a/lib/datalib.php b/lib/datalib.php index fc1f962ba73..57e58dec0a7 100644 --- a/lib/datalib.php +++ b/lib/datalib.php @@ -479,7 +479,12 @@ function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperp $fullname = $DB->sql_fullname(); +// ou-specific begins #407 (until 3.11) +/* $select = "deleted <> 1 AND id <> :guestid"; +*/ + $select = "deleted <> 1 AND u.id <> :guestid"; +// ou-specific ends #407 (until 3.11) $params = array('guestid' => $CFG->siteguest); if (!empty($search)) { @@ -502,6 +507,12 @@ function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperp } if ($extraselect) { +// ou-specific begins #407 (until 3.11) + // The extra WHERE clause may refer to the 'id' column which can now be ambiguous because we + // changed the query to include joins, so replace any 'id' that is on its own (no alias) + // with 'u.id'. + $extraselect = preg_replace('~([ =]|^)id([ =]|$)~', '$1u.id$2', $extraselect); +// ou-specific ends #407 (until 3.11) $select .= " AND $extraselect"; $params = $params + (array)$extraparams; } @@ -512,6 +523,8 @@ function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperp // If a context is specified, get extra user fields that the current user // is supposed to see. +// ou-specific begins #407 (until 3.11) +/* $extrafields = ''; if ($extracontext) { $extrafields = get_extra_user_fields_sql($extracontext, '', '', @@ -520,12 +533,30 @@ function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperp } $namefields = get_all_user_name_fields(true); $extrafields = "$extrafields, $namefields"; +*/ + $userfields = \core_user\fields::for_name(); + if ($extracontext) { + $userfields->with_identity($extracontext, true); + } + $userfields->excluding('id', 'username', 'email', 'city', 'country', 'lastaccess', 'confirmed', 'mnethostid'); + ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] = + (array)$userfields->get_sql('u', true); +// ou-specific ends #407 (until 3.11) // warning: will return UNCONFIRMED USERS +// ou-specific begins #407 (until 3.11) +/* return $DB->get_records_sql("SELECT id, username, email, city, country, lastaccess, confirmed, mnethostid, suspended $extrafields FROM {user} WHERE $select $sort", $params, $page, $recordsperpage); +*/ + return $DB->get_records_sql("SELECT u.id, username, email, city, country, lastaccess, confirmed, mnethostid, suspended $selects + FROM {user} u + $joins + WHERE $select + $sort", array_merge($params, $joinparams), $page, $recordsperpage); +// ou-specific ends #407 (until 3.11) } diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php index 61db87c47ba..63783ccb5bf 100644 --- a/lib/deprecatedlib.php +++ b/lib/deprecatedlib.php @@ -3228,9 +3228,17 @@ function user_get_participants_sql($courseid, $groupid = 0, $accesssince = 0, $r } $conditions[] = $idnumber; +// ou-specific begins #407 (until 3.11) +/* if (!empty($CFG->showuseridentity)) { // Search all user identify fields. $extrasearchfields = explode(',', $CFG->showuseridentity); +*/ + // TODO Does not support custom user profile fields (MDL-70456). + $extrasearchfields = \core_user\fields::get_identity_fields($context, false); + if (!empty($extrasearchfields)) { + // Search all user identify fields. +// ou-specific ends #407 (until 3.11) foreach ($extrasearchfields as $extrasearchfield) { if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) { // Already covered above. Search by country not supported. diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 169ad1b21a1..7b0b98a81e8 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -3645,12 +3645,20 @@ function fullname($user, $override=false) { function get_all_user_name_fields($returnsql = false, $tableprefix = null, $prefix = null, $fieldprefix = null, $order = false) { // This array is provided in this order because when called by fullname() (above) if firstname is before // firstnamephonetic str_replace() will change the wrong placeholder. +// ou-specific begins #407 (until 3.11) +/* $alternatenames = array('firstnamephonetic' => 'firstnamephonetic', 'lastnamephonetic' => 'lastnamephonetic', 'middlename' => 'middlename', 'alternatename' => 'alternatename', 'firstname' => 'firstname', 'lastname' => 'lastname'); +*/ + $alternatenames = []; + foreach (\core_user\fields::get_name_fields() as $field) { + $alternatenames[$field] = $field; + } +// ou-specific ends #407 (until 3.11) // Let's add a prefix to the array of user name fields if provided. if ($prefix) { @@ -3762,6 +3770,8 @@ function order_in_string($values, $stringformat) { * listed in $already */ function get_extra_user_fields($context, $already = array()) { +// ou-specific begins #407 (until 3.11) +/* global $CFG; // Only users with permission get the extra fields. @@ -3802,6 +3812,10 @@ function get_extra_user_fields($context, $already = array()) { $extra = array_values($extra); return $extra; +*/ + $fields = \core_user\fields::for_identity($context, false)->excluding(...$already); + return $fields->get_required_fields(); +// ou-specific ends #407 (until 3.11) } /** @@ -3816,6 +3830,8 @@ function get_extra_user_fields($context, $already = array()) { * @return string Partial SQL select clause, beginning with comma, for example ',u.idnumber,u.department' unless it is blank */ function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = array()) { +// ou-specific begins #407 (until 3.11) +/* $fields = get_extra_user_fields($context, $already); $result = ''; // Add punctuation for alias. @@ -3829,6 +3845,13 @@ function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = a } } return $result; +*/ + $fields = \core_user\fields::for_identity($context, false)->excluding(...$already); + // Note: There will never be any joins or join params because we turned off profile fields. + $selects = $fields->get_sql($alias, false, $prefix)->selects; + + return $selects; +// ou-specific ends #407 (until 3.11) } /** @@ -3837,6 +3860,8 @@ function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = a * @return string Text description taken from language file, e.g. 'Phone number' */ function get_user_field_name($field) { +// ou-specific begins #407 (until 3.11) +/* // Some fields have language strings which are not the same as field name. switch ($field) { case 'url' : { @@ -3863,6 +3888,9 @@ function get_user_field_name($field) { } // Otherwise just use the same lang string. return get_string($field); +*/ + return \core_user\fields::get_display_name($field); +// ou-specific ends #407 (until 3.11) } /** diff --git a/lib/myprofilelib.php b/lib/myprofilelib.php index a3f9223e262..816042d2c22 100644 --- a/lib/myprofilelib.php +++ b/lib/myprofilelib.php @@ -125,12 +125,18 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, } else { $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); } +// ou-specific begins #407 (until 3.11) +/* $canviewuseridentity = has_capability('moodle/site:viewuseridentity', $courseorusercontext); if ($canviewuseridentity) { $identityfields = array_flip(explode(',', $CFG->showuseridentity)); } else { $identityfields = array(); } +*/ + // TODO Does not support custom user profile fields (MDL-70456). + $identityfields = array_flip(\core_user\fields::get_identity_fields($courseorusercontext, false)); +// ou-specific ends #407 (until 3.11) if (is_mnet_remote_user($user)) { $sql = "SELECT h.id, h.name, h.wwwroot, @@ -156,7 +162,12 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, or ($user->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY and enrol_sharing_course($user, $USER)) or has_capability('moodle/course:useremail', $courseorusercontext) // TODO: Deprecate/remove for MDL-37479. )) +// ou-specific begins #407 (until 3.11) +/* or (isset($identityfields['email']) and $canviewuseridentity) +*/ + or (isset($identityfields['email'])) +// ou-specific ends #407 (until 3.11) ) { // ou-specific begins #404 (until Moodle 3.11) /* diff --git a/mod/assign/classes/output/grading_app.php b/mod/assign/classes/output/grading_app.php index 128a869cdee..46d5d999c8c 100644 --- a/mod/assign/classes/output/grading_app.php +++ b/mod/assign/classes/output/grading_app.php @@ -168,7 +168,13 @@ class grading_app implements templatable, renderable { $export->rarrow = $output->rarrow(); $export->larrow = $output->larrow(); // List of identity fields to display (the user info will not contain any fields the user cannot view anyway). +// ou-specific begins #407 (until 3.11) +/* $export->showuseridentity = $CFG->showuseridentity; +*/ + // TODO Does not support custom user profile fields (MDL-70456). + $export->showuseridentity = implode(',', \core_user\fields::get_identity_fields(null, false)); +// ou-specific ends #407 (until 3.11) $export->currentuserid = $USER->id; $helpicon = new \help_icon('sendstudentnotifications', 'assign'); $export->helpicon = $helpicon->export_for_template($output); diff --git a/user/classes/external/user_summary_exporter.php b/user/classes/external/user_summary_exporter.php index f8f21dc767f..4a5514d8876 100644 --- a/user/classes/external/user_summary_exporter.php +++ b/user/classes/external/user_summary_exporter.php @@ -48,7 +48,13 @@ class user_summary_exporter extends \core\external\exporter { $profileurl = (new moodle_url('/user/profile.php', array('id' => $this->data->id)))->out(false); +// ou-specific begins #407 (until 3.11) +/* $identityfields = array_flip(explode(',', $CFG->showuseridentity)); +*/ + // TODO Does not support custom user profile fields (MDL-70456). + $identityfields = array_flip(\core_user\fields::get_identity_fields(null, false)); +// ou-specific ends #407 (until 3.11) $data = $this->data; foreach ($identityfields as $field => $index) { if (!empty($data->$field)) { diff --git a/user/classes/fields.php b/user/classes/fields.php new file mode 100644 index 00000000000..7395652cfea --- /dev/null +++ b/user/classes/fields.php @@ -0,0 +1,645 @@ +. + +namespace core_user; + +// ou-specific file #407 (until Moodle 3.11) +/** + * 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'. + * + * 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)) { + if ($allowcustom) { + require_once($CFG->dirroot . '/user/profile/lib.php'); + $fieldinfo = profile_get_custom_field_data_by_shortname($matches[1]); + switch ($fieldinfo['visible']) { + case PROFILE_VISIBLE_NONE: + case PROFILE_VISIBLE_PRIVATE: + $allowed = !$context || has_capability('moodle/user:viewalldetails', $context); + break; + case PROFILE_VISIBLE_ALL: + $allowed = true; + break; + } + } else { + $allowed = false; + } + 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 $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 $fieldalias.shortname = $placeholder + 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]; + } + + /** + * 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]); + // Use format_string so it can be translated with multilang filter if necessary. + return format_string($fieldinfo['name']); + } + + // Some fields have language strings which are not the same as field name. + switch ($field) { + case 'url' : { + return get_string('webpage'); + } + case 'icq' : { + return get_string('icqnumber'); + } + case 'skype' : { + return get_string('skypeid'); + } + case 'aim' : { + return get_string('aimid'); + } + case 'yahoo' : { + return get_string('yahooid'); + } + case 'msn' : { + return get_string('msnid'); + } + 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 ''; + } + } +} diff --git a/user/classes/table/participants.php b/user/classes/table/participants.php index 3a01197d9b0..26d1a8e5f2f 100644 --- a/user/classes/table/participants.php +++ b/user/classes/table/participants.php @@ -141,9 +141,19 @@ class participants extends \table_sql implements dynamic_table { $headers[] = get_string('fullname'); $columns[] = 'fullname'; +// ou-specific begins #407 (until 3.11) +/* $extrafields = get_extra_user_fields($this->context); +*/ + $extrafields = \core_user\fields::get_identity_fields($this->context); +// ou-specific ends #407 (until 3.11) foreach ($extrafields as $field) { +// ou-specific begins #407 (until 3.11) +/* $headers[] = get_user_field_name($field); +*/ + $headers[] = \core_user\fields::get_display_name($field); +// ou-specific ends #407 (until 3.11) $columns[] = $field; } diff --git a/user/classes/table/participants_search.php b/user/classes/table/participants_search.php index 79a7137f46c..8b3a74bd018 100644 --- a/user/classes/table/participants_search.php +++ b/user/classes/table/participants_search.php @@ -30,7 +30,12 @@ use core_table\local\filter\filterset; use core_user; use moodle_recordset; use stdClass; +// ou-specific begins #407 (until 3.11) +/* use user_picture; +*/ +use core_user\fields; +// ou-specific ends #407 (until 3.11) defined('MOODLE_INTERNAL') || die; @@ -77,7 +82,12 @@ class participants_search { $this->context = $context; $this->filterset = $filterset; +// ou-specific begins #407 (until 3.11) +/* $this->userfields = get_extra_user_fields($this->context); + */ + $this->userfields = fields::get_identity_fields($this->context); +// ou-specific ends #407 (until 3.11) } /** @@ -192,7 +202,20 @@ class participants_search { 'params' => $params, ] = $this->get_enrolled_sql(); +// ou-specific begins #407 (until 3.11) +/* $userfieldssql = user_picture::fields('u', $this->userfields); +*/ + // 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); + } +// ou-specific ends #407 (until 3.11) // Include any compulsory enrolment SQL (eg capability related filtering that must be applied). if (!empty($esqlforced)) { @@ -206,12 +229,22 @@ class participants_search { } if ($isfrontpage) { +// ou-specific begins #407 (until 3.11) +/* $outerselect = "SELECT {$userfieldssql}, u.lastaccess"; +*/ + $outerselect = "SELECT u.lastaccess $userfieldssql"; +// ou-specific ends #407 (until 3.11) if ($accesssince) { $wheres[] = user_get_user_lastaccess_sql($accesssince, 'u', $matchaccesssince); } } else { +// ou-specific begins #407 (until 3.11) +/* $outerselect = "SELECT {$userfieldssql}, COALESCE(ul.timeaccess, 0) AS lastaccess"; +*/ + $outerselect = "SELECT COALESCE(ul.timeaccess, 0) AS lastaccess $userfieldssql"; +// ou-specific ends #407 (until 3.11) // 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; @@ -254,7 +287,12 @@ class participants_search { [ 'where' => $keywordswhere, 'params' => $keywordsparams, +// ou-specific begins #407 (until 3.11) +/* ] = $this->get_keywords_search_sql(); +*/ + ] = $this->get_keywords_search_sql($mappings); +// ou-specific ends #407 (until 3.11) if (!empty($keywordswhere)) { $wheres[] = $keywordswhere; @@ -874,7 +912,12 @@ class participants_search { * * @return array SQL query data in the format ['where' => '', 'params' => []]. */ +// ou-specific begins #407 (until 3.11) +/* protected function get_keywords_search_sql(): array { +*/ + protected function get_keywords_search_sql(array $mappings): array { +// ou-specific ends #407 (until 3.11) global $CFG, $DB, $USER; $keywords = []; @@ -962,6 +1005,8 @@ class participants_search { $conditions[] = $idnumber; +// ou-specific begins #407 (until 3.11) +/* if (!empty($CFG->showuseridentity)) { // Search all user identify fields. $extrasearchfields = explode(',', $CFG->showuseridentity); @@ -987,6 +1032,33 @@ class participants_search { $conditions[] = $condition; } } +*/ + // 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. Search by country not supported. + 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; + } +// ou-specific ends #407 (until 3.11) // Search by middlename. $middlename = $DB->sql_like('middlename', ':' . $searchkey4, false, false); diff --git a/user/profile/lib.php b/user/profile/lib.php index c82551f99c1..481671c1a6c 100644 --- a/user/profile/lib.php +++ b/user/profile/lib.php @@ -848,6 +848,38 @@ function profile_save_custom_fields($userid, $profilefields) { } } +// ou-specific begins #407 (until 3.11) +/** + * 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 + * @return array Array with id, name, and visible fields + */ +function profile_get_custom_field_data_by_shortname(string $shortname): array { + global $DB; + + $cache = \cache::make_from_params(cache_store::MODE_REQUEST, 'core_profile', 'customfields', + [], ['simplekeys' => true, 'simpledata' => true]); + $data = $cache->get($shortname); + if (!$data) { + // If we don't have data, we get and cache it for all fields to avoid multiple DB requests. + $fields = $DB->get_records('user_info_field', null, '', 'id, shortname, name, visible'); + foreach ($fields as $field) { + $cache->set($field->shortname, (array)$field); + if ($field->shortname === $shortname) { + $data = (array)$field; + } + } + if (!$data) { + throw new \coding_exception('Unknown custom field: ' . $shortname); + } + } + + return $data; +} + +// ou-specific ends #407 (until 3.11) /** * Trigger a user profile viewed event. * -- 2.27.0.windows.1