MDL-31465 core_user: Suspended field must be boolean
[moodle.git] / user / lib.php
CommitLineData
fb79269b 1<?php
fb79269b 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/>.
16
17/**
18 * External user API
19 *
a2ed6e69
SH
20 * @package core_user
21 * @copyright 2009 Moodle Pty Ltd (http://moodle.com)
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
fb79269b 23 */
24
fb79269b 25/**
26 * Creates a user
adfb459c 27 *
a2ed6e69 28 * @throws moodle_exception
bb78e249
RT
29 * @param stdClass $user user to create
30 * @param bool $updatepassword if true, authentication plugin will update password.
2b55cb1b
RT
31 * @param bool $triggerevent set false if user_created event should not be triggred.
32 * This will not affect user_password_updated event triggering.
fb79269b 33 * @return int id of the newly created user
34 */
2b55cb1b 35function user_create_user($user, $updatepassword = true, $triggerevent = true) {
ac9768fc 36 global $DB;
fb79269b 37
bb78e249 38 // Set the timecreate field to the current time.
fb79269b 39 if (!is_object($user)) {
8bf0f207 40 $user = (object) $user;
fb79269b 41 }
bd0f26bd 42
bb78e249 43 // Check username.
2f1e464a 44 if ($user->username !== core_text::strtolower($user->username)) {
45b4464c
JM
45 throw new moodle_exception('usernamelowercase');
46 } else {
7a067206 47 if ($user->username !== core_user::clean_field($user->username, 'username')) {
45b4464c
JM
48 throw new moodle_exception('invalidusername');
49 }
50 }
51
bb78e249
RT
52 // Save the password in a temp value for later.
53 if ($updatepassword && isset($user->password)) {
adfb459c 54
bb78e249 55 // Check password toward the password policy.
adfb459c
JM
56 if (!check_password_policy($user->password, $errmsg)) {
57 throw new moodle_exception($errmsg);
58 }
59
60 $userpassword = $user->password;
61 unset($user->password);
62 }
bd0f26bd 63
9f7379e9 64 // Apply default values for user preferences that are stored in users table.
7a067206 65 if (!isset($user->calendartype)) {
ac9768fc 66 $user->calendartype = core_user::get_property_default('calendartype');
8bf0f207 67 }
9f7379e9 68 if (!isset($user->maildisplay)) {
ac9768fc 69 $user->maildisplay = core_user::get_property_default('maildisplay');
9f7379e9
MG
70 }
71 if (!isset($user->mailformat)) {
ac9768fc 72 $user->mailformat = core_user::get_property_default('mailformat');
9f7379e9
MG
73 }
74 if (!isset($user->maildigest)) {
ac9768fc 75 $user->maildigest = core_user::get_property_default('maildigest');
9f7379e9
MG
76 }
77 if (!isset($user->autosubscribe)) {
ac9768fc 78 $user->autosubscribe = core_user::get_property_default('autosubscribe');
9f7379e9
MG
79 }
80 if (!isset($user->trackforums)) {
ac9768fc 81 $user->trackforums = core_user::get_property_default('trackforums');
9f7379e9 82 }
7e670032 83 if (!isset($user->lang)) {
ac9768fc 84 $user->lang = core_user::get_property_default('lang');
7e670032 85 }
9f7379e9 86
fb79269b 87 $user->timecreated = time();
bd0f26bd 88 $user->timemodified = $user->timecreated;
fb79269b 89
7a067206
SL
90 // Validate user data object.
91 $uservalidation = core_user::validate($user);
92 if ($uservalidation !== true) {
93 foreach ($uservalidation as $field => $message) {
94 debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER);
95 $user->$field = core_user::clean_field($user->$field, $field);
96 }
97 }
98
bb78e249 99 // Insert the user into the database.
fb79269b 100 $newuserid = $DB->insert_record('user', $user);
101
bb78e249
RT
102 // Create USER context for this user.
103 $usercontext = context_user::instance($newuserid);
b6dcb7d9 104
bb78e249 105 // Update user password if necessary.
adfb459c 106 if (isset($userpassword)) {
bb78e249
RT
107 // Get full database user row, in case auth is default.
108 $newuser = $DB->get_record('user', array('id' => $newuserid));
adfb459c
JM
109 $authplugin = get_auth_plugin($newuser->auth);
110 $authplugin->user_update_password($newuser, $userpassword);
111 }
112
2b55cb1b
RT
113 // Trigger event If required.
114 if ($triggerevent) {
115 \core\event\user_created::create_from_userid($newuserid)->trigger();
116 }
adfb459c 117
fb79269b 118 return $newuserid;
fb79269b 119}
120
121/**
122 * Update a user with a user object (will compare against the ID)
adfb459c 123 *
a2ed6e69 124 * @throws moodle_exception
bb78e249
RT
125 * @param stdClass $user the user to update
126 * @param bool $updatepassword if true, authentication plugin will update password.
2b55cb1b
RT
127 * @param bool $triggerevent set false if user_updated event should not be triggred.
128 * This will not affect user_password_updated event triggering.
fb79269b 129 */
2b55cb1b 130function user_update_user($user, $updatepassword = true, $triggerevent = true) {
fb79269b 131 global $DB;
bd0f26bd 132
a2ed6e69 133 // Set the timecreate field to the current time.
bd0f26bd 134 if (!is_object($user)) {
8bf0f207 135 $user = (object) $user;
bd0f26bd 136 }
adfb459c 137
a2ed6e69 138 // Check username.
45b4464c 139 if (isset($user->username)) {
2f1e464a 140 if ($user->username !== core_text::strtolower($user->username)) {
45b4464c
JM
141 throw new moodle_exception('usernamelowercase');
142 } else {
7a067206 143 if ($user->username !== core_user::clean_field($user->username, 'username')) {
45b4464c
JM
144 throw new moodle_exception('invalidusername');
145 }
146 }
147 }
148
bb78e249
RT
149 // Unset password here, for updating later, if password update is required.
150 if ($updatepassword && isset($user->password)) {
adfb459c 151
a2ed6e69 152 // Check password toward the password policy.
adfb459c
JM
153 if (!check_password_policy($user->password, $errmsg)) {
154 throw new moodle_exception($errmsg);
155 }
156
9e63c0ff
FS
157 $passwd = $user->password;
158 unset($user->password);
159 }
bd0f26bd 160
8bf0f207 161 // Make sure calendartype, if set, is valid.
7a067206 162 if (empty($user->calendartype)) {
8bf0f207
MN
163 // Unset this variable, must be an empty string, which we do not want to update the calendartype to.
164 unset($user->calendartype);
165 }
166
bd0f26bd 167 $user->timemodified = time();
7a067206
SL
168
169 // Validate user data object.
170 $uservalidation = core_user::validate($user);
171 if ($uservalidation !== true) {
172 foreach ($uservalidation as $field => $message) {
173 debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER);
174 $user->$field = core_user::clean_field($user->$field, $field);
175 }
176 }
177
fb79269b 178 $DB->update_record('user', $user);
b6dcb7d9 179
bb78e249
RT
180 if ($updatepassword) {
181 // Get full user record.
182 $updateduser = $DB->get_record('user', array('id' => $user->id));
9e63c0ff 183
a2ed6e69 184 // If password was set, then update its hash.
bb78e249
RT
185 if (isset($passwd)) {
186 $authplugin = get_auth_plugin($updateduser->auth);
187 if ($authplugin->can_change_password()) {
188 $authplugin->user_update_password($updateduser, $passwd);
189 }
adfb459c
JM
190 }
191 }
2b55cb1b
RT
192 // Trigger event if required.
193 if ($triggerevent) {
194 \core\event\user_updated::create_from_userid($user->id)->trigger();
195 }
adfb459c 196}
fb79269b 197
198/**
199 * Marks user deleted in internal user database and notifies the auth plugin.
200 * Also unenrols user from all roles and does other cleanup.
201 *
202 * @todo Decide if this transaction is really needed (look for internal TODO:)
203 * @param object $user Userobject before delete (without system magic quotes)
204 * @return boolean success
205 */
206function user_delete_user($user) {
45fb2cf8 207 return delete_user($user);
fb79269b 208}
209
210/**
211 * Get users by id
fb79269b 212 *
a2ed6e69
SH
213 * @param array $userids id of users to retrieve
214 * @return array
fb79269b 215 */
216function user_get_users_by_id($userids) {
217 global $DB;
218 return $DB->get_records_list('user', 'id', $userids);
219}
b1627a92 220
61c8e0d7
FM
221/**
222 * Returns the list of default 'displayable' fields
223 *
224 * Contains database field names but also names used to generate information, such as enrolledcourses
225 *
226 * @return array of user fields
227 */
228function user_get_default_fields() {
229 return array( 'id', 'username', 'fullname', 'firstname', 'lastname', 'email',
230 'address', 'phone1', 'phone2', 'icq', 'skype', 'yahoo', 'aim', 'msn', 'department',
231 'institution', 'interests', 'firstaccess', 'lastaccess', 'auth', 'confirmed',
232 'idnumber', 'lang', 'theme', 'timezone', 'mailformat', 'description', 'descriptionformat',
233 'city', 'url', 'country', 'profileimageurlsmall', 'profileimageurl', 'customfields',
511db621 234 'groups', 'roles', 'preferences', 'enrolledcourses', 'suspended'
61c8e0d7
FM
235 );
236}
01479290
DC
237
238/**
239 *
a2ed6e69 240 * Give user record from mdl_user, build an array contains all user details.
93ce0e82
JM
241 *
242 * Warning: description file urls are 'webservice/pluginfile.php' is use.
243 * it can be changed with $CFG->moodlewstextformatlinkstoimagesfile
244 *
a2ed6e69 245 * @throws moodle_exception
01479290 246 * @param stdClass $user user record from mdl_user
01479290 247 * @param stdClass $course moodle course
ad7612f5 248 * @param array $userfields required fields
d6731600 249 * @return array|null
01479290 250 */
ad7612f5 251function user_get_user_details($user, $course = null, array $userfields = array()) {
1f7273af 252 global $USER, $DB, $CFG, $PAGE;
a2ed6e69
SH
253 require_once($CFG->dirroot . "/user/profile/lib.php"); // Custom field library.
254 require_once($CFG->dirroot . "/lib/filelib.php"); // File handling on description and friends.
01479290 255
61c8e0d7 256 $defaultfields = user_get_default_fields();
ad7612f5
DC
257
258 if (empty($userfields)) {
259 $userfields = $defaultfields;
260 }
261
262 foreach ($userfields as $thefield) {
263 if (!in_array($thefield, $defaultfields)) {
264 throw new moodle_exception('invaliduserfield', 'error', '', $thefield);
265 }
266 }
267
a2ed6e69 268 // Make sure id and fullname are included.
ad7612f5
DC
269 if (!in_array('id', $userfields)) {
270 $userfields[] = 'id';
271 }
272
273 if (!in_array('fullname', $userfields)) {
274 $userfields[] = 'fullname';
275 }
276
01479290 277 if (!empty($course)) {
43731030
FM
278 $context = context_course::instance($course->id);
279 $usercontext = context_user::instance($user->id);
1e539f64 280 $canviewdetailscap = (has_capability('moodle/user:viewdetails', $context) || has_capability('moodle/user:viewdetails', $usercontext));
01479290 281 } else {
43731030 282 $context = context_user::instance($user->id);
01479290 283 $usercontext = $context;
1e539f64 284 $canviewdetailscap = has_capability('moodle/user:viewdetails', $usercontext);
01479290
DC
285 }
286
287 $currentuser = ($user->id == $USER->id);
288 $isadmin = is_siteadmin($USER);
289
48a7b182
JM
290 $showuseridentityfields = get_extra_user_fields($context);
291
01479290
DC
292 if (!empty($course)) {
293 $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
294 } else {
295 $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
296 }
86477112 297 $canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
01479290
DC
298 if (!empty($course)) {
299 $canviewuseremail = has_capability('moodle/course:useremail', $context);
300 } else {
301 $canviewuseremail = false;
302 }
a2ed6e69 303 $cannotviewdescription = !empty($CFG->profilesforenrolledusersonly) && !$currentuser && !$DB->record_exists('role_assignments', array('userid' => $user->id));
01479290
DC
304 if (!empty($course)) {
305 $canaccessallgroups = has_capability('moodle/site:accessallgroups', $context);
306 } else {
307 $canaccessallgroups = false;
308 }
309
310 if (!$currentuser && !$canviewdetailscap && !has_coursecontact_role($user->id)) {
a2ed6e69 311 // Skip this user details.
01479290
DC
312 return null;
313 }
314
315 $userdetails = array();
316 $userdetails['id'] = $user->id;
317
07d37084
DM
318 if (in_array('username', $userfields)) {
319 if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) {
320 $userdetails['username'] = $user->username;
321 }
01479290
DC
322 }
323 if ($isadmin or $canviewfullnames) {
ad7612f5
DC
324 if (in_array('firstname', $userfields)) {
325 $userdetails['firstname'] = $user->firstname;
326 }
327 if (in_array('lastname', $userfields)) {
328 $userdetails['lastname'] = $user->lastname;
329 }
01479290
DC
330 }
331 $userdetails['fullname'] = fullname($user);
332
ad7612f5
DC
333 if (in_array('customfields', $userfields)) {
334 $fields = $DB->get_recordset_sql("SELECT f.*
335 FROM {user_info_field} f
336 JOIN {user_info_category} c
337 ON f.categoryid=c.id
338 ORDER BY c.sortorder ASC, f.sortorder ASC");
339 $userdetails['customfields'] = array();
340 foreach ($fields as $field) {
341 require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
342 $newfield = 'profile_field_'.$field->datatype;
343 $formfield = new $newfield($field->id, $user->id);
344 if ($formfield->is_visible() and !$formfield->is_empty()) {
f804c730 345
e96e66aa
EL
346 // TODO: Part of MDL-50728, this conditional coding must be moved to
347 // proper profile fields API so they are self-contained.
f804c730
JL
348 // We only use display_data in fields that require text formatting.
349 if ($field->datatype == 'text' or $field->datatype == 'textarea') {
350 $fieldvalue = $formfield->display_data();
351 } else {
352 // Cases: datetime, checkbox and menu.
353 $fieldvalue = $formfield->data;
354 }
355
ad7612f5 356 $userdetails['customfields'][] =
f804c730 357 array('name' => $formfield->field->name, 'value' => $fieldvalue,
ad7612f5
DC
358 'type' => $field->datatype, 'shortname' => $formfield->field->shortname);
359 }
360 }
361 $fields->close();
a2ed6e69 362 // Unset customfields if it's empty.
ad7612f5
DC
363 if (empty($userdetails['customfields'])) {
364 unset($userdetails['customfields']);
01479290 365 }
01479290
DC
366 }
367
a2ed6e69 368 // Profile image.
ad7612f5 369 if (in_array('profileimageurl', $userfields)) {
1f7273af
DP
370 $userpicture = new user_picture($user);
371 $userpicture->size = 1; // Size f1.
372 $userdetails['profileimageurl'] = $userpicture->get_url($PAGE)->out(false);
ad7612f5
DC
373 }
374 if (in_array('profileimageurlsmall', $userfields)) {
1f7273af
DP
375 if (!isset($userpicture)) {
376 $userpicture = new user_picture($user);
377 }
378 $userpicture->size = 0; // Size f2.
379 $userdetails['profileimageurlsmall'] = $userpicture->get_url($PAGE)->out(false);
ad7612f5 380 }
01479290 381
a2ed6e69 382 // Hidden user field.
01479290
DC
383 if ($canviewhiddenuserfields) {
384 $hiddenfields = array();
a2ed6e69
SH
385 // Address, phone1 and phone2 not appears in hidden fields list but require viewhiddenfields capability
386 // according to user/profile.php.
ad7612f5 387 if ($user->address && in_array('address', $userfields)) {
01479290
DC
388 $userdetails['address'] = $user->address;
389 }
01479290
DC
390 } else {
391 $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
392 }
393
48a7b182 394 if ($user->phone1 && in_array('phone1', $userfields) &&
58f739c5 395 (in_array('phone1', $showuseridentityfields) or $canviewhiddenuserfields)) {
48a7b182
JM
396 $userdetails['phone1'] = $user->phone1;
397 }
398 if ($user->phone2 && in_array('phone2', $userfields) &&
58f739c5 399 (in_array('phone2', $showuseridentityfields) or $canviewhiddenuserfields)) {
48a7b182
JM
400 $userdetails['phone2'] = $user->phone2;
401 }
402
acf64596
JM
403 if (isset($user->description) &&
404 ((!isset($hiddenfields['description']) && !$cannotviewdescription) or $isadmin)) {
405 if (in_array('description', $userfields)) {
406 // Always return the descriptionformat if description is requested.
407 list($userdetails['description'], $userdetails['descriptionformat']) =
408 external_format_text($user->description, $user->descriptionformat,
409 $usercontext->id, 'user', 'profile', null);
01479290
DC
410 }
411 }
412
ad7612f5 413 if (in_array('country', $userfields) && (!isset($hiddenfields['country']) or $isadmin) && $user->country) {
01479290
DC
414 $userdetails['country'] = $user->country;
415 }
416
ad7612f5 417 if (in_array('city', $userfields) && (!isset($hiddenfields['city']) or $isadmin) && $user->city) {
01479290
DC
418 $userdetails['city'] = $user->city;
419 }
420
ad7612f5 421 if (in_array('url', $userfields) && $user->url && (!isset($hiddenfields['webpage']) or $isadmin)) {
01479290
DC
422 $url = $user->url;
423 if (strpos($user->url, '://') === false) {
424 $url = 'http://'. $url;
425 }
426 $user->url = clean_param($user->url, PARAM_URL);
427 $userdetails['url'] = $user->url;
428 }
429
ad7612f5 430 if (in_array('icq', $userfields) && $user->icq && (!isset($hiddenfields['icqnumber']) or $isadmin)) {
01479290
DC
431 $userdetails['icq'] = $user->icq;
432 }
433
ad7612f5 434 if (in_array('skype', $userfields) && $user->skype && (!isset($hiddenfields['skypeid']) or $isadmin)) {
01479290
DC
435 $userdetails['skype'] = $user->skype;
436 }
ad7612f5 437 if (in_array('yahoo', $userfields) && $user->yahoo && (!isset($hiddenfields['yahooid']) or $isadmin)) {
01479290
DC
438 $userdetails['yahoo'] = $user->yahoo;
439 }
ad7612f5 440 if (in_array('aim', $userfields) && $user->aim && (!isset($hiddenfields['aimid']) or $isadmin)) {
01479290
DC
441 $userdetails['aim'] = $user->aim;
442 }
ad7612f5 443 if (in_array('msn', $userfields) && $user->msn && (!isset($hiddenfields['msnid']) or $isadmin)) {
01479290
DC
444 $userdetails['msn'] = $user->msn;
445 }
511db621 446 if (in_array('suspended', $userfields) && (!isset($hiddenfields['suspended']) or $isadmin)) {
84900149 447 $userdetails['suspended'] = (bool)$user->suspended;
511db621 448 }
01479290 449
ad7612f5 450 if (in_array('firstaccess', $userfields) && (!isset($hiddenfields['firstaccess']) or $isadmin)) {
01479290
DC
451 if ($user->firstaccess) {
452 $userdetails['firstaccess'] = $user->firstaccess;
453 } else {
454 $userdetails['firstaccess'] = 0;
455 }
456 }
ad7612f5 457 if (in_array('lastaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) {
01479290
DC
458 if ($user->lastaccess) {
459 $userdetails['lastaccess'] = $user->lastaccess;
460 } else {
461 $userdetails['lastaccess'] = 0;
462 }
463 }
464
a2ed6e69
SH
465 if (in_array('email', $userfields) && ($isadmin // The admin is allowed the users email.
466 or $currentuser // Of course the current user is as well.
467 or $canviewuseremail // This is a capability in course context, it will be false in usercontext.
58f739c5 468 or in_array('email', $showuseridentityfields)
01479290 469 or $user->maildisplay == 1
ca774c94 470 or ($user->maildisplay == 2 and enrol_sharing_course($user, $USER)))) {
90e30b96 471 $userdetails['email'] = $user->email;
01479290
DC
472 }
473
c4e868d5 474 if (in_array('interests', $userfields)) {
e11d7380 475 $interests = core_tag_tag::get_item_tags_array('core', 'user', $user->id, core_tag_tag::BOTH_STANDARD_AND_NOT, 0, false);
c4e868d5
MG
476 if ($interests) {
477 $userdetails['interests'] = join(', ', $interests);
01479290
DC
478 }
479 }
480
a2ed6e69 481 // Departement/Institution/Idnumber are not displayed on any profile, however you can get them from editing profile.
07d37084
DM
482 if (in_array('idnumber', $userfields) && $user->idnumber) {
483 if (in_array('idnumber', $showuseridentityfields) or $currentuser or
484 has_capability('moodle/user:viewalldetails', $context)) {
3a3f3b22
CW
485 $userdetails['idnumber'] = $user->idnumber;
486 }
48a7b182 487 }
07d37084
DM
488 if (in_array('institution', $userfields) && $user->institution) {
489 if (in_array('institution', $showuseridentityfields) or $currentuser or
490 has_capability('moodle/user:viewalldetails', $context)) {
01479290
DC
491 $userdetails['institution'] = $user->institution;
492 }
48a7b182 493 }
07d37084
DM
494 // Isset because it's ok to have department 0.
495 if (in_array('department', $userfields) && isset($user->department)) {
496 if (in_array('department', $showuseridentityfields) or $currentuser or
497 has_capability('moodle/user:viewalldetails', $context)) {
01479290
DC
498 $userdetails['department'] = $user->department;
499 }
500 }
501
ad7612f5 502 if (in_array('roles', $userfields) && !empty($course)) {
a2ed6e69 503 // Not a big secret.
01479290
DC
504 $roles = get_user_roles($context, $user->id, false);
505 $userdetails['roles'] = array();
506 foreach ($roles as $role) {
507 $userdetails['roles'][] = array(
508 'roleid' => $role->roleid,
509 'name' => $role->name,
510 'shortname' => $role->shortname,
511 'sortorder' => $role->sortorder
512 );
513 }
514 }
515
a2ed6e69 516 // If groups are in use and enforced throughout the course, then make sure we can meet in at least one course level group.
ad7612f5 517 if (in_array('groups', $userfields) && !empty($course) && $canaccessallgroups) {
93ce0e82
JM
518 $usergroups = groups_get_all_groups($course->id, $user->id, $course->defaultgroupingid,
519 'g.id, g.name,g.description,g.descriptionformat');
01479290
DC
520 $userdetails['groups'] = array();
521 foreach ($usergroups as $group) {
93ce0e82
JM
522 list($group->description, $group->descriptionformat) =
523 external_format_text($group->description, $group->descriptionformat,
524 $context->id, 'group', 'description', $group->id);
a2ed6e69
SH
525 $userdetails['groups'][] = array('id' => $group->id, 'name' => $group->name,
526 'description' => $group->description, 'descriptionformat' => $group->descriptionformat);
01479290
DC
527 }
528 }
a2ed6e69 529 // List of courses where the user is enrolled.
ad7612f5 530 if (in_array('enrolledcourses', $userfields) && !isset($hiddenfields['mycourses'])) {
01479290
DC
531 $enrolledcourses = array();
532 if ($mycourses = enrol_get_users_courses($user->id, true)) {
533 foreach ($mycourses as $mycourse) {
534 if ($mycourse->category) {
43731030 535 $coursecontext = context_course::instance($mycourse->id);
01479290
DC
536 $enrolledcourse = array();
537 $enrolledcourse['id'] = $mycourse->id;
43ff71a0 538 $enrolledcourse['fullname'] = format_string($mycourse->fullname, true, array('context' => $coursecontext));
8ebbb06a 539 $enrolledcourse['shortname'] = format_string($mycourse->shortname, true, array('context' => $coursecontext));
01479290
DC
540 $enrolledcourses[] = $enrolledcourse;
541 }
542 }
543 $userdetails['enrolledcourses'] = $enrolledcourses;
544 }
545 }
546
a2ed6e69 547 // User preferences.
ad7612f5 548 if (in_array('preferences', $userfields) && $currentuser) {
01479290
DC
549 $preferences = array();
550 $userpreferences = get_user_preferences();
a2ed6e69 551 foreach ($userpreferences as $prefname => $prefvalue) {
01479290 552 $preferences[] = array('name' => $prefname, 'value' => $prefvalue);
a2ed6e69
SH
553 }
554 $userdetails['preferences'] = $preferences;
01479290 555 }
ad7612f5 556
01479290
DC
557 return $userdetails;
558}
559
86477112
FS
560/**
561 * Tries to obtain user details, either recurring directly to the user's system profile
c70b9853 562 * or through one of the user's course enrollments (course profile).
86477112 563 *
a2ed6e69 564 * @param stdClass $user The user.
c70b9853 565 * @return array if unsuccessful or the allowed user details.
86477112
FS
566 */
567function user_get_user_details_courses($user) {
568 global $USER;
569 $userdetails = null;
570
a2ed6e69 571 // Get the courses that the user is enrolled in (only active).
86477112
FS
572 $courses = enrol_get_users_courses($user->id, true);
573
c70b9853
JM
574 $systemprofile = false;
575 if (can_view_user_details_cap($user) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) {
576 $systemprofile = true;
577 }
86477112 578
c70b9853 579 // Try using system profile.
86477112
FS
580 if ($systemprofile) {
581 $userdetails = user_get_user_details($user, null);
582 } else {
c70b9853 583 // Try through course profile.
86477112 584 foreach ($courses as $course) {
854859e1 585 if (can_view_user_details_cap($user, $course) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) {
86477112
FS
586 $userdetails = user_get_user_details($user, $course);
587 }
588 }
589 }
590
591 return $userdetails;
592}
593
594/**
c70b9853
JM
595 * Check if $USER have the necessary capabilities to obtain user details.
596 *
a2ed6e69
SH
597 * @param stdClass $user
598 * @param stdClass $course if null then only consider system profile otherwise also consider the course's profile.
c70b9853 599 * @return bool true if $USER can view user details.
86477112
FS
600 */
601function can_view_user_details_cap($user, $course = null) {
c70b9853 602 // Check $USER has the capability to view the user details at user context.
bea86e84 603 $usercontext = context_user::instance($user->id);
c70b9853
JM
604 $result = has_capability('moodle/user:viewdetails', $usercontext);
605 // Otherwise can $USER see them at course context.
606 if (!$result && !empty($course)) {
bea86e84 607 $context = context_course::instance($course->id);
c70b9853 608 $result = has_capability('moodle/user:viewdetails', $context);
86477112
FS
609 }
610 return $result;
611}
612
b1627a92
DC
613/**
614 * Return a list of page types
615 * @param string $pagetype current page type
616 * @param stdClass $parentcontext Block's parent context
617 * @param stdClass $currentcontext Current context of block
a2ed6e69 618 * @return array
b1627a92 619 */
b38e2e28 620function user_page_type_list($pagetype, $parentcontext, $currentcontext) {
a2ed6e69 621 return array('user-profile' => get_string('page-user-profile', 'pagetype'));
d6731600 622}
52dc1de7
AA
623
624/**
625 * Count the number of failed login attempts for the given user, since last successful login.
626 *
627 * @param int|stdclass $user user id or object.
628 * @param bool $reset Resets failed login count, if set to true.
629 *
630 * @return int number of failed login attempts since the last successful login.
631 */
632function user_count_login_failures($user, $reset = true) {
633 global $DB;
634
635 if (!is_object($user)) {
636 $user = $DB->get_record('user', array('id' => $user), '*', MUST_EXIST);
637 }
638 if ($user->deleted) {
639 // Deleted user, nothing to do.
640 return 0;
641 }
642 $count = get_user_preferences('login_failed_count_since_success', 0, $user);
643 if ($reset) {
644 set_user_preference('login_failed_count_since_success', 0, $user);
645 }
646 return $count;
647}
648
b865a3e2 649/**
22893d6f
JC
650 * Converts a string into a flat array of menu items, where each menu items is a
651 * stdClass with fields type, url, title, pix, and imgsrc.
b865a3e2
JC
652 *
653 * @param string $text the menu items definition
654 * @param moodle_page $page the current page
655 * @return array
656 */
657function user_convert_text_to_menu_items($text, $page) {
658 global $OUTPUT, $CFG;
659
660 $lines = explode("\n", $text);
661 $items = array();
662 $lastchild = null;
663 $lastdepth = null;
664 $lastsort = 0;
088fcf90 665 $children = array();
b865a3e2
JC
666 foreach ($lines as $line) {
667 $line = trim($line);
668 $bits = explode('|', $line, 3);
22893d6f
JC
669 $itemtype = 'link';
670 if (preg_match("/^#+$/", $line)) {
671 $itemtype = 'divider';
672 } else if (!array_key_exists(0, $bits) or empty($bits[0])) {
b865a3e2
JC
673 // Every item must have a name to be valid.
674 continue;
675 } else {
676 $bits[0] = ltrim($bits[0], '-');
677 }
678
679 // Create the child.
680 $child = new stdClass();
22893d6f
JC
681 $child->itemtype = $itemtype;
682 if ($itemtype === 'divider') {
683 // Add the divider to the list of children and skip link
684 // processing.
685 $children[] = $child;
686 continue;
687 }
b865a3e2
JC
688
689 // Name processing.
690 $namebits = explode(',', $bits[0], 2);
691 if (count($namebits) == 2) {
06c27531
AN
692 // Check the validity of the identifier part of the string.
693 if (clean_param($namebits[0], PARAM_STRINGID) !== '') {
694 // Treat this as a language string.
695 $child->title = get_string($namebits[0], $namebits[1]);
d7dbef73 696 $child->titleidentifier = implode(',', $namebits);
06c27531
AN
697 }
698 }
699 if (empty($child->title)) {
b865a3e2
JC
700 // Use it as is, don't even clean it.
701 $child->title = $bits[0];
d7dbef73 702 $child->titleidentifier = str_replace(" ", "-", $bits[0]);
b865a3e2
JC
703 }
704
705 // URL processing.
706 if (!array_key_exists(1, $bits) or empty($bits[1])) {
22893d6f 707 // Set the url to null, and set the itemtype to invalid.
b865a3e2 708 $bits[1] = null;
22893d6f 709 $child->itemtype = "invalid";
b865a3e2 710 } else {
4887d152 711 // Nasty hack to replace the grades with the direct url.
c9451960
AG
712 if (strpos($bits[1], '/grade/report/mygrades.php') !== false) {
713 $bits[1] = user_mygrades_url();
714 }
715
b865a3e2
JC
716 // Make sure the url is a moodle url.
717 $bits[1] = new moodle_url(trim($bits[1]));
718 }
719 $child->url = $bits[1];
720
721 // PIX processing.
722 $pixpath = "t/edit";
723 if (!array_key_exists(2, $bits) or empty($bits[2])) {
724 // Use the default.
725 $child->pix = $pixpath;
726 } else {
727 // Check for the specified image existing.
728 $pixpath = "t/" . $bits[2];
729 if ($page->theme->resolve_image_location($pixpath, 'moodle', true)) {
730 // Use the image.
731 $child->pix = $pixpath;
732 } else {
733 // Treat it like a URL.
734 $child->pix = null;
735 $child->imgsrc = $bits[2];
736 }
737 }
738
739 // Add this child to the list of children.
740 $children[] = $child;
741 }
742 return $children;
743}
744
6da0e4cf
JC
745/**
746 * Get a list of essential user navigation items.
747 *
748 * @param stdclass $user user object.
749 * @param moodle_page $page page object.
9dcd5035
DB
750 * @param array $options associative array.
751 * options are:
752 * - avatarsize=35 (size of avatar image)
6da0e4cf
JC
753 * @return stdClass $returnobj navigation information object, where:
754 *
755 * $returnobj->navitems array array of links where each link is a
756 * stdClass with fields url, title, and
757 * pix
758 * $returnobj->metadata array array of useful user metadata to be
759 * used when constructing navigation;
760 * fields include:
761 *
762 * ROLE FIELDS
763 * asotherrole bool whether viewing as another role
764 * rolename string name of the role
765 *
766 * USER FIELDS
767 * These fields are for the currently-logged in user, or for
768 * the user that the real user is currently logged in as.
769 *
770 * userid int the id of the user in question
771 * userfullname string the user's full name
772 * userprofileurl moodle_url the url of the user's profile
773 * useravatar string a HTML fragment - the rendered
774 * user_picture for this user
775 * userloginfail string an error string denoting the number
776 * of login failures since last login
777 *
778 * "REAL USER" FIELDS
779 * These fields are for when asotheruser is true, and
780 * correspond to the underlying "real user".
781 *
782 * asotheruser bool whether viewing as another user
783 * realuserid int the id of the user in question
784 * realuserfullname string the user's full name
785 * realuserprofileurl moodle_url the url of the user's profile
786 * realuseravatar string a HTML fragment - the rendered
787 * user_picture for this user
788 *
789 * MNET PROVIDER FIELDS
790 * asmnetuser bool whether viewing as a user from an
791 * MNet provider
792 * mnetidprovidername string name of the MNet provider
793 * mnetidproviderwwwroot string URL of the MNet provider
794 */
c68b3206 795function user_get_user_navigation_info($user, $page, $options = array()) {
6da0e4cf
JC
796 global $OUTPUT, $DB, $SESSION, $CFG;
797
798 $returnobject = new stdClass();
799 $returnobject->navitems = array();
800 $returnobject->metadata = array();
801
802 $course = $page->course;
803
804 // Query the environment.
805 $context = context_course::instance($course->id);
806
807 // Get basic user metadata.
808 $returnobject->metadata['userid'] = $user->id;
809 $returnobject->metadata['userfullname'] = fullname($user, true);
810 $returnobject->metadata['userprofileurl'] = new moodle_url('/user/profile.php', array(
811 'id' => $user->id
812 ));
9dcd5035
DB
813
814 $avataroptions = array('link' => false, 'visibletoscreenreaders' => false);
815 if (!empty($options['avatarsize'])) {
816 $avataroptions['size'] = $options['avatarsize'];
817 }
6da0e4cf 818 $returnobject->metadata['useravatar'] = $OUTPUT->user_picture (
9dcd5035 819 $user, $avataroptions
6da0e4cf
JC
820 );
821 // Build a list of items for a regular user.
822
823 // Query MNet status.
824 if ($returnobject->metadata['asmnetuser'] = is_mnet_remote_user($user)) {
825 $mnetidprovider = $DB->get_record('mnet_host', array('id' => $user->mnethostid));
826 $returnobject->metadata['mnetidprovidername'] = $mnetidprovider->name;
827 $returnobject->metadata['mnetidproviderwwwroot'] = $mnetidprovider->wwwroot;
828 }
829
830 // Did the user just log in?
831 if (isset($SESSION->justloggedin)) {
832 // Don't unset this flag as login_info still needs it.
833 if (!empty($CFG->displayloginfailures)) {
834 // We're already in /user/lib.php, so we don't need to include.
835 if ($count = user_count_login_failures($user)) {
836
837 // Get login failures string.
838 $a = new stdClass();
839 $a->attempts = html_writer::tag('span', $count, array('class' => 'value'));
840 $returnobject->metadata['userloginfail'] =
841 get_string('failedloginattempts', '', $a);
842
843 }
844 }
845 }
846
81d7de1a 847 // Links: Dashboard.
6da0e4cf 848 $myhome = new stdClass();
22893d6f 849 $myhome->itemtype = 'link';
6da0e4cf
JC
850 $myhome->url = new moodle_url('/my/');
851 $myhome->title = get_string('mymoodle', 'admin');
d7dbef73 852 $myhome->titleidentifier = 'mymoodle,admin';
6da0e4cf
JC
853 $myhome->pix = "i/course";
854 $returnobject->navitems[] = $myhome;
855
856 // Links: My Profile.
857 $myprofile = new stdClass();
22893d6f 858 $myprofile->itemtype = 'link';
6da0e4cf 859 $myprofile->url = new moodle_url('/user/profile.php', array('id' => $user->id));
4887d152 860 $myprofile->title = get_string('profile');
d7dbef73 861 $myprofile->titleidentifier = 'profile,moodle';
6da0e4cf
JC
862 $myprofile->pix = "i/user";
863 $returnobject->navitems[] = $myprofile;
864
865 // Links: Role-return or logout link.
866 $lastobj = null;
867 $buildlogout = true;
868 $returnobject->metadata['asotherrole'] = false;
869 if (is_role_switched($course->id)) {
870 if ($role = $DB->get_record('role', array('id' => $user->access['rsw'][$context->path]))) {
871 // Build role-return link instead of logout link.
872 $rolereturn = new stdClass();
22893d6f 873 $rolereturn->itemtype = 'link';
6da0e4cf
JC
874 $rolereturn->url = new moodle_url('/course/switchrole.php', array(
875 'id' => $course->id,
876 'sesskey' => sesskey(),
877 'switchrole' => 0,
878 'returnurl' => $page->url->out_as_local_url(false)
879 ));
880 $rolereturn->pix = "a/logout";
881 $rolereturn->title = get_string('switchrolereturn');
d7dbef73 882 $rolereturn->titleidentifier = 'switchrolereturn,moodle';
6da0e4cf
JC
883 $lastobj = $rolereturn;
884
885 $returnobject->metadata['asotherrole'] = true;
886 $returnobject->metadata['rolename'] = role_get_name($role, $context);
887
888 $buildlogout = false;
889 }
890 }
891
892 if ($returnobject->metadata['asotheruser'] = \core\session\manager::is_loggedinas()) {
893 $realuser = \core\session\manager::get_realuser();
894
895 // Save values for the real user, as $user will be full of data for the
896 // user the user is disguised as.
897 $returnobject->metadata['realuserid'] = $realuser->id;
898 $returnobject->metadata['realuserfullname'] = fullname($realuser, true);
899 $returnobject->metadata['realuserprofileurl'] = new moodle_url('/user/profile.php', array(
900 'id' => $realuser->id
901 ));
c68b3206 902 $returnobject->metadata['realuseravatar'] = $OUTPUT->user_picture($realuser, $avataroptions);
6da0e4cf
JC
903
904 // Build a user-revert link.
905 $userrevert = new stdClass();
22893d6f 906 $userrevert->itemtype = 'link';
6da0e4cf
JC
907 $userrevert->url = new moodle_url('/course/loginas.php', array(
908 'id' => $course->id,
909 'sesskey' => sesskey()
910 ));
911 $userrevert->pix = "a/logout";
912 $userrevert->title = get_string('logout');
d7dbef73 913 $userrevert->titleidentifier = 'logout,moodle';
6da0e4cf
JC
914 $lastobj = $userrevert;
915
916 $buildlogout = false;
917 }
918
919 if ($buildlogout) {
920 // Build a logout link.
921 $logout = new stdClass();
22893d6f 922 $logout->itemtype = 'link';
6da0e4cf
JC
923 $logout->url = new moodle_url('/login/logout.php', array('sesskey' => sesskey()));
924 $logout->pix = "a/logout";
925 $logout->title = get_string('logout');
d7dbef73 926 $logout->titleidentifier = 'logout,moodle';
6da0e4cf
JC
927 $lastobj = $logout;
928 }
929
b865a3e2
JC
930 // Before we add the last item (usually a logout link), add any
931 // custom-defined items.
932 $customitems = user_convert_text_to_menu_items($CFG->customusermenuitems, $page);
933 foreach ($customitems as $item) {
934 $returnobject->navitems[] = $item;
935 }
936
6da0e4cf
JC
937 // Add the last item to the list.
938 if (!is_null($lastobj)) {
939 $returnobject->navitems[] = $lastobj;
940 }
941
942 return $returnobject;
1d658535
PS
943}
944
945/**
946 * Add password to the list of used hashes for this user.
947 *
948 * This is supposed to be used from:
949 * 1/ change own password form
950 * 2/ password reset process
951 * 3/ user signup in auth plugins if password changing supported
952 *
953 * @param int $userid user id
954 * @param string $password plaintext password
955 * @return void
956 */
957function user_add_password_history($userid, $password) {
958 global $CFG, $DB;
1d658535
PS
959
960 if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
961 return;
962 }
963
964 // Note: this is using separate code form normal password hashing because
965 // we need to have this under control in the future. Also the auth
966 // plugin might not store the passwords locally at all.
967
968 $record = new stdClass();
969 $record->userid = $userid;
970 $record->hash = password_hash($password, PASSWORD_DEFAULT);
971 $record->timecreated = time();
972 $DB->insert_record('user_password_history', $record);
973
974 $i = 0;
975 $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
976 foreach ($records as $record) {
977 $i++;
978 if ($i > $CFG->passwordreuselimit) {
979 $DB->delete_records('user_password_history', array('id' => $record->id));
980 }
981 }
982}
983
984/**
985 * Was this password used before on change or reset password page?
986 *
987 * The $CFG->passwordreuselimit setting determines
988 * how many times different password needs to be used
989 * before allowing previously used password again.
990 *
991 * @param int $userid user id
992 * @param string $password plaintext password
993 * @return bool true if password reused
994 */
995function user_is_previously_used_password($userid, $password) {
996 global $CFG, $DB;
1d658535
PS
997
998 if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
999 return false;
1000 }
1001
1002 $reused = false;
1003
1004 $i = 0;
1005 $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
1006 foreach ($records as $record) {
1007 $i++;
1008 if ($i > $CFG->passwordreuselimit) {
1009 $DB->delete_records('user_password_history', array('id' => $record->id));
1010 continue;
1011 }
1012 // NOTE: this is slow but we cannot compare the hashes directly any more.
1013 if (password_verify($password, $record->hash)) {
1014 $reused = true;
1015 }
1016 }
1017
1018 return $reused;
1019}
3221718e
JL
1020
1021/**
1022 * Remove a user device from the Moodle database (for PUSH notifications usually).
1023 *
1024 * @param string $uuid The device UUID.
1025 * @param string $appid The app id. If empty all the devices matching the UUID for the user will be removed.
1026 * @return bool true if removed, false if the device didn't exists in the database
1027 * @since Moodle 2.9
1028 */
1029function user_remove_user_device($uuid, $appid = "") {
1030 global $DB, $USER;
1031
1032 $conditions = array('uuid' => $uuid, 'userid' => $USER->id);
1033 if (!empty($appid)) {
1034 $conditions['appid'] = $appid;
1035 }
1036
1037 if (!$DB->count_records('user_devices', $conditions)) {
1038 return false;
1039 }
1040
1041 $DB->delete_records('user_devices', $conditions);
1042
1043 return true;
1044}
0ff203b6
JL
1045
1046/**
1047 * Trigger user_list_viewed event.
1048 *
1049 * @param stdClass $course course object
1050 * @param stdClass $context course context object
1051 * @since Moodle 2.9
1052 */
1053function user_list_view($course, $context) {
1054
1055 $event = \core\event\user_list_viewed::create(array(
1056 'objectid' => $course->id,
1057 'courseid' => $course->id,
1058 'context' => $context,
1059 'other' => array(
1060 'courseshortname' => $course->shortname,
1061 'coursefullname' => $course->fullname
1062 )
1063 ));
1064 $event->trigger();
1065}
c9451960
AG
1066
1067/**
4887d152 1068 * Returns the url to use for the "Grades" link in the user navigation.
c9451960
AG
1069 *
1070 * @param int $userid The user's ID.
1071 * @param int $courseid The course ID if available.
4887d152 1072 * @return mixed A URL to be directed to for "Grades".
c9451960
AG
1073 */
1074function user_mygrades_url($userid = null, $courseid = SITEID) {
1075 global $CFG, $USER;
1076 $url = null;
1077 if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report != 'external') {
1078 if (isset($userid) && $USER->id != $userid) {
1079 // Send to the gradebook report.
1080 $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php',
1081 array('id' => $courseid, 'userid' => $userid));
1082 } else {
1083 $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php');
1084 }
1085 } else if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report == 'external'
1086 && !empty($CFG->gradereport_mygradeurl)) {
1087 $url = $CFG->gradereport_mygradeurl;
1088 } else {
1089 $url = $CFG->wwwroot;
1090 }
1091 return $url;
1092}
66a43cd5
AG
1093
1094/**
1095 * Check if a user has the permission to viewdetails in a shared course's context.
1096 *
1097 * @param object $user The other user's details.
1098 * @param object $course Use this course to see if we have permission to see this user's profile.
1099 * @param context $usercontext The user context if available.
1100 * @return bool true for ability to view this user, else false.
1101 */
1102function user_can_view_profile($user, $course = null, $usercontext = null) {
1103 global $USER, $CFG;
1104
1105 if ($user->deleted) {
1106 return false;
1107 }
1108
1109 // If any of these four things, return true.
1110 // Number 1.
1111 if ($USER->id == $user->id) {
1112 return true;
1113 }
1114
1115 // Number 2.
1116 if (empty($CFG->forceloginforprofiles)) {
1117 return true;
1118 }
1119
1120 if (empty($usercontext)) {
1121 $usercontext = context_user::instance($user->id);
1122 }
1123 // Number 3.
1124 if (has_capability('moodle/user:viewdetails', $usercontext)) {
1125 return true;
1126 }
1127
1128 // Number 4.
1129 if (has_coursecontact_role($user->id)) {
1130 return true;
1131 }
1132
1133 if (isset($course)) {
1134 $sharedcourses = array($course);
1135 } else {
1136 $sharedcourses = enrol_get_shared_courses($USER->id, $user->id, true);
1137 }
1138 foreach ($sharedcourses as $sharedcourse) {
1139 $coursecontext = context_course::instance($sharedcourse->id);
1140 if (has_capability('moodle/user:viewdetails', $coursecontext)) {
1141 if (!groups_user_groups_visible($sharedcourse, $user->id)) {
1142 // Not a member of the same group.
1143 continue;
1144 }
1145 return true;
1146 }
1147 }
1148 return false;
1149}
c4e868d5
MG
1150
1151/**
1152 * Returns users tagged with a specified tag.
1153 *
1154 * @param core_tag_tag $tag
1155 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
1156 * are displayed on the page and the per-page limit may be bigger
1157 * @param int $fromctx context id where the link was displayed, may be used by callbacks
1158 * to display items in the same context first
1159 * @param int $ctx context id where to search for records
1160 * @param bool $rec search in subcontexts as well
1161 * @param int $page 0-based number of page being displayed
1162 * @return \core_tag\output\tagindex
1163 */
1164function user_get_tagged_users($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
1165 global $PAGE;
1166
1167 if ($ctx && $ctx != context_system::instance()->id) {
1168 $usercount = 0;
1169 } else {
1170 // Users can only be displayed in system context.
1171 $usercount = $tag->count_tagged_items('core', 'user',
1172 'it.deleted=:notdeleted', array('notdeleted' => 0));
1173 }
1174 $perpage = $exclusivemode ? 24 : 5;
1175 $content = '';
1176 $totalpages = ceil($usercount / $perpage);
1177
1178 if ($usercount) {
1179 $userlist = $tag->get_tagged_items('core', 'user', $page * $perpage, $perpage,
1180 'it.deleted=:notdeleted', array('notdeleted' => 0));
1181 $renderer = $PAGE->get_renderer('core', 'user');
1182 $content .= $renderer->user_list($userlist, $exclusivemode);
1183 }
1184
1185 return new core_tag\output\tagindex($tag, 'core', 'user', $content,
1186 $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
1187}