Merge branch 'MDL-60410_master' of git://github.com/dmonllao/moodle
[moodle.git] / login / lib.php
blob9ea72fde6041b5562cc49bd6f4e4ba7115b6c77a
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.
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 /**
19 * Login library file of login/password related Moodle functions.
21 * @package core
22 * @subpackage lib
23 * @copyright Catalyst IT
24 * @copyright Peter Bulmer
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 define('PWRESET_STATUS_NOEMAILSENT', 1);
28 define('PWRESET_STATUS_TOKENSENT', 2);
29 define('PWRESET_STATUS_OTHEREMAILSENT', 3);
30 define('PWRESET_STATUS_ALREADYSENT', 4);
32 /**
33 * Processes a user's request to set a new password in the event they forgot the old one.
34 * If no user identifier has been supplied, it displays a form where they can submit their identifier.
35 * Where they have supplied identifier, the function will check their status, and send email as appropriate.
37 function core_login_process_password_reset_request() {
38 global $OUTPUT, $PAGE;
39 $mform = new login_forgot_password_form();
41 if ($mform->is_cancelled()) {
42 redirect(get_login_url());
44 } else if ($data = $mform->get_data()) {
46 $username = $email = '';
47 if (!empty($data->username)) {
48 $username = $data->username;
49 } else {
50 $email = $data->email;
52 list($status, $notice, $url) = core_login_process_password_reset($username, $email);
54 // Any email has now been sent.
55 // Next display results to requesting user if settings permit.
56 echo $OUTPUT->header();
57 notice($notice, $url);
58 die; // Never reached.
61 // Make sure we really are on the https page when https login required.
62 $PAGE->verify_https_required();
64 // DISPLAY FORM.
66 echo $OUTPUT->header();
67 echo $OUTPUT->box(get_string('passwordforgotteninstructions2'), 'generalbox boxwidthnormal boxaligncenter');
68 $mform->display();
70 echo $OUTPUT->footer();
73 /**
74 * Process the password reset for the given user (via username or email).
76 * @param string $username the user name
77 * @param string $email the user email
78 * @return array an array containing fields indicating the reset status, a info notice and redirect URL.
79 * @since Moodle 3.4
81 function core_login_process_password_reset($username, $email) {
82 global $CFG, $DB;
84 if (empty($username) && empty($email)) {
85 print_error('cannotmailconfirm');
88 // Next find the user account in the database which the requesting user claims to own.
89 if (!empty($username)) {
90 // Username has been specified - load the user record based on that.
91 $username = core_text::strtolower($username); // Mimic the login page process.
92 $userparams = array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 0, 'suspended' => 0);
93 $user = $DB->get_record('user', $userparams);
94 } else {
95 // Try to load the user record based on email address.
96 // this is tricky because
97 // 1/ the email is not guaranteed to be unique - TODO: send email with all usernames to select the account for pw reset
98 // 2/ mailbox may be case sensitive, the email domain is case insensitive - let's pretend it is all case-insensitive.
100 $select = $DB->sql_like('email', ':email', false, true, false, '|') .
101 " AND mnethostid = :mnethostid AND deleted=0 AND suspended=0";
102 $params = array('email' => $DB->sql_like_escape($email, '|'), 'mnethostid' => $CFG->mnet_localhost_id);
103 $user = $DB->get_record_select('user', $select, $params, '*', IGNORE_MULTIPLE);
106 // Target user details have now been identified, or we know that there is no such account.
107 // Send email address to account's email address if appropriate.
108 $pwresetstatus = PWRESET_STATUS_NOEMAILSENT;
109 if ($user and !empty($user->confirmed)) {
110 $systemcontext = context_system::instance();
112 $userauth = get_auth_plugin($user->auth);
113 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)
114 or !has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
115 if (send_password_change_info($user)) {
116 $pwresetstatus = PWRESET_STATUS_OTHEREMAILSENT;
117 } else {
118 print_error('cannotmailconfirm');
120 } else {
121 // The account the requesting user claims to be is entitled to change their password.
122 // Next, check if they have an existing password reset in progress.
123 $resetinprogress = $DB->get_record('user_password_resets', array('userid' => $user->id));
124 if (empty($resetinprogress)) {
125 // Completely new reset request - common case.
126 $resetrecord = core_login_generate_password_reset($user);
127 $sendemail = true;
128 } else if ($resetinprogress->timerequested < (time() - $CFG->pwresettime)) {
129 // Preexisting, but expired request - delete old record & create new one.
130 // Uncommon case - expired requests are cleaned up by cron.
131 $DB->delete_records('user_password_resets', array('id' => $resetinprogress->id));
132 $resetrecord = core_login_generate_password_reset($user);
133 $sendemail = true;
134 } else if (empty($resetinprogress->timererequested)) {
135 // Preexisting, valid request. This is the first time user has re-requested the reset.
136 // Re-sending the same email once can actually help in certain circumstances
137 // eg by reducing the delay caused by greylisting.
138 $resetinprogress->timererequested = time();
139 $DB->update_record('user_password_resets', $resetinprogress);
140 $resetrecord = $resetinprogress;
141 $sendemail = true;
142 } else {
143 // Preexisting, valid request. User has already re-requested email.
144 $pwresetstatus = PWRESET_STATUS_ALREADYSENT;
145 $sendemail = false;
148 if ($sendemail) {
149 $sendresult = send_password_change_confirmation_email($user, $resetrecord);
150 if ($sendresult) {
151 $pwresetstatus = PWRESET_STATUS_TOKENSENT;
152 } else {
153 print_error('cannotmailconfirm');
159 $url = $CFG->wwwroot.'/index.php';
160 if (!empty($CFG->protectusernames)) {
161 // Neither confirm, nor deny existance of any username or email address in database.
162 // Print general (non-commital) message.
163 $status = 'emailpasswordconfirmmaybesent';
164 $notice = get_string($status);
165 } else if (empty($user)) {
166 // Protect usernames is off, and we couldn't find the user with details specified.
167 // Print failure advice.
168 $status = 'emailpasswordconfirmnotsent';
169 $notice = get_string($status);
170 $url = $CFG->wwwroot.'/forgot_password.php';
171 } else if (empty($user->email)) {
172 // User doesn't have an email set - can't send a password change confimation email.
173 $status = 'emailpasswordconfirmnoemail';
174 $notice = get_string($status);
175 } else if ($pwresetstatus == PWRESET_STATUS_ALREADYSENT) {
176 // User found, protectusernames is off, but user has already (re) requested a reset.
177 // Don't send a 3rd reset email.
178 $status = 'emailalreadysent';
179 $notice = get_string($status);
180 } else if ($pwresetstatus == PWRESET_STATUS_NOEMAILSENT) {
181 // User found, protectusernames is off, but user is not confirmed.
182 // Pretend we sent them an email.
183 // This is a big usability problem - need to tell users why we didn't send them an email.
184 // Obfuscate email address to protect privacy.
185 $protectedemail = preg_replace('/([^@]*)@(.*)/', '******@$2', $user->email);
186 $status = 'emailpasswordconfirmsent';
187 $notice = get_string($status, '', $protectedemail);
188 } else {
189 // Confirm email sent. (Obfuscate email address to protect privacy).
190 $protectedemail = preg_replace('/([^@]*)@(.*)/', '******@$2', $user->email);
191 // This is a small usability problem - may be obfuscating the email address which the user has just supplied.
192 $status = 'emailresetconfirmsent';
193 $notice = get_string($status, '', $protectedemail);
195 return array($status, $notice, $url);
199 * This function processes a user's submitted token to validate the request to set a new password.
200 * If the user's token is validated, they are prompted to set a new password.
201 * @param string $token the one-use identifier which should verify the password reset request as being valid.
202 * @return void
204 function core_login_process_password_set($token) {
205 global $DB, $CFG, $OUTPUT, $PAGE, $SESSION;
206 require_once($CFG->dirroot.'/user/lib.php');
208 $pwresettime = isset($CFG->pwresettime) ? $CFG->pwresettime : 1800;
209 $sql = "SELECT u.*, upr.token, upr.timerequested, upr.id as tokenid
210 FROM {user} u
211 JOIN {user_password_resets} upr ON upr.userid = u.id
212 WHERE upr.token = ?";
213 $user = $DB->get_record_sql($sql, array($token));
215 $forgotpasswordurl = "{$CFG->httpswwwroot}/login/forgot_password.php";
216 if (empty($user) or ($user->timerequested < (time() - $pwresettime - DAYSECS))) {
217 // There is no valid reset request record - not even a recently expired one.
218 // (suspicious)
219 // Direct the user to the forgot password page to request a password reset.
220 echo $OUTPUT->header();
221 notice(get_string('noresetrecord'), $forgotpasswordurl);
222 die; // Never reached.
224 if ($user->timerequested < (time() - $pwresettime)) {
225 // There is a reset record, but it's expired.
226 // Direct the user to the forgot password page to request a password reset.
227 $pwresetmins = floor($pwresettime / MINSECS);
228 echo $OUTPUT->header();
229 notice(get_string('resetrecordexpired', '', $pwresetmins), $forgotpasswordurl);
230 die; // Never reached.
233 if ($user->auth === 'nologin' or !is_enabled_auth($user->auth)) {
234 // Bad luck - user is not able to login, do not let them set password.
235 echo $OUTPUT->header();
236 print_error('forgotteninvalidurl');
237 die; // Never reached.
240 // Check this isn't guest user.
241 if (isguestuser($user)) {
242 print_error('cannotresetguestpwd');
245 // Token is correct, and unexpired.
246 $mform = new login_set_password_form(null, $user);
247 $data = $mform->get_data();
248 if (empty($data)) {
249 // User hasn't submitted form, they got here directly from email link.
250 // Next, display the form.
251 $setdata = new stdClass();
252 $setdata->username = $user->username;
253 $setdata->username2 = $user->username;
254 $setdata->token = $user->token;
255 $mform->set_data($setdata);
256 $PAGE->verify_https_required();
257 echo $OUTPUT->header();
258 echo $OUTPUT->box(get_string('setpasswordinstructions'), 'generalbox boxwidthnormal boxaligncenter');
259 $mform->display();
260 echo $OUTPUT->footer();
261 return;
262 } else {
263 // User has submitted form.
264 // Delete this token so it can't be used again.
265 $DB->delete_records('user_password_resets', array('id' => $user->tokenid));
266 $userauth = get_auth_plugin($user->auth);
267 if (!$userauth->user_update_password($user, $data->password)) {
268 print_error('errorpasswordupdate', 'auth');
270 user_add_password_history($user->id, $data->password);
271 if (!empty($CFG->passwordchangelogout)) {
272 \core\session\manager::kill_user_sessions($user->id, session_id());
274 // Reset login lockout (if present) before a new password is set.
275 login_unlock_account($user);
276 // Clear any requirement to change passwords.
277 unset_user_preference('auth_forcepasswordchange', $user);
278 unset_user_preference('create_password', $user);
280 if (!empty($user->lang)) {
281 // Unset previous session language - use user preference instead.
282 unset($SESSION->lang);
284 complete_user_login($user); // Triggers the login event.
286 \core\session\manager::apply_concurrent_login_limit($user->id, session_id());
288 $urltogo = core_login_get_return_url();
289 unset($SESSION->wantsurl);
290 redirect($urltogo, get_string('passwordset'), 1);
294 /** Create a new record in the database to track a new password set request for user.
295 * @param object $user the user record, the requester would like a new password set for.
296 * @return record created.
298 function core_login_generate_password_reset ($user) {
299 global $DB;
300 $resetrecord = new stdClass();
301 $resetrecord->timerequested = time();
302 $resetrecord->userid = $user->id;
303 $resetrecord->token = random_string(32);
304 $resetrecord->id = $DB->insert_record('user_password_resets', $resetrecord);
305 return $resetrecord;
308 /** Determine where a user should be redirected after they have been logged in.
309 * @return string url the user should be redirected to.
311 function core_login_get_return_url() {
312 global $CFG, $SESSION, $USER;
313 // Prepare redirection.
314 if (user_not_fully_set_up($USER, true)) {
315 $urltogo = $CFG->wwwroot.'/user/edit.php';
316 // We don't delete $SESSION->wantsurl yet, so we get there later.
318 } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0
319 or strpos($SESSION->wantsurl, str_replace('http://', 'https://', $CFG->wwwroot)) === 0)) {
320 $urltogo = $SESSION->wantsurl; // Because it's an address in this site.
321 unset($SESSION->wantsurl);
322 } else {
323 // No wantsurl stored or external - go to homepage.
324 $urltogo = $CFG->wwwroot.'/';
325 unset($SESSION->wantsurl);
328 // If the url to go to is the same as the site page, check for default homepage.
329 if ($urltogo == ($CFG->wwwroot . '/')) {
330 $homepage = get_home_page();
331 // Go to my-moodle page instead of site homepage if defaulthomepage set to homepage_my.
332 if ($homepage == HOMEPAGE_MY && !is_siteadmin() && !isguestuser()) {
333 if ($urltogo == $CFG->wwwroot or $urltogo == $CFG->wwwroot.'/' or $urltogo == $CFG->wwwroot.'/index.php') {
334 $urltogo = $CFG->wwwroot.'/my/';
338 return $urltogo;
342 * Validates the forgot password form data.
344 * This is used by the forgot_password_form and by the core_auth_request_password_rest WS.
345 * @param array $data array containing the data to be validated (email and username)
346 * @return array array of errors compatible with mform
347 * @since Moodle 3.4
349 function core_login_validate_forgot_password_data($data) {
350 global $CFG, $DB;
352 $errors = array();
354 if ((!empty($data['username']) and !empty($data['email'])) or (empty($data['username']) and empty($data['email']))) {
355 $errors['username'] = get_string('usernameoremail');
356 $errors['email'] = get_string('usernameoremail');
358 } else if (!empty($data['email'])) {
359 if (!validate_email($data['email'])) {
360 $errors['email'] = get_string('invalidemail');
362 } else if ($DB->count_records('user', array('email' => $data['email'])) > 1) {
363 $errors['email'] = get_string('forgottenduplicate');
365 } else {
366 if ($user = get_complete_user_data('email', $data['email'])) {
367 if (empty($user->confirmed)) {
368 $errors['email'] = get_string('confirmednot');
371 if (!$user and empty($CFG->protectusernames)) {
372 $errors['email'] = get_string('emailnotfound');
376 } else {
377 if ($user = get_complete_user_data('username', $data['username'])) {
378 if (empty($user->confirmed)) {
379 $errors['email'] = get_string('confirmednot');
382 if (!$user and empty($CFG->protectusernames)) {
383 $errors['username'] = get_string('usernamenotfound');
387 return $errors;