MDL-46946 user: Make missing required custom fields trigger profile edit
[moodle.git] / user / profile / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Profile field API library file.
19  *
20  * @package core_user
21  * @copyright  2007 onwards Shane Elliot {@link http://pukunui.com}
22  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 define ('PROFILE_VISIBLE_ALL',     '2'); // Only visible for users with moodle/user:update capability.
26 define ('PROFILE_VISIBLE_PRIVATE', '1'); // Either we are viewing our own profile or we have moodle/user:update capability.
27 define ('PROFILE_VISIBLE_NONE',    '0'); // Only visible for moodle/user:update capability.
29 /**
30  * Base class for the customisable profile fields.
31  *
32  * @package core_user
33  * @copyright  2007 onwards Shane Elliot {@link http://pukunui.com}
34  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  */
36 class profile_field_base {
38     // These 2 variables are really what we're interested in.
39     // Everything else can be extracted from them.
41     /** @var int */
42     public $fieldid;
44     /** @var int */
45     public $userid;
47     /** @var stdClass */
48     public $field;
50     /** @var string */
51     public $inputname;
53     /** @var mixed */
54     public $data;
56     /** @var string */
57     public $dataformat;
59     /**
60      * Constructor method.
61      * @param int $fieldid id of the profile from the user_info_field table
62      * @param int $userid id of the user for whom we are displaying data
63      */
64     public function __construct($fieldid=0, $userid=0) {
65         global $USER;
67         $this->set_fieldid($fieldid);
68         $this->set_userid($userid);
69         $this->load_data();
70     }
72     /**
73      * Old syntax of class constructor. Deprecated in PHP7.
74      *
75      * @deprecated since Moodle 3.1
76      */
77     public function profile_field_base($fieldid=0, $userid=0) {
78         debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
79         self::__construct($fieldid, $userid);
80     }
82     /**
83      * Abstract method: Adds the profile field to the moodle form class
84      * @abstract The following methods must be overwritten by child classes
85      * @param moodleform $mform instance of the moodleform class
86      */
87     public function edit_field_add($mform) {
88         print_error('mustbeoveride', 'debug', '', 'edit_field_add');
89     }
91     /**
92      * Display the data for this field
93      * @return string
94      */
95     public function display_data() {
96         $options = new stdClass();
97         $options->para = false;
98         return format_text($this->data, FORMAT_MOODLE, $options);
99     }
101     /**
102      * Print out the form field in the edit profile page
103      * @param moodleform $mform instance of the moodleform class
104      * @return bool
105      */
106     public function edit_field($mform) {
107         if ($this->field->visible != PROFILE_VISIBLE_NONE
108           or has_capability('moodle/user:update', context_system::instance())) {
110             $this->edit_field_add($mform);
111             $this->edit_field_set_default($mform);
112             $this->edit_field_set_required($mform);
113             return true;
114         }
115         return false;
116     }
118     /**
119      * Tweaks the edit form
120      * @param moodleform $mform instance of the moodleform class
121      * @return bool
122      */
123     public function edit_after_data($mform) {
124         if ($this->field->visible != PROFILE_VISIBLE_NONE
125           or has_capability('moodle/user:update', context_system::instance())) {
126             $this->edit_field_set_locked($mform);
127             return true;
128         }
129         return false;
130     }
132     /**
133      * Saves the data coming from form
134      * @param stdClass $usernew data coming from the form
135      * @return mixed returns data id if success of db insert/update, false on fail, 0 if not permitted
136      */
137     public function edit_save_data($usernew) {
138         global $DB;
140         if (!isset($usernew->{$this->inputname})) {
141             // Field not present in form, probably locked and invisible - skip it.
142             return;
143         }
145         $data = new stdClass();
147         $usernew->{$this->inputname} = $this->edit_save_data_preprocess($usernew->{$this->inputname}, $data);
149         $data->userid  = $usernew->id;
150         $data->fieldid = $this->field->id;
151         $data->data    = $usernew->{$this->inputname};
153         if ($dataid = $DB->get_field('user_info_data', 'id', array('userid' => $data->userid, 'fieldid' => $data->fieldid))) {
154             $data->id = $dataid;
155             $DB->update_record('user_info_data', $data);
156         } else {
157             $DB->insert_record('user_info_data', $data);
158         }
159     }
161     /**
162      * Validate the form field from profile page
163      *
164      * @param stdClass $usernew
165      * @return  string  contains error message otherwise null
166      */
167     public function edit_validate_field($usernew) {
168         global $DB;
170         $errors = array();
171         // Get input value.
172         if (isset($usernew->{$this->inputname})) {
173             if (is_array($usernew->{$this->inputname}) && isset($usernew->{$this->inputname}['text'])) {
174                 $value = $usernew->{$this->inputname}['text'];
175             } else {
176                 $value = $usernew->{$this->inputname};
177             }
178         } else {
179             $value = '';
180         }
182         // Check for uniqueness of data if required.
183         if ($this->is_unique() && (($value !== '') || $this->is_required())) {
184             $data = $DB->get_records_sql('
185                     SELECT id, userid
186                       FROM {user_info_data}
187                      WHERE fieldid = ?
188                        AND ' . $DB->sql_compare_text('data', 255) . ' = ' . $DB->sql_compare_text('?', 255),
189                     array($this->field->id, $value));
190             if ($data) {
191                 $existing = false;
192                 foreach ($data as $v) {
193                     if ($v->userid == $usernew->id) {
194                         $existing = true;
195                         break;
196                     }
197                 }
198                 if (!$existing) {
199                     $errors[$this->inputname] = get_string('valuealreadyused');
200                 }
201             }
202         }
203         return $errors;
204     }
206     /**
207      * Sets the default data for the field in the form object
208      * @param  moodleform $mform instance of the moodleform class
209      */
210     public function edit_field_set_default($mform) {
211         if (!empty($default)) {
212             $mform->setDefault($this->inputname, $this->field->defaultdata);
213         }
214     }
216     /**
217      * Sets the required flag for the field in the form object
218      *
219      * @param moodleform $mform instance of the moodleform class
220      */
221     public function edit_field_set_required($mform) {
222         global $USER;
223         if ($this->is_required() && ($this->userid == $USER->id || isguestuser())) {
224             $mform->addRule($this->inputname, get_string('required'), 'required', null, 'client');
225         }
226     }
228     /**
229      * HardFreeze the field if locked.
230      * @param moodleform $mform instance of the moodleform class
231      */
232     public function edit_field_set_locked($mform) {
233         if (!$mform->elementExists($this->inputname)) {
234             return;
235         }
236         if ($this->is_locked() and !has_capability('moodle/user:update', context_system::instance())) {
237             $mform->hardFreeze($this->inputname);
238             $mform->setConstant($this->inputname, $this->data);
239         }
240     }
242     /**
243      * Hook for child classess to process the data before it gets saved in database
244      * @param stdClass $data
245      * @param stdClass $datarecord The object that will be used to save the record
246      * @return  mixed
247      */
248     public function edit_save_data_preprocess($data, $datarecord) {
249         return $data;
250     }
252     /**
253      * Loads a user object with data for this field ready for the edit profile
254      * form
255      * @param stdClass $user a user object
256      */
257     public function edit_load_user_data($user) {
258         if ($this->data !== null) {
259             $user->{$this->inputname} = $this->data;
260         }
261     }
263     /**
264      * Check if the field data should be loaded into the user object
265      * By default it is, but for field types where the data may be potentially
266      * large, the child class should override this and return false
267      * @return bool
268      */
269     public function is_user_object_data() {
270         return true;
271     }
273     /**
274      * Accessor method: set the userid for this instance
275      * @internal This method should not generally be overwritten by child classes.
276      * @param integer $userid id from the user table
277      */
278     public function set_userid($userid) {
279         $this->userid = $userid;
280     }
282     /**
283      * Accessor method: set the fieldid for this instance
284      * @internal This method should not generally be overwritten by child classes.
285      * @param integer $fieldid id from the user_info_field table
286      */
287     public function set_fieldid($fieldid) {
288         $this->fieldid = $fieldid;
289     }
291     /**
292      * Accessor method: Load the field record and user data associated with the
293      * object's fieldid and userid
294      * @internal This method should not generally be overwritten by child classes.
295      */
296     public function load_data() {
297         global $DB;
299         // Load the field object.
300         if (($this->fieldid == 0) or (!($field = $DB->get_record('user_info_field', array('id' => $this->fieldid))))) {
301             $this->field = null;
302             $this->inputname = '';
303         } else {
304             $this->field = $field;
305             $this->inputname = 'profile_field_'.$field->shortname;
306         }
308         if (!empty($this->field)) {
309             $params = array('userid' => $this->userid, 'fieldid' => $this->fieldid);
310             if ($data = $DB->get_record('user_info_data', $params, 'data, dataformat')) {
311                 $this->data = $data->data;
312                 $this->dataformat = $data->dataformat;
313             } else {
314                 $this->data = $this->field->defaultdata;
315                 $this->dataformat = FORMAT_HTML;
316             }
317         } else {
318             $this->data = null;
319         }
320     }
322     /**
323      * Check if the field data is visible to the current user
324      * @internal This method should not generally be overwritten by child classes.
325      * @return bool
326      */
327     public function is_visible() {
328         global $USER;
330         switch ($this->field->visible) {
331             case PROFILE_VISIBLE_ALL:
332                 return true;
333             case PROFILE_VISIBLE_PRIVATE:
334                 if ($this->userid == $USER->id) {
335                     return true;
336                 } else {
337                     return has_capability('moodle/user:viewalldetails',
338                             context_user::instance($this->userid));
339                 }
340             default:
341                 return has_capability('moodle/user:viewalldetails',
342                         context_user::instance($this->userid));
343         }
344     }
346     /**
347      * Check if the field data is considered empty
348      * @internal This method should not generally be overwritten by child classes.
349      * @return boolean
350      */
351     public function is_empty() {
352         return ( ($this->data != '0') and empty($this->data));
353     }
355     /**
356      * Check if the field is required on the edit profile page
357      * @internal This method should not generally be overwritten by child classes.
358      * @return bool
359      */
360     public function is_required() {
361         return (boolean)$this->field->required;
362     }
364     /**
365      * Check if the field is locked on the edit profile page
366      * @internal This method should not generally be overwritten by child classes.
367      * @return bool
368      */
369     public function is_locked() {
370         return (boolean)$this->field->locked;
371     }
373     /**
374      * Check if the field data should be unique
375      * @internal This method should not generally be overwritten by child classes.
376      * @return bool
377      */
378     public function is_unique() {
379         return (boolean)$this->field->forceunique;
380     }
382     /**
383      * Check if the field should appear on the signup page
384      * @internal This method should not generally be overwritten by child classes.
385      * @return bool
386      */
387     public function is_signup_field() {
388         return (boolean)$this->field->signup;
389     }
392 /**
393  * Loads user profile field data into the user object.
394  * @param stdClass $user
395  */
396 function profile_load_data($user) {
397     global $CFG, $DB;
399     if ($fields = $DB->get_records('user_info_field')) {
400         foreach ($fields as $field) {
401             require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
402             $newfield = 'profile_field_'.$field->datatype;
403             $formfield = new $newfield($field->id, $user->id);
404             $formfield->edit_load_user_data($user);
405         }
406     }
409 /**
410  * Print out the customisable categories and fields for a users profile
411  *
412  * @param moodleform $mform instance of the moodleform class
413  * @param int $userid id of user whose profile is being edited.
414  */
415 function profile_definition($mform, $userid = 0) {
416     global $CFG, $DB;
418     // If user is "admin" fields are displayed regardless.
419     $update = has_capability('moodle/user:update', context_system::instance());
421     if ($categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) {
422         foreach ($categories as $category) {
423             if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) {
425                 // Check first if *any* fields will be displayed.
426                 $display = false;
427                 foreach ($fields as $field) {
428                     if ($field->visible != PROFILE_VISIBLE_NONE) {
429                         $display = true;
430                     }
431                 }
433                 // Display the header and the fields.
434                 if ($display or $update) {
435                     $mform->addElement('header', 'category_'.$category->id, format_string($category->name));
436                     foreach ($fields as $field) {
437                         require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
438                         $newfield = 'profile_field_'.$field->datatype;
439                         $formfield = new $newfield($field->id, $userid);
440                         $formfield->edit_field($mform);
441                     }
442                 }
443             }
444         }
445     }
448 /**
449  * Adds profile fields to user edit forms.
450  * @param moodleform $mform
451  * @param int $userid
452  */
453 function profile_definition_after_data($mform, $userid) {
454     global $CFG, $DB;
456     $userid = ($userid < 0) ? 0 : (int)$userid;
458     if ($fields = $DB->get_records('user_info_field')) {
459         foreach ($fields as $field) {
460             require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
461             $newfield = 'profile_field_'.$field->datatype;
462             $formfield = new $newfield($field->id, $userid);
463             $formfield->edit_after_data($mform);
464         }
465     }
468 /**
469  * Validates profile data.
470  * @param stdClass $usernew
471  * @param array $files
472  * @return array
473  */
474 function profile_validation($usernew, $files) {
475     global $CFG, $DB;
477     $err = array();
478     if ($fields = $DB->get_records('user_info_field')) {
479         foreach ($fields as $field) {
480             require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
481             $newfield = 'profile_field_'.$field->datatype;
482             $formfield = new $newfield($field->id, $usernew->id);
483             $err += $formfield->edit_validate_field($usernew, $files);
484         }
485     }
486     return $err;
489 /**
490  * Saves profile data for a user.
491  * @param stdClass $usernew
492  */
493 function profile_save_data($usernew) {
494     global $CFG, $DB;
496     if ($fields = $DB->get_records('user_info_field')) {
497         foreach ($fields as $field) {
498             require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
499             $newfield = 'profile_field_'.$field->datatype;
500             $formfield = new $newfield($field->id, $usernew->id);
501             $formfield->edit_save_data($usernew);
502         }
503     }
506 /**
507  * Display profile fields.
508  * @param int $userid
509  */
510 function profile_display_fields($userid) {
511     global $CFG, $USER, $DB;
513     if ($categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) {
514         foreach ($categories as $category) {
515             if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) {
516                 foreach ($fields as $field) {
517                     require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
518                     $newfield = 'profile_field_'.$field->datatype;
519                     $formfield = new $newfield($field->id, $userid);
520                     if ($formfield->is_visible() and !$formfield->is_empty()) {
521                         echo html_writer::tag('dt', format_string($formfield->field->name));
522                         echo html_writer::tag('dd', $formfield->display_data());
523                     }
524                 }
525             }
526         }
527     }
530 /**
531  * Adds code snippet to a moodle form object for custom profile fields that
532  * should appear on the signup page
533  * @param moodleform $mform moodle form object
534  */
535 function profile_signup_fields($mform) {
536     global $CFG, $DB;
538     // Only retrieve required custom fields (with category information)
539     // results are sort by categories, then by fields.
540     $sql = "SELECT uf.id as fieldid, ic.id as categoryid, ic.name as categoryname, uf.datatype
541                 FROM {user_info_field} uf
542                 JOIN {user_info_category} ic
543                 ON uf.categoryid = ic.id AND uf.signup = 1 AND uf.visible<>0
544                 ORDER BY ic.sortorder ASC, uf.sortorder ASC";
546     if ( $fields = $DB->get_records_sql($sql)) {
547         foreach ($fields as $field) {
548             // Check if we change the categories.
549             if (!isset($currentcat) || $currentcat != $field->categoryid) {
550                  $currentcat = $field->categoryid;
551                  $mform->addElement('header', 'category_'.$field->categoryid, format_string($field->categoryname));
552             }
553             require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
554             $newfield = 'profile_field_'.$field->datatype;
555             $formfield = new $newfield($field->fieldid);
556             $formfield->edit_field($mform);
557         }
558     }
561 /**
562  * Returns an object with the custom profile fields set for the given user
563  * @param integer $userid
564  * @param bool $onlyinuserobject True if you only want the ones in $USER.
565  * @return stdClass
566  */
567 function profile_user_record($userid, $onlyinuserobject = true) {
568     global $CFG, $DB;
570     $usercustomfields = new stdClass();
572     if ($fields = $DB->get_records('user_info_field')) {
573         foreach ($fields as $field) {
574             require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
575             $newfield = 'profile_field_'.$field->datatype;
576             $formfield = new $newfield($field->id, $userid);
577             if (!$onlyinuserobject || $formfield->is_user_object_data()) {
578                 $usercustomfields->{$field->shortname} = $formfield->data;
579             }
580         }
581     }
583     return $usercustomfields;
586 /**
587  * Obtains a list of all available custom profile fields, indexed by id.
588  *
589  * Some profile fields are not included in the user object data (see
590  * profile_user_record function above). Optionally, you can obtain only those
591  * fields that are included in the user object.
592  *
593  * To be clear, this function returns the available fields, and does not
594  * return the field values for a particular user.
595  *
596  * @param bool $onlyinuserobject True if you only want the ones in $USER
597  * @return array Array of field objects from database (indexed by id)
598  * @since Moodle 2.7.1
599  */
600 function profile_get_custom_fields($onlyinuserobject = false) {
601     global $DB, $CFG;
603     // Get all the fields.
604     $fields = $DB->get_records('user_info_field', null, 'id ASC');
606     // If only doing the user object ones, unset the rest.
607     if ($onlyinuserobject) {
608         foreach ($fields as $id => $field) {
609             require_once($CFG->dirroot . '/user/profile/field/' .
610                     $field->datatype . '/field.class.php');
611             $newfield = 'profile_field_' . $field->datatype;
612             $formfield = new $newfield();
613             if (!$formfield->is_user_object_data()) {
614                 unset($fields[$id]);
615             }
616         }
617     }
619     return $fields;
622 /**
623  * Load custom profile fields into user object
624  *
625  * Please note originally in 1.9 we were using the custom field names directly,
626  * but it was causing unexpected collisions when adding new fields to user table,
627  * so instead we now use 'profile_' prefix.
628  *
629  * @param stdClass $user user object
630  */
631 function profile_load_custom_fields($user) {
632     $user->profile = (array)profile_user_record($user->id);
635 /**
636  * Trigger a user profile viewed event.
637  *
638  * @param stdClass  $user user  object
639  * @param stdClass  $context  context object (course or user)
640  * @param stdClass  $course course  object
641  * @since Moodle 2.9
642  */
643 function profile_view($user, $context, $course = null) {
645     $eventdata = array(
646         'objectid' => $user->id,
647         'relateduserid' => $user->id,
648         'context' => $context
649     );
651     if (!empty($course)) {
652         $eventdata['courseid'] = $course->id;
653         $eventdata['other'] = array(
654             'courseid' => $course->id,
655             'courseshortname' => $course->shortname,
656             'coursefullname' => $course->fullname
657         );
658     }
660     $event = \core\event\user_profile_viewed::create($eventdata);
661     $event->add_record_snapshot('user', $user);
662     $event->trigger();
665 /**
666  * Does the user have all required custom fields set?
667  *
668  * Internal, to be exclusively used by {@link user_not_fully_set_up()} only.
669  *
670  * Note that if users have no way to fill a required field via editing their
671  * profiles (e.g. the field is not visible or it is locked), we still return true.
672  * So this is actually checking if we should redirect the user to edit their
673  * profile, rather than whether there is a value in the database.
674  *
675  * @param int $userid
676  * @return bool
677  */
678 function profile_has_required_custom_fields_set($userid) {
679     global $DB;
681     $sql = "SELECT f.id
682               FROM {user_info_field} f
683          LEFT JOIN {user_info_data} d ON (d.fieldid = f.id AND d.userid = ?)
684              WHERE f.required = 1 AND f.visible > 0 AND f.locked = 0 AND d.id IS NULL";
686     if ($DB->record_exists_sql($sql, [$userid])) {
687         return false;
688     }
690     return true;