diff --git a/lib/myprofilelib.php b/lib/myprofilelib.php index 54ea27c5cfc..c873280bc2b 100644 --- a/lib/myprofilelib.php +++ b/lib/myprofilelib.php @@ -367,19 +367,13 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $tree->add_node($node); } - if ($categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) { - foreach ($categories as $category) { - if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $user->id); - if ($formfield->is_visible() and !$formfield->is_empty()) { - $node = new core_user\output\myprofile\node('contact', 'custom_field_' . $formfield->field->shortname, - format_string($formfield->field->name), null, null, $formfield->display_data()); - $tree->add_node($node); - } - } + $categories = profile_get_user_fields_with_data_by_category($user->id); + foreach ($categories as $categoryid => $fields) { + foreach ($fields as $formfield) { + if ($formfield->is_visible() and !$formfield->is_empty()) { + $node = new core_user\output\myprofile\node('contact', 'custom_field_' . $formfield->field->shortname, + format_string($formfield->field->name), null, null, $formfield->display_data()); + $tree->add_node($node); } } } diff --git a/user/lib.php b/user/lib.php index 5074fc2f796..a7f9942eedf 100644 --- a/user/lib.php +++ b/user/lib.php @@ -331,34 +331,28 @@ function user_get_user_details($user, $course = null, array $userfields = array( $userdetails['fullname'] = fullname($user); if (in_array('customfields', $userfields)) { - $fields = $DB->get_recordset_sql("SELECT f.* - FROM {user_info_field} f - JOIN {user_info_category} c - ON f.categoryid=c.id - ORDER BY c.sortorder ASC, f.sortorder ASC"); + $categories = profile_get_user_fields_with_data_by_category($user->id); $userdetails['customfields'] = array(); - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $user->id); - if ($formfield->is_visible() and !$formfield->is_empty()) { + 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 ($field->datatype == 'text' or $field->datatype == 'textarea') { - $fieldvalue = $formfield->display_data(); - } else { - // Cases: datetime, checkbox and menu. - $fieldvalue = $formfield->data; + // 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); } - - $userdetails['customfields'][] = - array('name' => $formfield->field->name, 'value' => $fieldvalue, - 'type' => $field->datatype, 'shortname' => $formfield->field->shortname); } } - $fields->close(); // Unset customfields if it's empty. if (empty($userdetails['customfields'])) { unset($userdetails['customfields']); diff --git a/user/profile/field/checkbox/field.class.php b/user/profile/field/checkbox/field.class.php index 7e08b368277..d6d9943157c 100644 --- a/user/profile/field/checkbox/field.class.php +++ b/user/profile/field/checkbox/field.class.php @@ -30,39 +30,6 @@ */ class profile_field_checkbox extends profile_field_base { - /** - * Constructor method. - * Pulls out the options for the checkbox from the database and sets the - * the corresponding key for the data if it exists - * - * @param int $fieldid - * @param int $userid - */ - public function __construct($fieldid=0, $userid=0) { - global $DB; - // First call parent constructor. - parent::__construct($fieldid, $userid); - - if (!empty($this->field)) { - $datafield = $DB->get_field('user_info_data', 'data', array('userid' => $this->userid, 'fieldid' => $this->fieldid)); - if ($datafield !== false) { - $this->data = $datafield; - } else { - $this->data = $this->field->defaultdata; - } - } - } - - /** - * Old syntax of class constructor. Deprecated in PHP7. - * - * @deprecated since Moodle 3.1 - */ - public function profile_field_checkbox($fieldid=0, $userid=0) { - debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); - self::__construct($fieldid, $userid); - } - /** * Add elements for editing the profile field value. * @param moodleform $mform diff --git a/user/profile/field/menu/field.class.php b/user/profile/field/menu/field.class.php index e662c490ba7..a304cc4102a 100644 --- a/user/profile/field/menu/field.class.php +++ b/user/profile/field/menu/field.class.php @@ -43,10 +43,11 @@ class profile_field_menu extends profile_field_base { * * @param int $fieldid * @param int $userid + * @param object $fielddata */ - public function __construct($fieldid = 0, $userid = 0) { + public function __construct($fieldid = 0, $userid = 0, $fielddata = null) { // First call parent constructor. - parent::__construct($fieldid, $userid); + parent::__construct($fieldid, $userid, $fielddata); // Param 1 for menu type is the options. if (isset($this->field->param1)) { @@ -73,16 +74,6 @@ class profile_field_menu extends profile_field_base { } } - /** - * Old syntax of class constructor. Deprecated in PHP7. - * - * @deprecated since Moodle 3.1 - */ - public function profile_field_menu($fieldid=0, $userid=0) { - debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); - self::__construct($fieldid, $userid); - } - /** * Create the code snippet for this field instance * Overwrites the base class method diff --git a/user/profile/field/upgrade.txt b/user/profile/field/upgrade.txt new file mode 100644 index 00000000000..72fc8683d1b --- /dev/null +++ b/user/profile/field/upgrade.txt @@ -0,0 +1,6 @@ +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. diff --git a/user/profile/lib.php b/user/profile/lib.php index 36a63e20d34..6223bb63395 100644 --- a/user/profile/lib.php +++ b/user/profile/lib.php @@ -56,17 +56,44 @@ class profile_field_base { /** @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 object $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) { - global $USER; + 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); - $this->load_data(); + if ($fielddata) { + $this->set_field($fielddata); + if ($userid && !empty($fielddata->hasuserdata)) { + $this->set_user_data($fielddata->data, $fielddata->dataformat); + } + } else { + $this->load_data(); + } } /** @@ -288,9 +315,73 @@ class profile_field_base { $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; + } + + /** + * 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() { @@ -301,18 +392,13 @@ class profile_field_base { $this->field = null; $this->inputname = ''; } else { - $this->field = $field; - $this->inputname = 'profile_field_'.$field->shortname; + $this->set_field($field); } - if (!empty($this->field)) { + if (!empty($this->field) && $this->userid) { $params = array('userid' => $this->userid, 'fieldid' => $this->fieldid); if ($data = $DB->get_record('user_info_data', $params, 'data, dataformat')) { - $this->data = $data->data; - $this->dataformat = $data->dataformat; - } else { - $this->data = $this->field->defaultdata; - $this->dataformat = FORMAT_HTML; + $this->set_user_data($data->data, $data->dataformat); } } else { $this->data = null; @@ -327,6 +413,8 @@ class profile_field_base { public function is_visible() { global $USER; + $context = $this->userid ? context_user::instance($this->userid) : context_system::instance(); + switch ($this->field->visible) { case PROFILE_VISIBLE_ALL: return true; @@ -334,12 +422,10 @@ class profile_field_base { if ($this->userid == $USER->id) { return true; } else { - return has_capability('moodle/user:viewalldetails', - context_user::instance($this->userid)); + return has_capability('moodle/user:viewalldetails', $context); } default: - return has_capability('moodle/user:viewalldetails', - context_user::instance($this->userid)); + return has_capability('moodle/user:viewalldetails', $context); } } @@ -411,20 +497,64 @@ class profile_field_base { } } +/** + * 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($userid) { + global $DB, $CFG; + + // Join any user info data present with each user info field for the user object. + $sql = 'SELECT uif.*, uic.name AS categoryname '; + if ($userid) { + $sql .= ', uid.id AS hasuserdata, uid.data, uid.dataformat '; + } + $sql .= 'FROM {user_info_field} uif '; + $sql .= 'LEFT JOIN {user_info_category} uic ON uif.categoryid = uic.id '; + if ($userid) { + $sql .= 'LEFT JOIN {user_info_data} uid ON uif.id = uid.fieldid AND uid.userid = :userid '; + } + $sql .= 'ORDER BY uic.sortorder ASC, uif.sortorder ASC '; + $fields = $DB->get_records_sql($sql, ['userid' => $userid]); + $data = []; + foreach ($fields as $field) { + require_once($CFG->dirroot . '/user/profile/field/' . $field->datatype . '/field.class.php'); + $classname = 'profile_field_' . $field->datatype; + $field->hasuserdata = !empty($field->hasuserdata); + /** @var profile_field_base $fieldobject */ + $fieldobject = new $classname($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($userid) { + $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($user) { - global $CFG, $DB; + global $CFG; - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $user->id); - $formfield->edit_load_user_data($user); - } + $fields = profile_get_user_fields_with_data($user->id); + foreach ($fields as $formfield) { + $formfield->edit_load_user_data($user); } } @@ -440,28 +570,21 @@ function profile_definition($mform, $userid = 0) { // If user is "admin" fields are displayed regardless. $update = has_capability('moodle/user:update', context_system::instance()); - if ($categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) { - foreach ($categories as $category) { - if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) { + $categories = profile_get_user_fields_with_data_by_category($userid); + foreach ($categories as $categoryid => $fields) { + // Check first if *any* fields will be displayed. + $display = false; + foreach ($fields as $formfield) { + if ($formfield->is_visible()) { + $display = true; + } + } - // Check first if *any* fields will be displayed. - $display = false; - foreach ($fields as $field) { - if ($field->visible != PROFILE_VISIBLE_NONE) { - $display = true; - } - } - - // Display the header and the fields. - if ($display or $update) { - $mform->addElement('header', 'category_'.$category->id, format_string($category->name)); - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid); - $formfield->edit_field($mform); - } - } + // Display the header and the fields. + if ($display or $update) { + $mform->addElement('header', 'category_'.$categoryid, format_string($formfield->get_category_name())); + foreach ($fields as $formfield) { + $formfield->edit_field($mform); } } } @@ -473,17 +596,13 @@ function profile_definition($mform, $userid = 0) { * @param int $userid */ function profile_definition_after_data($mform, $userid) { - global $CFG, $DB; + global $CFG; $userid = ($userid < 0) ? 0 : (int)$userid; - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid); - $formfield->edit_after_data($mform); - } + $fields = profile_get_user_fields_with_data($userid); + foreach ($fields as $formfield) { + $formfield->edit_after_data($mform); } } @@ -494,16 +613,12 @@ function profile_definition_after_data($mform, $userid) { * @return array */ function profile_validation($usernew, $files) { - global $CFG, $DB; + global $CFG; $err = array(); - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $usernew->id); - $err += $formfield->edit_validate_field($usernew, $files); - } + $fields = profile_get_user_fields_with_data($usernew->id); + foreach ($fields as $formfield) { + $err += $formfield->edit_validate_field($usernew, $files); } return $err; } @@ -513,15 +628,11 @@ function profile_validation($usernew, $files) { * @param stdClass $usernew */ function profile_save_data($usernew) { - global $CFG, $DB; + global $CFG; - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $usernew->id); - $formfield->edit_save_data($usernew); - } + $fields = profile_get_user_fields_with_data($usernew->id); + foreach ($fields as $formfield) { + $formfield->edit_save_data($usernew); } } @@ -532,18 +643,12 @@ function profile_save_data($usernew) { function profile_display_fields($userid) { global $CFG, $USER, $DB; - if ($categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) { - foreach ($categories as $category) { - if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid); - 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()); - } - } + $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()); } } } @@ -611,18 +716,14 @@ function profile_signup_fields($mform) { * @return stdClass */ function profile_user_record($userid, $onlyinuserobject = true) { - global $CFG, $DB; + global $CFG; $usercustomfields = new stdClass(); - if ($fields = $DB->get_records('user_info_field')) { - foreach ($fields as $field) { - require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); - $newfield = 'profile_field_'.$field->datatype; - $formfield = new $newfield($field->id, $userid); - if (!$onlyinuserobject || $formfield->is_user_object_data()) { - $usercustomfields->{$field->shortname} = $formfield->data; - } + $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; } }