Skip to content

Commit

Permalink
Drupal: New email change user-experience/flow.
Browse files Browse the repository at this point in the history
When changing an email, use receives notification at new and old email address.
Add checks for previous email address.
Also add check for boincuser_delete account to terms of use form.
Matches UX from upstream: BOINC#2451

Resolves: https://dev.gridrepublic.org/browse/DBOINCP-436

Also see:
* BOINC#2500
* https://boinc.berkeley.edu/trac/wiki/EmailChangeNotification
  • Loading branch information
Shawn Kwang committed Jun 21, 2018
1 parent c78a174 commit 2c6f081
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 11 deletions.
58 changes: 53 additions & 5 deletions drupal/sites/default/boinc/modules/boincuser/boincuser.module
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ function boincuser_menu() {
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
$items['user/%user/recoveremail/%'] = array(
'title' => t('Revert email change'),
'description' => t('Form to revert email to previous address.'),
'page callback' => 'drupal_get_form',
'page arguments' => array('boincuser_revertemail', 3),
'access callback' => 'user_is_logged_in',
'type' => MENU_CALLBACK,
);
return $items;
}

Expand Down Expand Up @@ -297,7 +305,9 @@ function boincuser_user($op, &$edit, &$account, $category = NULL) {
expavg_time,
cross_project_id,
teamid,
venue
venue,
previous_email_addr,
email_addr_change_time
FROM {user}
WHERE id = %d",
$account->boincuser_id
Expand All @@ -311,6 +321,8 @@ function boincuser_user($op, &$edit, &$account, $category = NULL) {
$account->boincuser_cpid = md5($boinc_user->cross_project_id . $account->mail);
$account->boincuser_default_pref_set = $boinc_user->venue;
$account->boincteam_id = $boinc_user->teamid;
$account->boincuser_previous_email_addr = $boinc_user->previous_email_addr;
$account->boincuser_email_addr_change_time = $boinc_user->email_addr_change_time;
db_set_active('default');
// Set Drupal team ID
$account->team = NULL;
Expand Down Expand Up @@ -447,18 +459,33 @@ function boincuser_user($op, &$edit, &$account, $category = NULL) {
switch ($edit['update_source']) {
case 'user_account':
// Ensure that BOINC data is altered
$boinc_user = BoincUser::lookup_id($account->boincuser_id);

$changing_email = ($edit['mail'] AND $edit['mail'] != $boinc_user->email_addr) ? true : false;
$changing_pass = ($edit['pass']) ? true : false;
if ($changing_email OR $changing_pass) {
// Set password hash appropriately
$passwd = ($edit['pass']) ? $edit['pass'] : $edit['current_pass'];
$passwd_hash = password_hash( md5($passwd.$edit['mail']), PASSWORD_DEFAULT );
$email_addr = $edit['mail'];

if ($changing_email) {
// locally store current email to set as previous email
$prev_email = $account->mail;
$mytime = (user_access('administer users')) ? $boinc_user->email_addr_change_time : time();
$querypart = "email_addr='{$email_addr}', passwd_hash='{$passwd_hash}', previous_email_addr = '{$prev_email}', email_addr_change_time = $mytime";
}
else {
$querypart = "email_addr='{$email_addr}', passwd_hash='{$passwd_hash}'";
}

// Update user account information
$result = $boinc_user->update(
"email_addr='{$email_addr}', passwd_hash='{$passwd_hash}'"
);
$result = $boinc_user->update($querypart);

if ($changing_email) {
// reload account
$account = user_load($account->uid);
_boincuser_send_emailchange($account, $email_addr, $prev_email);
}
}
break;
case 'user_profile':
Expand Down Expand Up @@ -621,6 +648,8 @@ function boincuser_views_api() {
* Implementation of hook_form_alter()
*/
function boincuser_form_alter(&$form, $form_state, $form_id) {
require_boinc('token');

global $user;
switch ($form_id) {
case 'flag_confirm':
Expand Down Expand Up @@ -855,6 +884,9 @@ function boincuser_form_alter(&$form, $form_state, $form_id) {
$account = user_load($form['#uid']);
drupal_set_title($account->boincuser_name);

// Set special message if user has not agreed to TOU
}

// A bit hackish... but don't require the user to enter his password if
// coming from the password reset function
$reset_pass = (strpos($_SERVER['HTTP_REFERER'], "/user/reset/{$form['#uid']}") === FALSE) ? 0 : 1;
Expand All @@ -870,6 +902,20 @@ function boincuser_form_alter(&$form, $form_state, $form_id) {
$form['account']['mail']['#size'] = 40;
$form['account']['pass']['#size'] = 17;

// If email address was changed less than 7 days (7 * 86400 s)
// ago, it cannot be changed again.
$duration = TOKEN_DURATION_ONE_WEEK;
if (($account->boincuser_email_addr_change_time + $duration) > time()) {
$form['account']['mail']['#required'] = FALSE;
$form['account']['mailhelp'] = array(
'#value' => bts("You email address was changed within the past seven (7) days. Please look for an email to !prev_email if you need to revert this change. You may change your email address on !time.",
array(
'!prev_email' => $account->boincuser_previous_email_addr,
'!time' => date('F j, Y \a\t G:i T', $account->boincuser_email_addr_change_time + $duration),
), NULL, 'boinc:account-credentials-change'),
);
}

if (!$reset_pass AND ($user->uid == $account->uid OR !user_access('administer users'))) {
// Add a password authenticator, required to change email or pw
$form['account']['current_pass'] = array(
Expand Down Expand Up @@ -949,6 +995,7 @@ function boincuser_form_alter(&$form, $form_state, $form_id) {
);

// Rearrange form elements
$form['account']['mailhelp']['#weight'] = 4;
$form['account']['mail']['#weight'] = 5;
$form['account']['pass']['#weight'] = 15;
$form['account']['boincuser_id']['#weight'] = 45;
Expand Down Expand Up @@ -1555,6 +1602,7 @@ function boincuser_create_account() {
}

// Process input
// @todo - lookup_prev_email_addr - check for unique email
$boinc_user = BoincUser::lookup_email_addr($params['email_addr']);
if ($boinc_user) {
// Return authenticator for existing users
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ function boincuser_register_validate($form, &$form_state) {
}
}

// @todo - check previous email addresses first

// Check for an existing BOINC user
// This is somewhat redundent as Drupal will also check if the email
// is a duplicate. However, in the case where there is no Drupal
Expand Down Expand Up @@ -374,10 +376,26 @@ function boincuser_account_validate($edit, $account) {
$changing_pass = ($edit['pass']) ? true : false;
if ($changing_email) {
// E-mail address is set to change; check for an existing BOINC user
$boinc_user_already_exists = BoincUser::lookup_email_addr($edit['mail']);
// Check previous email addresses as well, this user's current
// email cannot be the same as another user's previous email
// address.
$boinc_user_already_exists = ( BoincUser::lookup_email_addr($edit['mail']) || BoincUser::lookup_prev_email_addr($edit['mail']) );
if ($boinc_user_already_exists) {
form_set_error('mail', bts('A BOINC account already exists for @email.', array('@email' => $edit['mail']), NULL, 'boinc:add-new-user'));
}

// Check email has not been changed in last X days (default X=7).
$duration = 86400 * 7;
if ( (($boinc_user->email_addr_change_time + $duration) > time()) and (!(user_access('administer users'))) ) {
form_set_error('email_addr_change_time',
bts('Your email address was changed within the past seven (7) days. You must wait until !futuredate to change your email again. If you need to reverse this change, please look for an email sent to !prev_email_addr.',
array(
'!futuredate' => date('F j, Y \a\t H:i T', $boinc_user->email_addr_change_time + $duration),
'!prev_email_addr' => $boinc_user->previous_email_addr,
),
NULL, 'boinc:account-credentials-change')
);
}
}

// If user is changing email or password, require that the current
Expand All @@ -396,6 +414,7 @@ function boincuser_account_validate($edit, $account) {
elseif ( (!password_verify($given_hash, $boinc_user->passwd_hash)) and ($given_hash != $boinc_user->passwd_hash) ) {
form_set_error('current_pass', bts('Password entered is not valid. Please verify that it is correct.', array(), NULL, 'boinc:account-credentials-change'));
}

}
}

Expand Down Expand Up @@ -701,10 +720,12 @@ function boincuser_termsofuse_form() {
$form['form control tabs'] = array(
'#value' => '<li class="tab">' . l(bts('NO - LOGOUT', array(), NULL, 'boinc:form-cancel'), '/logout') . '</li>',
);
$deletelink = '/user/' . $user->uid . '/delete';
$form['deleteaccount'] = array(
'#value' => '<li class="tab">' . l(bts('NO - DELETE ACCOUNT', array(), NULL, 'boinc:form-delete-user'), $deletelink) . '</li>',
);
if (module_exists('boincuser_delete')) {
$deletelink = '/user/' . $user->uid . '/delete';
$form['deleteaccount'] = array(
'#value' => '<li class="tab">' . l(bts('NO - DELETE ACCOUNT', array(), NULL, 'boinc:form-delete-user'), $deletelink) . '</li>',
);
}

// Set form redirect
$form['#redirect'] = $_REQUEST['destination'];
Expand Down Expand Up @@ -732,4 +753,63 @@ function boincuser_termsofuse_form_submit($form, &$form_state) {
if ($_SESSION['messages']['warning']) {
unset($_SESSION['messages']['warning']);
}
}

/**
* Revert email to previous email address. The user must supply a
* token given via e-mail. The token is active for 7 days, and is
* removed after used.
*/
function boincuser_revertemail(&$form_state, $token) {
require_boinc('token');
require_boinc('util');

global $user;
$form = array();

// check BOINC user exists
$account = user_load(array('uid' => $user->uid));
$uid = $user->uid;
$boincid = $account->boincuser_id;
// check $token is valid
if (!is_valid_token($boincid, $token, 'E')) {
drupal_set_message(bts('ERROR: You have supplied an incorrect (most likely expired) token. Please obtain a new token by !link your account be deleted.',
array(
'!link' => l(bts('re-requesting', array(), NULL, 'boinc:delete-user-account'), "/user/${uid}/delete"),
),
NULL, 'boinc:delete-user-account'), 'error');
drupal_goto();
}

// Attach account to this form.
$form['_account'] = array('#type' => 'value', '#value' => $account);

// Instructions
$form['main']['instructions1'] = array(
'#value' => '<p>'.
bts('In order to change your email back to the previous address (!prev_email), you must also change your password.',
array(
'!prev_email' => $account->boincuser_previous_email_addr,
),
NULL, 'boinc:revert-email-change').
'</p>',
);

// @todo - add two password fields and submit/cancel buttons.
}

/**
* Validation handler for revertemail form
*/
function boincuser_revertemail_validate($form, &$form_state) {
// @todo - add validation.
// Check email for account collision using BoincUser::lookup_email_addr( previous_email ).
}

/**
* Submit handler for revertemail form
*/
function boincuser_revertemail_submit($form, &$form_state) {
// @todo - add submit
// Save previous email and new password in database: BOINC and drupal (email only).
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,81 @@ function boincuser_select_user_of_the_day() {
return $uotd;
}


/**
* Send the user an email about his/her email change.
*
* An email is sent to the user's previous email address informing
* them of the email change, and providing a way to revert the
* change. An email is also sent to the new (current) address,
* informing the user of the change.
*
* The user is given a secure URL with which to revert the
* change. Because the email address is not yet changed by Drupal when
* this function is called, it may be called with optional parameters
* new and prev email.
*/
function _boincuser_send_emailchange($account, $new_email=NULL, $prev_email=NULL) {
require_boinc('token');
module_load_include('inc', 'rules', 'modules/system.rules');

global $base_url;
$site_name = variable_get('site_name', 'Drupal-BOINC');

if (is_null($new_email)) {
$new_email = $account->mail;
}
if (is_null($prev_email)) {
$prev_email = $account->boincuser_previous_email_addr;
}

// @todo - set constant in drupal, or use BOINC contsants
$duration = TOKEN_DURATION_ONE_WEEK;
$changedate = date('F j, Y \a\t G:i T', $account->boincuser_email_addr_change_time);
$newdate = date('F j, Y \a\t G:i T', $account->boincuser_email_addr_change_time + $duration);

$token = create_token($account->boincuser_id, TOKEN_TYPE_CHANGE_EMAIL, $duration);

// Send email #1 to current address
$mysubject = "Notification of email change at {$site_name}";
$mymessage = ''
. "{$account->boincuser_name},\n"
. "\n"
. "Your email address was changed from {$prev_email} to {$new_email} "
. "on {$changedate}. You will not be able to change your email address "
. "until {$newdate}. If you need to reverse this change, please look for "
. "an email send to the email address: {$prev_email}.\n"
. "\n"
. "Thanks, \n"
. "{$site_name} support team\n";

$settings = array(
'from' => '',
'to' => $new_email,
'subject' => $mysubject,
'message' => $mymessage,
);
rules_action_mail_to_user($account, $settings);

// Send email #2 to previous address.
$mymessage = ''
. "Your email address has been changed. If you did not intend to take this action, then please click this link to reverse this change, or copy-and-paste the link into your browser location bar. You will need to change your password as well.\n"
. "\n"
. "{$base_url}/user/{$account->uid}/recoveremail/{$token}\n"
. "\n"
. "Thanks, \n"
. "{$site_name} support team\n";

$settings = array(
'from' => '',
'to' => $prev_email,
'subject' => $mysubject,
'message' => $mymessage,
);

rules_action_mail($settings);
}

/**
* Determine the first unique name from a given base. Also cleans the
* username so that it will be valid for drupal. And truncates user
Expand Down Expand Up @@ -291,7 +366,6 @@ function _boincuser_ignore_paths($path, $paths_to_ignore) {
return FALSE;
}


/* * * * * * * * * * * * * * * * * * * * * * * * * * * *
* General BOINC functions
* * * * * * * * * * * * * * * * * * * * * * * * * * * */
Expand Down

0 comments on commit 2c6f081

Please sign in to comment.