From e9f4a64e7bf395468f41a38dc13cba799997c91f Mon Sep 17 00:00:00 2001 From: Linty Date: Mon, 27 May 2024 14:16:13 +0200 Subject: [PATCH] related to #2158 change password rework - add new method for generate a password link - update password.php now we're using functions to generate the link to the password and the contents of the email and we've removed the email from the reset key - update user_list.tpl add modal for pasword and update css - update user_list.js add ajax function for reset password and adjusting scripts for modal password - add functions 'ws_users_generate_reset_password_link', 'generate_reset_password_link' and 'pwg_generate_reset_password_mail' --- admin/themes/default/js/user_list.js | 155 ++++++++++++++++---- admin/themes/default/template/user_list.tpl | 116 +++++++++++++-- include/functions_mail.inc.php | 37 +++++ include/functions_user.inc.php | 35 +++++ include/ws_functions/pwg.users.php | 63 ++++++++ password.php | 75 +--------- ws.php | 20 +++ 7 files changed, 386 insertions(+), 115 deletions(-) diff --git a/admin/themes/default/js/user_list.js b/admin/themes/default/js/user_list.js index fd71097c11..ce8f8932df 100644 --- a/admin/themes/default/js/user_list.js +++ b/admin/themes/default/js/user_list.js @@ -63,6 +63,7 @@ function open_user_list() { function close_user_list() { hide_temporary_messages(); + $('#result_send_mail_copy_input').val(''); $("#UserList").fadeOut(); } @@ -81,6 +82,15 @@ function isSelectionMode() { return $("#toggleSelectionMode").is(":checked") } +function reset_password_modals() { + $('.user-property-password-change').hide(); + $('.user-property-password-change-inputs').hide(); + $('#edit_password_success_change').hide(); + $('#edit_password_result_mail').hide(); + $('#edit_password_result_mail_copy').hide(); + $('.user-property-password-choice').show(); +} + $( document ).ready(function() { $(".user-property-register").tipTip({ @@ -96,9 +106,10 @@ $( document ).ready(function() { fadeOut: 200 }); $(".advanced-filter-level select option").eq(1).remove(); + + /* Edit Password */ $('.button-edit-password-icon').click(function () { - $('.user-property-password-change-inputs, .edit-password-success').hide() - $('.user-property-password-choice').show(); + reset_password_modals(); $('.user-property-password-change').fadeIn(); }) @@ -107,6 +118,24 @@ $( document ).ready(function() { reset_input_password(); }) + $('.icon-show-password').on('click', function() { + const icon = $(this); + const closestInput = icon.siblings(); + if (closestInput.attr('type') === 'password') { + closestInput.attr('type', 'text'); + icon.removeClass('icon-eye').addClass('icon-eye-off'); + } else { + closestInput.attr('type', 'password'); + icon.removeClass('icon-eye-off').addClass('icon-eye'); + } + }); + + // Password input onkeyup clear error + $('#edit_user_password, #edit_user_conf_password').on('keyup', function() { + hide_error_edit_user(); + }); + + /* Edit Username */ $('.edit-username').click(function () { $('.user-property-username-change').show(); }) @@ -382,25 +411,6 @@ $( document ).ready(function() { }); }); - // Show / Hide password input - $('.icon-show-password').on('click', function() { - const icon = $(this); - const closestInput = icon.siblings(); - if (closestInput.attr('type') === 'password') { - closestInput.attr('type', 'text'); - icon.removeClass('icon-eye').addClass('icon-eye-off'); - } else { - closestInput.attr('type', 'password'); - icon.removeClass('icon-eye-off').addClass('icon-eye'); - } - }); - - // Password input onchange clear error - $('#edit_user_password, #edit_user_conf_password').on('keyup', function() { - hide_error_edit_user(); - }); - - /* tabsheet pop in guest */ $('.guest-edit-user-tabsheet').off('click').on('click', function() { const tabName = $(this).attr('id').split('_'); @@ -1026,9 +1036,9 @@ function editTabsBind () { } function check_tabs (title_tab_name_id) { - - if (plugins_load.length > 1) { - const countMoresPlugins = plugins_load.length - 1; + if (plugins_load.length > 2) { + $('.edit-user-tab-title').css({gap : '0px', justifyContent: 'space-between'}); + const countMoresPlugins = plugins_load.length - 2; if (!document.getElementById('mores_plugins_expand')) { $('.edit-user-tab-title').append( '' + @@ -1044,7 +1054,7 @@ function check_tabs (title_tab_name_id) { tabsheetTitle.css({'max-width': '100%'}); tabsheetTitle.appendTo(dropdown); - if (1 == countMoresPlugins) { + if (1 === countMoresPlugins) { tabsheetTitle.css('border-radius', '10px'); } else { dropdown.children().css('border-radius', '0'); @@ -1124,13 +1134,12 @@ function plugin_add_tab_in_user_modal(tab_name, content_id, users_table=null, se // DOM modification const content = $('#' + content_id); - // TODO Add a super div with the real width and height (for better css custom) $('.edit-user-tab-title').append( '

'+ tab_name +'

' ); $('.edit-user-slides').append( - '
' + '
' ); content.appendTo('#tab_' + name); @@ -1143,7 +1152,7 @@ function plugin_add_tab_in_user_modal(tab_name, content_id, users_table=null, se plugins_load.push('name_tab_' + name); check_tabs('name_tab_' + name); - if (plugins_load.length <= 1) { + if (plugins_load.length <= 2) { $('#name_tab_' + name).tipTip({ delay: 0, fadeIn: 200, @@ -1249,6 +1258,9 @@ function generate_user_list() { $(".user-container").click(user_container_click); } +function copyToClipboard(toCopy) { + navigator.clipboard.writeText(toCopy); +} /*--------------------- Fill the pop-in values ---------------------*/ @@ -1315,6 +1327,7 @@ function fill_user_edit_summary(user_to_edit, pop_in, isGuest) { if(!window.isSecureContext) { $('#copy_password').hide(); $('.update-password-success').css('margin', '40px 0'); + $('#result_send_mail_copy').hide(); } } @@ -1369,12 +1382,49 @@ function fill_user_edit_update(user_to_edit, pop_in) { $('.user-property-password-choice').hide(); $('.user-property-password-change-inputs').fadeIn(); }); + if (user_to_edit.email) { + pop_in.find('#send_password_link') + .removeClass('unavailable tiptip') + .attr('title', '') + .off('click') + .on('click', function () { + send_link_password(user_to_edit.email, user_to_edit.username, user_to_edit.id, true); + }); + pop_in.find('#send_password_link').off('mouseenter mouseleave focus blur'); + } else { + pop_in.find('#send_password_link') + .addClass('unavailable tiptip') + .off('click') + .attr('title', cannotSendMail) + .tipTip({ + delay: 0, + fadeIn: 200, + fadeOut: 200, + edgeOffset: 3 + }); + } + pop_in.find('#copy_password_link').off('click').on('click', function() { + const inputValue = $('#result_send_mail_copy_input').val(); + if (inputValue === '') { + send_link_password(user_to_edit.email, user_to_edit.username, user_to_edit.id, false); + } else { + if (window.isSecureContext && navigator.clipboard) { + copyToClipboard(inputValue); + }; + } + $('.user-property-password-choice').hide(); + $('#edit_password_result_mail_copy').fadeIn(); + $('#close_password_mail_send_close').off('click').on('click', function() { + reset_password_modals(); + }); + $('#result_send_mail_copy_input').trigger('focus'); + }); pop_in.find('.edit-password-validate').unbind("click").click(function() { const errDiv = $('#UserList .EditUserErrors'); const inputPassword = $('#edit_user_password').val(); const inputConfirmPassword = $('#edit_user_conf_password').val(); - if (inputPassword == '' || inputConfirmPassword == '') { + if (inputPassword === '' || inputConfirmPassword === '') { errDiv.html(missingField); show_error_edit_user(); } else if (inputPassword !== inputConfirmPassword) { @@ -1663,18 +1713,18 @@ function update_user_password() { data = jQuery.parseJSON(raw_data); if (data.stat == 'ok') { $('.user-property-password-change-inputs').hide(); - $('.edit-password-success').fadeIn(); + $('#edit_password_success_change').fadeIn(); if (window.isSecureContext && navigator.clipboard) { $('#copy_password').on('click', async function() { - navigator.clipboard.writeText(ajax_data['password']); + copyToClipboard(ajax_data['password']) $('#password_msg_success').html(passwordCopied); }); }; $('#close_password_success').on('click', function() { $('.user-property-password-change').hide(); - $('.edit-password-success').hide(); + $('#edit_password_success_change').hide(); $('.user-property-password-change-inputs').show(); reset_input_password(); }); @@ -1989,3 +2039,44 @@ function show_filter_infos(nb_filters) { $(".filter-counter").css('display', 'none').html(0); } } + +function send_link_password(email, username, user_id, send_by_mail) { + $.ajax({ + url: "ws.php?format=json", + dataType: "json", + data: { + method: 'pwg.users.generateResetPasswordLink', + user_id: user_id, + send_by_mail: send_by_mail, + pwg_token: pwg_token + }, + success: function(response) { + if('ok' === response.stat) { + $('#result_send_mail_copy_input').val(response.result.generated_link); + if(send_by_mail) { + if(response.result.send_by_mail) { + $('#result_send_mail').removeClass('update-password-fail icon-red').addClass('update-password-success icon-green'); + $('#icon_password_msg_result_mail').removeClass('icon-cancel').addClass('icon-ok'); + $('#password_msg_result_mail').html(sprintf(mailSentAt, username, email)); + } else { + $('#result_send_mail').removeClass('update-password-success icon-green').addClass('update-password-fail icon-red'); + $('#icon_password_msg_result_mail').removeClass('icon-ok').addClass('icon-cancel'); + $('#password_msg_result_mail').html(errorMailSent); + } + $('.user-property-password-choice').hide(); + $('#edit_password_result_mail').fadeIn(); + $('#close_password_mail_close').off('click').on('click', function() { + reset_password_modals(); + }); + } else { + if (window.isSecureContext && navigator.clipboard) { + copyToClipboard(response.result.generated_link); + }; + } + } + }, + error: function(err) { + console.log(err); + }, + }); +} diff --git a/admin/themes/default/template/user_list.tpl b/admin/themes/default/template/user_list.tpl index 49008e6279..fc28115a7e 100644 --- a/admin/themes/default/template/user_list.tpl +++ b/admin/themes/default/template/user_list.tpl @@ -31,6 +31,9 @@ const missingField = "{'Please complete all fields'|@translate|escape:javascript const passwordUpdated = "{'Password updated'|@translate|escape:javascript}"; const passwordCopied = "{'Password copied'|@translate|escape:javascript}"; const copyPassword = "{'Copy password'|@translate|escape:javascript}"; +const mailSentAt = "{'Mail sent to %s [%s].'|@translate|escape:javascript}"; +const errorMailSent = "{'Error sending email'|@translate|escape:javascript}"; +const cannotSendMail = "{'Cannot send an email to this user because he doesn\'t have an email address'|@translate|escape:javascript}" const registered_str = '{"Registered"|@translate|escape:javascript}'; const last_visit_str = '{"Last visit"|@translate|escape:javascript}'; @@ -671,7 +674,7 @@ $(document).ready(function() {

{'Properties'|@translate}

{'Preferences'|@translate}

-

{'Notifications'|@translate}

+ {*

{'Notifications'|@translate}

*}
@@ -720,7 +723,7 @@ $(document).ready(function() { placeholder="{'Select groups or type them'|translate}" name="group_id[]" multiple style="box-sizing:border-box;">

- {'Some of these groups give access to notifications. To find out more, go to the Notifications tab.'|@translate} + {* {'Some of these groups give access to notifications. To find out more, go to the Notifications tab.'|@translate} *}

@@ -787,9 +790,9 @@ $(document).ready(function() { -
+ {*

Notifications tab WIP

-
+
*} @@ -834,10 +837,12 @@ $(document).ready(function() {
- -

{'or'|@translate}

+
+ +

{'Change password'|@translate}

-

{'Cancel'|@translate}

+
+

{'Cancel'|@translate}

- @@ -1538,7 +1563,8 @@ $(document).ready(function() { .edit-user-tab-title { display: flex; - justify-content: space-between; + /* justify-content: space-between; */ + gap: 30px; font-weight: bold; font-size: 14px; } @@ -1802,6 +1828,9 @@ $(document).ready(function() { font-weight:bold; margin-bottom:45px; height:30px; + display: flex; + gap: 5px; + max-width: 250px; } .user-property-username-change { @@ -1879,17 +1908,15 @@ $(document).ready(function() { } .user-property-password-choice .head-button-2 { - gap: 5px; - justify-content: center; -} - -.user-property-password-choice .or { + display: block; text-align: center; - font-weight: 700; + margin-right: 0; + margin-bottom: 20px; } .user-property-password-choice .edit-password-cancel { text-align: center; + margin: 0; } .edit-password-success, @@ -1902,12 +1929,52 @@ $(document).ready(function() { padding-top: 30px; } -.update-password-success { +.edit-password-success .edit-password-success-input { + background-color: transparent; + font-size: 15px; + border: none; + flex: auto; + padding: 5px; +} + +.edit-password-success .edit-password-success-reset-link { + display: flex; + align-items: center; + width: 100%; + margin: 10px 0; + background-color: #F3F3F3; +} + +.edit-password-success .edit-password-success-reset-link span { + padding-right: 5px; +} + +#result_send_mail_copy { + margin-bottom: 0px; +} + +#edit_password_result_mail_copy { + padding-top: 0px; +} + +.update-password-success, +.update-password-fail { padding: 10px; font-weight: bold; margin-bottom: 20px; } +#result_send_mail { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +} + +#edit_password_result_mail { + padding-top: 20px; +} + .update-username-success { display: flex; padding: 10px; @@ -1987,6 +2054,9 @@ $(document).ready(function() { .edit-username-title { font-size:1.4em; + width: auto; + overflow-x: hidden; + text-overflow: ellipsis; } .edit-username-specifier { @@ -2157,6 +2227,20 @@ $(document).ready(function() { font-weight:normal; } +/* plugins tabsheet */ +.edit-user-tabsheet-plugins { + width: 435px !important; + height: 480px !important; + margin-right: 15px !important; + margin-top: 29px !important; +} +/* for firefox */ +@-moz-document url-prefix() { + .edit-user-tabsheet-plugins { + margin-top: 25px !important; + } +} + /* update */ .update-user-button { diff --git a/include/functions_mail.inc.php b/include/functions_mail.inc.php index fe5859ebb2..edd7bda957 100644 --- a/include/functions_mail.inc.php +++ b/include/functions_mail.inc.php @@ -1004,6 +1004,43 @@ function pwg_send_mail_test($success, $mail, $args) } } +/** + * Generate content mail for reset password + * + * Return the content mail to send + * @since 15 + * @param string $username + * @param string $reset_password_link + * @param string $gallery_title + * @return string mail content + */ +function pwg_generate_reset_password_mail($username, $reset_password_link, $gallery_title) +{ + set_make_full_url(); + + $message = l10n('Someone requested that the password be reset for the following user account:') . "\r\n\r\n"; + $message.= l10n( + 'Username "%s" on gallery %s', + $username, + get_gallery_home_url() + ); + $message.= "\r\n\r\n"; + $message.= l10n('To reset your password, visit the following address:') . "\r\n"; + $message.= $reset_password_link; + $message.= "\r\n\r\n"; + $message.= l10n('If this was a mistake, just ignore this email and nothing will happen.')."\r\n"; + + unset_make_full_url(); + + $message = trigger_change('render_lost_password_mail_content', $message); + + return array( + 'subject' => '['.$gallery_title.'] '.l10n('Password Reset'), + 'content' => $message, + 'email_format' => 'text/plain', + ); +} + trigger_notify('functions_mail_included'); ?> diff --git a/include/functions_user.inc.php b/include/functions_user.inc.php index 5fab17c57b..d4f1bf9eeb 100644 --- a/include/functions_user.inc.php +++ b/include/functions_user.inc.php @@ -1733,6 +1733,41 @@ function deactivate_password_reset_key($user_id) ); } +/** + * Generate reset password link + * + * @since 15 + * @param int $user_id + * @param string $user_email + * @return array activation_key and reset password link + */ +function generate_reset_password_link($user_id) +{ + $activation_key = generate_key(20); + + list($expire) = pwg_db_fetch_row(pwg_query('SELECT ADDDATE(NOW(), INTERVAL 1 HOUR)')); + + single_update( + USER_INFOS_TABLE, + array( + 'activation_key' => pwg_password_hash($activation_key), + 'activation_key_expire' => $expire, + ), + array('user_id' => $user_id) + ); + + set_make_full_url(); + + $reset_password_link = get_root_url().'password.php?key='.$activation_key; + + unset_make_full_url(); + + return array( + 'activation_key' => $activation_key, + 'reset_password_link' => $reset_password_link, + ); +} + /** * Gets the last visit (datetime) of a user, based on history table * diff --git a/include/ws_functions/pwg.users.php b/include/ws_functions/pwg.users.php index f1afdabf03..32fb098c21 100644 --- a/include/ws_functions/pwg.users.php +++ b/include/ws_functions/pwg.users.php @@ -961,4 +961,67 @@ function ws_users_favorites_getList($params, &$service) ); } +/** + * API method + * Returns the reset password link of the current user + * @since 15 + * @param mixed[] $params + * @option int user_id + * @option string pwg_token + * @option boolean send_by_mail + */ +function ws_users_generate_reset_password_link($params, &$service) +{ + global $user, $conf; + include_once(PHPWG_ROOT_PATH.'admin/include/functions.php'); + include_once(PHPWG_ROOT_PATH.'include/functions_mail.inc.php'); + + if (get_pwg_token() != $params['pwg_token']) + { + return new PwgError(403, 'Invalid security token'); + } + + // check if user exist + if (get_username($params['user_id']) === false) + { + return new PwgError(WS_ERR_INVALID_PARAM, 'This user does not exist.'); + } + + $user_lost = getuserdata($params['user_id']); + + // Cannot perform this action for a guest or generic user + if (is_a_guest($user_lost['status']) or is_generic($user_lost['status'])) + { + return new PwgError(403, 'Password reset is not allowed for this user'); + } + + // Only webmaster can perform this action for another webmaster + if ('admin' === $user['status'] && 'webmaster' === $user_lost['status']) + { + return new PwgError(403, 'You cannot perform this action'); + } + + $generate_link = generate_reset_password_link($params['user_id']); + $send_by_mail_response = null; + + if ($params['send_by_mail'] and !empty($user_lost['email'])) + { + $email_params = pwg_generate_reset_password_mail($user_lost['username'], $generate_link['reset_password_link'], $conf['gallery_title']); + // Here we remove the display of errors because they prevent the response from being parsed + if (@pwg_mail($user_lost['email'], $email_params)) + { + $send_by_mail_response = 'Mail sent at : ' . $user_lost['email']; + } + else + { + $send_by_mail_response = false; + } + } + + + return array( + 'generated_link' => $generate_link['reset_password_link'], + 'send_by_mail' => $send_by_mail_response, + ); +} ?> diff --git a/password.php b/password.php index 286f0a8d7f..ee9262adc1 100644 --- a/password.php +++ b/password.php @@ -76,44 +76,11 @@ function process_password_request() return false; } - $activation_key = generate_key(20); - - list($expire) = pwg_db_fetch_row(pwg_query('SELECT ADDDATE(NOW(), INTERVAL 1 HOUR)')); - - single_update( - USER_INFOS_TABLE, - array( - 'activation_key' => pwg_password_hash($activation_key), - 'activation_key_expire' => $expire, - ), - array('user_id' => $user_id) - ); + $generate_link = generate_reset_password_link($user_id); - $userdata['activation_key'] = $activation_key; - - set_make_full_url(); - - $message = l10n('Someone requested that the password be reset for the following user account:') . "\r\n\r\n"; - $message.= l10n( - 'Username "%s" on gallery %s', - $userdata['username'], - get_gallery_home_url() - ); - $message.= "\r\n\r\n"; - $message.= l10n('To reset your password, visit the following address:') . "\r\n"; - $message.= get_root_url().'password.php?key='.$activation_key.'-'.urlencode($userdata['email']); - $message.= "\r\n\r\n"; - $message.= l10n('If this was a mistake, just ignore this email and nothing will happen.')."\r\n"; + $userdata['activation_key'] = $generate_link['activation_key']; - unset_make_full_url(); - - $message = trigger_change('render_lost_password_mail_content', $message); - - $email_params = array( - 'subject' => '['.$conf['gallery_title'].'] '.l10n('Password Reset'), - 'content' => $message, - 'email_format' => 'text/plain', - ); + $email_params = pwg_generate_reset_password_mail($userdata['username'], $generate_link['reset_password_link'], $conf['gallery_title']); if (pwg_mail($userdata['email'], $email_params)) { @@ -137,54 +104,27 @@ function check_password_reset_key($reset_key) { global $page, $conf; - list($key, $email) = explode('-', $reset_key, 2); - + $key = $reset_key; if (!preg_match('/^[a-z0-9]{20}$/i', $key)) { $page['errors'][] = l10n('Invalid key'); return false; } - $user_ids = array(); - - $query = ' -SELECT - '.$conf['user_fields']['id'].' AS id - FROM '.USERS_TABLE.' - WHERE '.$conf['user_fields']['email'].' = \''.pwg_db_real_escape_string($email).'\' -;'; - $user_ids = query2array($query, null, 'id'); - - if (count($user_ids) == 0) - { - $page['errors'][] = l10n('Invalid username or email'); - return false; - } - - $user_id = null; - $query = ' SELECT user_id, status, - activation_key, - activation_key_expire, - NOW() AS dbnow + activation_key FROM '.USER_INFOS_TABLE.' - WHERE user_id IN ('.implode(',', $user_ids).') + WHERE activation_key IS NOT NULL + AND activation_key_expire > NOW() ;'; $result = pwg_query($query); while ($row = pwg_db_fetch_assoc($result)) { if (pwg_password_verify($key, $row['activation_key'])) { - if (strtotime($row['dbnow']) > strtotime($row['activation_key_expire'])) - { - // key has expired - $page['errors'][] = l10n('Invalid key'); - return false; - } - if (is_a_guest($row['status']) or is_generic($row['status'])) { $page['errors'][] = l10n('Password reset is not allowed for this user'); @@ -192,6 +132,7 @@ function check_password_reset_key($reset_key) } $user_id = $row['user_id']; + break; } } diff --git a/ws.php b/ws.php index 83d86f80bd..b8ab47c653 100644 --- a/ws.php +++ b/ws.php @@ -1453,6 +1453,26 @@ function ws_addDefaultMethods( $arr ) '', $ws_functions_root . 'pwg.images.php' ); + + $service->addMethod( + 'pwg.users.generateResetPasswordLink', + 'ws_users_generate_reset_password_link', + array( + 'user_id' => array( + 'type'=>WS_TYPE_ID + ), + 'pwg_token' => array(), + 'send_by_mail' => array( + 'flags' => WS_PARAM_OPTIONAL, + 'type' => WS_TYPE_BOOL, + 'default' => false, + ), + ), + 'Return the reset password link
+ (Only webmaster can perform this action for another webmaster)', + $ws_functions_root . 'pwg.users.php', + array('admin_only'=>true) + ); } ?>