Simpler Password Reset in Drupal 7 - Revisited and Improved
This is a follow-up to my earlier post in which I described a simple trick changing Drupal's password reset behavior. That trick emails the user a link that logs them directly into the website, instead of the default one-time login page. That one-time login page forces user to click a Log in button before they are able to change their password.
In the thread following that original post, Heine referred me to the Drupal issue in which the one-time login form was created. Now I understand better the reasons behind the one-time login form. To be perfectly frank, the solution from that thread was lazy. It solved the problem, but not elegantly, and Drupal deserves better. The user asked to reset their password so that's the form they should see, as soon as possible without the unnecessary step.
Here's what the user sees first, in core Drupal:
And here's what (I think) they should see:
This post describes how to accomplish this with a custom module. [UPDATE: Save yourself the trouble of writing a custom module by downloading Simple Password Reset module from drupal.org.]
Our strategy is to replace the unwanted behavior, which comes from a menu item defined in user.module. We use hook_menu_alter() to replace that behavior.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">custom_menu_alter</span><span style="color: #007700">(&</span><span style="color: #0000BB">$items</span><span style="color: #007700">) {<br> </span><span style="color: #FF8000">// Drupal's default behavior is to show the user a log-in form, then their user profile. We change this item to skip the unnecessary step.<br> </span><span style="color: #0000BB">$items</span><span style="color: #007700">[</span><span style="color: #DD0000">'user/reset/%/%/%'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'title' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Reset password'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'access callback' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'custom_pass_reset_access'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'access arguments' </span><span style="color: #007700">=> array(</span><span style="color: #0000BB">2</span><span style="color: #007700">, </span><span style="color: #0000BB">3</span><span style="color: #007700">, </span><span style="color: #0000BB">4</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'page callback' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'custom_pass_reset_page'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'page arguments' </span><span style="color: #007700">=> array(</span><span style="color: #0000BB">2</span><span style="color: #007700">, </span><span style="color: #0000BB">3</span><span style="color: #007700">, </span><span style="color: #0000BB">4</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #0000BB">MENU_CALLBACK</span><span style="color: #007700">,<br> );<br>}<br></span><span style="color: #0000BB">?></span></span>
An access callback ensures that only users who received the password reset email can reset their password. The URL parameters have to be just right. This is the same logic used in the core Drupal function that renders the one-time login form.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">custom_pass_reset_access</span><span style="color: #007700">(</span><span style="color: #0000BB">$uid</span><span style="color: #007700">, </span><span style="color: #0000BB">$timestamp</span><span style="color: #007700">, </span><span style="color: #0000BB">$hashed_pass</span><span style="color: #007700">) {<br> if (!</span><span style="color: #0000BB">drupal_anonymous_user</span><span style="color: #007700">()) {<br> return </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">;<br> }<p> </p></span><span style="color: #FF8000">// Following logic copied from user_pass_reset().<br> // Time out, in seconds, until login URL expires. Defaults to 24 hours =<br> // 86400 seconds.<br> </span><span style="color: #0000BB">$timeout </span><span style="color: #007700">= </span><span style="color: #0000BB">variable_get</span><span style="color: #007700">(</span><span style="color: #DD0000">'user_password_reset_timeout'</span><span style="color: #007700">, </span><span style="color: #0000BB">86400</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$current </span><span style="color: #007700">= </span><span style="color: #0000BB">REQUEST_TIME</span><span style="color: #007700">;<br> </span><span style="color: #FF8000">// Some redundant checks for extra security ?<br> </span><span style="color: #0000BB">$users </span><span style="color: #007700">= </span><span style="color: #0000BB">user_load_multiple</span><span style="color: #007700">(array(</span><span style="color: #0000BB">$uid</span><span style="color: #007700">), array(</span><span style="color: #DD0000">'status' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'1'</span><span style="color: #007700">));<br> if (</span><span style="color: #0000BB">$timestamp </span><span style="color: #007700"><= </span><span style="color: #0000BB">$current </span><span style="color: #007700">&& </span><span style="color: #0000BB">$account </span><span style="color: #007700">= </span><span style="color: #0000BB">reset</span><span style="color: #007700">(</span><span style="color: #0000BB">$users</span><span style="color: #007700">)) {<br> </span><span style="color: #FF8000">// No time out for first time login.<br> </span><span style="color: #007700">if (</span><span style="color: #0000BB">$account</span><span style="color: #007700">-></span><span style="color: #0000BB">login </span><span style="color: #007700">&& </span><span style="color: #0000BB">$current </span><span style="color: #007700">- </span><span style="color: #0000BB">$timestamp </span><span style="color: #007700">> </span><span style="color: #0000BB">$timeout</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">drupal_set_message</span><span style="color: #007700">(</span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'You have tried to use a one-time login link that has expired. Please request a new one using the form below.'</span><span style="color: #007700">));<br> </span><span style="color: #0000BB">drupal_goto</span><span style="color: #007700">(</span><span style="color: #DD0000">'user/password'</span><span style="color: #007700">);<br> }<br> elseif (</span><span style="color: #0000BB">$account</span><span style="color: #007700">-></span><span style="color: #0000BB">uid </span><span style="color: #007700">&& </span><span style="color: #0000BB">$timestamp </span><span style="color: #007700">>= </span><span style="color: #0000BB">$account</span><span style="color: #007700">-></span><span style="color: #0000BB">login </span><span style="color: #007700">&& </span><span style="color: #0000BB">$timestamp </span><span style="color: #007700"><= </span><span style="color: #0000BB">$current </span><span style="color: #007700">&& </span><span style="color: #0000BB">$hashed_pass </span><span style="color: #007700">== </span><span style="color: #0000BB">user_pass_rehash</span><span style="color: #007700">(</span><span style="color: #0000BB">$account</span><span style="color: #007700">-></span><span style="color: #0000BB">pass</span><span style="color: #007700">, </span><span style="color: #0000BB">$timestamp</span><span style="color: #007700">, </span><span style="color: #0000BB">$account</span><span style="color: #007700">-></span><span style="color: #0000BB">login</span><span style="color: #007700">)) {<br> return </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">;<br> }<br> }<br> return </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">;<br>}<br></span><span style="color: #0000BB">?></span></span>
Now that our access callback ensures only the right users can access our reset page, we can show the profile edit form. Remember, this is where Drupal shows the unwanted one-time login. Instead we let the user make the password change right away.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">custom_pass_reset_page</span><span style="color: #007700">(</span><span style="color: #0000BB">$uid</span><span style="color: #007700">, </span><span style="color: #0000BB">$timestamp</span><span style="color: #007700">, </span><span style="color: #0000BB">$hashed_pass</span><span style="color: #007700">, </span><span style="color: #0000BB">$option </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">module_load_include</span><span style="color: #007700">(</span><span style="color: #DD0000">'inc'</span><span style="color: #007700">, </span><span style="color: #DD0000">'user'</span><span style="color: #007700">, </span><span style="color: #DD0000">'user.pages'</span><span style="color: #007700">);<p> </p></span><span style="color: #FF8000">// Show the user edit form instead of silly one-time login form.<br> </span><span style="color: #0000BB">$account </span><span style="color: #007700">= </span><span style="color: #0000BB">user_load</span><span style="color: #007700">(</span><span style="color: #0000BB">$uid</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$form </span><span style="color: #007700">= </span><span style="color: #0000BB">drupal_get_form</span><span style="color: #007700">(</span><span style="color: #DD0000">'user_profile_form'</span><span style="color: #007700">, </span><span style="color: #0000BB">$account</span><span style="color: #007700">);<br> return </span><span style="color: #0000BB">$form</span><span style="color: #007700">;<br>}<br></span><span style="color: #0000BB">?></span></span>
That seems quite simple, but we're not done. At this point the user would be able to change their password, but then they'd have to log in manually. We want to save them the trouble, and log them in right away. The profile edit form becomes the one-time login form. To do this, we need a hook_form_alter().
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">custom_form_user_profile_form_alter</span><span style="color: #007700">(&</span><span style="color: #0000BB">$form</span><span style="color: #007700">, &</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">arg</span><span style="color: #007700">(</span><span style="color: #0000BB">0</span><span style="color: #007700">) == </span><span style="color: #DD0000">'user' </span><span style="color: #007700">&& </span><span style="color: #0000BB">arg</span><span style="color: #007700">(</span><span style="color: #0000BB">1</span><span style="color: #007700">) == </span><span style="color: #DD0000">'reset' </span><span style="color: #007700">&& </span><span style="color: #0000BB">drupal_anonymous_user</span><span style="color: #007700">()) {<br> </span><span style="color: #FF8000">// This is not the normal profile edit form, but actually the password reset form.<p> // Our submit handler will log the use in after form submit.<br> </p></span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'#submit'</span><span style="color: #007700">][] = </span><span style="color: #DD0000">'custom_pass_reset_submit'</span><span style="color: #007700">;<p> </p></span><span style="color: #FF8000">// Require a new password.<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'account'</span><span style="color: #007700">][</span><span style="color: #DD0000">'pass'</span><span style="color: #007700">][</span><span style="color: #DD0000">'#required'</span><span style="color: #007700">] = </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">;<p> if (</p></span><span style="color: #0000BB">arg</span><span style="color: #007700">(</span><span style="color: #0000BB">5</span><span style="color: #007700">) == </span><span style="color: #DD0000">'brief'</span><span style="color: #007700">) {<br> </span><span style="color: #FF8000">// The user is most interested in getting a working password, don't show their picture, timezone, etc.<br> </span><span style="color: #007700">foreach (</span><span style="color: #0000BB">element_children</span><span style="color: #007700">(</span><span style="color: #0000BB">$form</span><span style="color: #007700">) as </span><span style="color: #0000BB">$key</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">in_array</span><span style="color: #007700">(</span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #0000BB">$key</span><span style="color: #007700">][</span><span style="color: #DD0000">'#type'</span><span style="color: #007700">], array(</span><span style="color: #DD0000">'hidden'</span><span style="color: #007700">, </span><span style="color: #DD0000">'actions'</span><span style="color: #007700">))) {<br> </span><span style="color: #FF8000">// Do not alter these elements.<br> </span><span style="color: #007700">}<br> else {<br> </span><span style="color: #FF8000">// Hide other elements.<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #0000BB">$key</span><span style="color: #007700">][</span><span style="color: #DD0000">'#access'</span><span style="color: #007700">] = </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">;<br> }<br> }<br> </span><span style="color: #FF8000">// Except don't hide these.<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'account'</span><span style="color: #007700">][</span><span style="color: #DD0000">'#access'</span><span style="color: #007700">] = </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'actions'</span><span style="color: #007700">][</span><span style="color: #DD0000">'#access'</span><span style="color: #007700">] = </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'actions'</span><span style="color: #007700">][</span><span style="color: #DD0000">'submit'</span><span style="color: #007700">][</span><span style="color: #DD0000">'#value'</span><span style="color: #007700">] = </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Reset password'</span><span style="color: #007700">);<p> </p></span><span style="color: #FF8000">// But seriously do hide these.<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'account'</span><span style="color: #007700">][</span><span style="color: #DD0000">'mail'</span><span style="color: #007700">][</span><span style="color: #DD0000">'#access'</span><span style="color: #007700">] = </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">;<br> }<br> }<br>}<p>function </p></span><span style="color: #0000BB">custom_pass_reset_submit</span><span style="color: #007700">(</span><span style="color: #0000BB">$form</span><span style="color: #007700">, &</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">drupal_anonymous_user</span><span style="color: #007700">()) { </span><span style="color: #FF8000">// Sanity check.<br> // This logic copied from user_pass_reset().<br> </span><span style="color: #0000BB">$account </span><span style="color: #007700">= </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'user'</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">$GLOBALS</span><span style="color: #007700">[</span><span style="color: #DD0000">'user'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'user'</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">user_login_finalize</span><span style="color: #007700">();<p> </p></span><span style="color: #0000BB">watchdog</span><span style="color: #007700">(</span><span style="color: #DD0000">'user'</span><span style="color: #007700">, </span><span style="color: #DD0000">'User %name used one-time login link.'</span><span style="color: #007700">, array(</span><span style="color: #DD0000">'%name' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$account</span><span style="color: #007700">-></span><span style="color: #0000BB">name</span><span style="color: #007700">));<br> </span><span style="color: #0000BB">drupal_set_message</span><span style="color: #007700">(</span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Your password has been reset. Welcome back, %name.'</span><span style="color: #007700">, array(</span><span style="color: #DD0000">'%name' </span><span style="color: #007700">=> </span><span style="color: #0000BB">format_username</span><span style="color: #007700">(</span><span style="color: #0000BB">$account</span><span style="color: #007700">))));<p> </p></span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'redirect'</span><span style="color: #007700">] = </span><span style="color: #DD0000">'user'</span><span style="color: #007700">;<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
Two things to note in the above code. First, we add a submit handler to the profile edit form (but only when used to reset a password). Our submit handler logs the user in so they don't have to do it manually.
Second thing to notice, we honor an optional parameter, "brief", which abbreviates the form. This is because Drupal uses the reset page not just when a password is reset but also when new user accounts are created or email addresses are confirmed. In those cases it may be reasonable to show the entire profile edit form. When just resetting password, it's nice to show them only that portion of the form. To use this feature, under Admin >> Configuration >> People >> Settings >> Password recovery, replace the token [user:one-time-login-url] with this:
[user:one-time-login-url]/brief
This technique allows an administrator to choose between the longer or shorter version of the form fairly easily.
So there you have it! This approach should have none of the drawbacks of my earlier trick, and all the benefits.
Tags: