diff --git a/includes/controllers/ApiController.php b/includes/controllers/ApiController.php index 9b39de333..d13aa97de 100644 --- a/includes/controllers/ApiController.php +++ b/includes/controllers/ApiController.php @@ -179,8 +179,13 @@ public function createUser() $user = $userController->create([ 'name' => strval($_POST['name']), 'email' => strval($_POST['email']), - 'password' => $this->wiki->generateRandomString(30), + 'password' => $this->wiki->generateRandomString(30) ]); + if (!boolval($this->wiki->config['contact_disable_email_for_password']) && !empty($user)) { + $link = $userController->sendPasswordRecoveryEmail($user); + } else { + $link = ''; + } $code = Response::HTTP_OK; $result = [ 'created' => [$user['name']], @@ -188,6 +193,7 @@ public function createUser() 'name' => $user['name'], 'email' => $user['email'], 'signuptime' => $user['signuptime'], + 'link' => $link ], ]; } catch (UserNameAlreadyUsedException $th) { diff --git a/includes/controllers/UserController.php b/includes/controllers/UserController.php index 4ba023444..8af9d5f50 100644 --- a/includes/controllers/UserController.php +++ b/includes/controllers/UserController.php @@ -103,6 +103,15 @@ public function create(array $newValues): ?User return null; } + + public function sendPasswordRecoveryEmail(User $user): string + { + if ($this->userManager->sendPasswordRecoveryEmail($user, _t('LOGIN_PASSWORD_FOR'))) { + return $this->userManager->getUserLink(); + } else { + return ""; + } + } /** * update user params diff --git a/includes/services/UserManager.php b/includes/services/UserManager.php index 20655ae90..047a70154 100644 --- a/includes/services/UserManager.php +++ b/includes/services/UserManager.php @@ -19,6 +19,10 @@ use YesWiki\Security\Controller\SecurityController; use YesWiki\Wiki; +if (! function_exists('send_mail')) { + require_once 'includes/email.inc.php'; +} + class UserManager implements UserProviderInterface, PasswordUpgraderInterface { protected $wiki; @@ -26,9 +30,12 @@ class UserManager implements UserProviderInterface, PasswordUpgraderInterface protected $passwordHasherFactory; protected $securityController; protected $params; - + protected $userlink; private $getOneByNameCacheResults; + private const PW_SALT = 'FBcA'; + public const KEY_VOCABULARY = 'http://outils-reseaux.org/_vocabulary/key'; + public function __construct( Wiki $wiki, DbService $dbService, @@ -42,6 +49,7 @@ public function __construct( $this->securityController = $securityController; $this->params = $params; $this->getOneByNameCacheResults = []; + $this->userlink = ""; } private function arrayToUser(?array $userAsArray = null, bool $fillEmpty = false): ?User @@ -107,6 +115,7 @@ function ($userAsArray) { */ public function create($wikiNameOrUser, string $email = '', string $plainPassword = '') { + $this->userlink = ''; if ($this->securityController->isWikiHibernated()) { throw new Exception(_t('WIKI_IN_HIBERNATION')); } @@ -177,6 +186,92 @@ public function create($wikiNameOrUser, string $email = '', string $plainPasswor ); } + /* + * Password recovery process (AKA reset password) + * 1. A key is generated using name, email alongside with other stuff. + * 2. The triple (user's name, specific key "vocabulary",key) is stored in triples table. + * 3. In order to update h·er·is password, the user must provided that key. + * 4. The new password is accepted only if the key matches with the value in triples table. + * 5. The corresponding row is removed from triples table. + */ + + protected function generateUserLink($user) + { + // Generate the password recovery key + $key = md5($user['name'] . '_' . $user['email'] . random_int(0, 10000) . date('Y-m-d H:i:s') . self::PW_SALT); + $tripleStore = $this->wiki->services->get(TripleStore::class); + // Erase the previous triples in the trible table + $tripleStore->delete($user['name'], self::KEY_VOCABULARY, null, '', ''); + // Store the (name, vocabulary, key) triple in triples table + $tripleStore->create($user['name'], self::KEY_VOCABULARY, $key, '', ''); + + // Generate the recovery email + $this->userlink = $this->wiki->Href('', 'MotDePassePerdu', [ + 'a' => 'recover', + 'email' => $key, + 'u' => base64_encode($user['name']) + ], false); + } + + /** + * Part of the Password recovery process: Handles the password recovery email process. + * + * Generates the password recovery key + * Stores the (name, vocabulary, key) triple in triples table + * Generates the recovery email + * Sends it + * + * @return bool True if OK or false if any problems + */ + public function sendPasswordRecoveryEmail(User $user, string $title): bool + { + $this->generateUserLink($user); + $pieces = parse_url($this->params->get('base_url')); + $domain = isset($pieces['host']) ? $pieces['host'] : ''; + + $message = _t('LOGIN_DEAR') . ' ' . $user['name'] . ",\n"; + $message .= _t('LOGIN_CLICK_FOLLOWING_LINK') . ' :' . "\n"; + $message .= '-----------------------' . "\n"; + $message .= $this->userlink . "\n"; + $message .= '-----------------------' . "\n"; + $message .= _t('LOGIN_THE_TEAM') . ' ' . $domain . "\n"; + + $subject = $title . ' ' . $domain; + // Send the email + return send_mail($this->params->get('BAZ_ADRESSE_MAIL_ADMIN'), $this->params->get('BAZ_ADRESSE_MAIL_ADMIN'), $user['email'], $subject, $message); + } + + /** + * Assessor for userlink field + * + * @return string + */ + public function getUserLink(): string + { + return $this->userlink; + } + + /** + * Assessor for userlink field + * + * @return string + */ + public function getLastUserLink(User $user): string + { + $tripleStore = $this->wiki->services->get(TripleStore::class); + $key = $tripleStore->getOne($user['name'], self::KEY_VOCABULARY, '', ''); + if ($key != null) { + $this->userlink = $this->wiki->Href('', 'MotDePassePerdu', [ + 'a' => 'recover', + 'email' => $key, + 'u' => base64_encode($user['name']) + ], false); + } else { + $this->generateUserLink($user); + } + return $this->userlink; + } + /** * update user params * for e-mail check is existing e-mail. @@ -423,4 +518,4 @@ public function logout() { $this->wiki->services->get(AuthController::class)->logout(); } -} +} \ No newline at end of file diff --git a/javascripts/users-table.js b/javascripts/users-table.js index 3d15e6ce0..3dc85030a 100644 --- a/javascripts/users-table.js +++ b/javascripts/users-table.js @@ -18,6 +18,7 @@ const usersTableService = { success(data) { const userName = data.user.name const userEmail = data.user.email + const userLink = data.user.link const { signuptime } = data.user // append In Datable const table = $(form).siblings('.dataTables_wrapper').first() @@ -33,6 +34,9 @@ const usersTableService = { '', '' ]).draw() + if (userLink !== '') { + $(`#users-table-link-change-password`).html("

"+userLink+"") + } toastMessage(_t('USERSTABLE_USER_CREATED', { name: userName }), 1100, 'alert alert-success') }, error(e) { diff --git a/lang/yeswiki_fr.php b/lang/yeswiki_fr.php index da3833bcc..906f3fc98 100644 --- a/lang/yeswiki_fr.php +++ b/lang/yeswiki_fr.php @@ -213,6 +213,7 @@ 'ERROR_ACTION_TRAIL' => 'Erreur action {{trail ...}}', 'INDICATE_THE_PARAMETER_TOC' => 'Indiquez le nom de la page sommaire, paramètre "toc"', // actions/usersettings.php + 'USER_GOTOADMIN' => 'Gestion des utilisateurices', 'USER_SETTINGS' => 'Paramètres utilisateur', 'USER_SIGN_UP' => 'S\'inscrire', 'YOU_ARE_NOW_DISCONNECTED' => 'Vous êtes maintenant déconnecté', @@ -628,4 +629,4 @@ 'REACTION_TITLE_PARAM_NEEDED' => 'Le paramètre \'titre\' est obligatoire', 'REACTION_BAD_IMAGE_FORMAT' => 'Mauvais format d\'image : doit être un fichier, un icône utf8 ou une classe Fontawesome', 'REACTION_NO_IMAGE' => 'Image manquante', -]; +]; \ No newline at end of file diff --git a/lang/yeswikijs_en.php b/lang/yeswikijs_en.php index bc0187441..00f8019d3 100644 --- a/lang/yeswikijs_en.php +++ b/lang/yeswikijs_en.php @@ -83,6 +83,7 @@ 'MULTIDELETE_END' => 'Deletions finished', 'MULTIDELETE_ERROR' => 'Item {itemId} has not been deleted! {error}', // javascripts/users-table.js + 'LINK_TO_CHANGE_PASSWORD' => "Link to change password", 'USERSTABLE_USER_CREATED' => "User '{name}' created", 'USERSTABLE_USER_NOT_CREATED' => "User '{name}' not created : {error}", 'USERSTABLE_USER_DELETED' => 'The user "{username}" was deleted.', diff --git a/lang/yeswikijs_fr.php b/lang/yeswikijs_fr.php index ffdd5c384..7d78d6998 100644 --- a/lang/yeswikijs_fr.php +++ b/lang/yeswikijs_fr.php @@ -113,6 +113,7 @@ 'MULTIDELETE_ERROR' => "L'élément {itemId} n'a pas été supprimé ! {error}", // javascripts/users-table.js + 'LINK_TO_CHANGE_PASSWORD' => "Lien pour changer le mot de passe", 'USERSTABLE_USER_CREATED' => "Utilisateur '{name}' créé", 'USERSTABLE_USER_NOT_CREATED' => "Utilisateur '{name}' non créé : {error}", 'USERSTABLE_USER_DELETED' => 'L\'utilisateur "{username}" a été supprimé.', diff --git a/templates/users-table.twig b/templates/users-table.twig index 64129fbb3..459ea17fa 100644 --- a/templates/users-table.twig +++ b/templates/users-table.twig @@ -28,6 +28,7 @@
+ {% endif %} diff --git a/tools/contact/config.yaml b/tools/contact/config.yaml index c0525e8d4..3efc09c48 100644 --- a/tools/contact/config.yaml +++ b/tools/contact/config.yaml @@ -9,6 +9,7 @@ parameters: contact_reply_to: '' # default mail to reply to contact_debug: 0 # debug mode (0 pour rien, 1 pour normal, 2 pour détaillé) contact_passphrase: '' # passphrase pour envoyer des mail + contact_disable_email_for_password: false # pour désactiver l'envoie d'email pour ré-initaliser un mot de passe (ex: LDAP, SSO) contact_editable_config_params: - 'contact_use_long_wiki_urls_in_emails' - 'contact_mail_func' @@ -19,3 +20,4 @@ parameters: - 'contact_from' - 'contact_reply_to' - 'contact_debug' + - 'contact_disable_email_for_password' diff --git a/tools/contact/lang/contact_fr.inc.php b/tools/contact/lang/contact_fr.inc.php index 9f0b94905..d466732d6 100755 --- a/tools/contact/lang/contact_fr.inc.php +++ b/tools/contact/lang/contact_fr.inc.php @@ -154,4 +154,5 @@ 'EDIT_CONFIG_HINT_CONTACT_REPLY_TO' => 'Utilisateur auquel la réponse mail sera envoyée', 'EDIT_CONFIG_HINT_CONTACT_DEBUG' => 'Mode verbeux pour débugguer (mettre 2 pour avoir des informations)', 'EDIT_CONFIG_GROUP_CONTACT' => 'Envoi des e-mails', + 'EDIT_CONFIG_HINT_CONTACT_DISABLE_EMAIL_FOR_PASSWORD' => 'Désactiver l\'envoie d\'email pour ré-initaliser un mot de passe (ex: LDAP, SSO)', ]; diff --git a/tools/login/actions/LoginAction.php b/tools/login/actions/LoginAction.php index b6c1eb514..2578162c4 100644 --- a/tools/login/actions/LoginAction.php +++ b/tools/login/actions/LoginAction.php @@ -77,10 +77,9 @@ public function formatArguments($arg) : $incomingurl ), - 'lostpasswordurl' => !empty($arg['lostpasswordurl']) - ? $this->wiki->generateLink($arg['lostpasswordurl']) - // TODO : check page name for other languages - : $this->wiki->Href('', 'MotDePassePerdu'), + 'lostpasswordurl' => ! boolval($this->params->get('contact_disable_email_for_password')) ? (! empty($arg['lostpasswordurl']) ? $this->wiki->generateLink($arg['lostpasswordurl']) : + // TODO : check page name for other languages + $this->wiki->Href('', 'MotDePassePerdu')) : '', 'class' => !empty($arg['class']) ? $arg['class'] : '', 'btnclass' => !empty($arg['btnclass']) ? $arg['btnclass'] : '', @@ -151,7 +150,7 @@ private function renderForm(string $action): string 'email' => ((isset($user['email'])) ? $user['email'] : ((isset($_POST['email'])) ? $_POST['email'] : '')), 'incomingurl' => $this->arguments['incomingurl'], 'signupurl' => $this->arguments['signupurl'], - 'lostpasswordurl' => $this->arguments['lostpasswordurl'], + 'lostpasswordurl' => ! boolval($this->params->get('contact_disable_email_for_password')) ? $this->arguments['lostpasswordurl'] : '', 'profileurl' => $this->arguments['profileurl'], 'userpage' => $this->arguments['userpage'], 'PageMenuUser' => $pageMenuUserContent, diff --git a/tools/login/actions/LostPasswordAction.php b/tools/login/actions/LostPasswordAction.php index e26117aa7..fde97cbe4 100644 --- a/tools/login/actions/LostPasswordAction.php +++ b/tools/login/actions/LostPasswordAction.php @@ -11,14 +11,8 @@ use YesWiki\Core\YesWikiAction; use YesWiki\Security\Controller\SecurityController; -if (!function_exists('send_mail')) { - require_once 'includes/email.inc.php'; -} - class LostPasswordAction extends YesWikiAction { - private const PW_SALT = 'FBcA'; - public const KEY_VOCABULARY = 'http://outils-reseaux.org/_vocabulary/key'; protected $authController; protected $errorType; @@ -137,7 +131,7 @@ private function manageSubStep(int $subStep): ?User $user = $this->userManager->getOneByEmail($email); if (!empty($user)) { $this->typeOfRendering = 'successPage'; - $this->sendPasswordRecoveryEmail($user); + $this->userManager->sendPasswordRecoveryEmail($user, _t('LOGIN_PASSWORD_LOST_FOR')); } else { $this->errorType = 'userNotFound'; $this->typeOfRendering = 'userNotFound'; @@ -184,53 +178,6 @@ private function manageSubStep(int $subStep): ?User return $user ?? null; } - /* Password recovery process (AKA reset password) - 1. A key is generated using name, email alongside with other stuff. - 2. The triple (user's name, specific key "vocabulary",key) is stored in triples table. - 3. In order to update h·er·is password, the user must provided that key. - 4. The new password is accepted only if the key matches with the value in triples table. - 5. The corresponding row is removed from triples table. - */ - - /** Part of the Password recovery process: Handles the password recovery email process. - * - * Generates the password recovery key - * Stores the (name, vocabulary, key) triple in triples table - * Generates the recovery email - * Sends it - * - * @return bool True if OK or false if any problems - */ - private function sendPasswordRecoveryEmail(User $user) - { - // Generate the password recovery key - $key = md5($user['name'] . '_' . $user['email'] . random_int(0, 10000) . date('Y-m-d H:i:s') . self::PW_SALT); - // Erase the previous triples in the trible table - $this->tripleStore->delete($user['name'], self::KEY_VOCABULARY, null, '', ''); - // Store the (name, vocabulary, key) triple in triples table - $res = $this->tripleStore->create($user['name'], self::KEY_VOCABULARY, $key, '', ''); - - // Generate the recovery email - $passwordLink = $this->wiki->Href('', '', [ - 'a' => 'recover', - 'email' => $key, - 'u' => base64_encode($user['name']), - ], false); - $pieces = parse_url($this->params->get('base_url')); - $domain = isset($pieces['host']) ? $pieces['host'] : ''; - - $message = _t('LOGIN_DEAR') . ' ' . $user['name'] . ",\n"; - $message .= _t('LOGIN_CLICK_FOLLOWING_LINK') . ' :' . "\n"; - $message .= '-----------------------' . "\n"; - $message .= $passwordLink . "\n"; - $message .= '-----------------------' . "\n"; - $message .= _t('LOGIN_THE_TEAM') . ' ' . $domain . "\n"; - - $subject = _t('LOGIN_PASSWORD_LOST_FOR') . ' ' . $domain; - // Send the email - return send_mail($this->params->get('BAZ_ADRESSE_MAIL_ADMIN'), $this->params->get('BAZ_ADRESSE_MAIL_ADMIN'), $user['email'], $subject, $message); - } - /** Part of the Password recovery process: sets the password to a new value if given the the proper recovery key (sent in a recovery email). * * In order to update h·er·is password, the user provides a key (sent using sendPasswordRecoveryEmail()) @@ -262,7 +209,7 @@ private function resetPassword(string $userName, string $key, string $password) } $this->authController->setPassword($user, $password); // Was able to update password => Remove the key from triples table - $this->tripleStore->delete($user['name'], self::KEY_VOCABULARY, $key, '', ''); + $this->tripleStore->delete($user['name'], UserManager::KEY_VOCABULARY, $key, '', ''); return true; } @@ -282,7 +229,7 @@ private function resetPassword(string $userName, string $key, string $password) private function checkEmailKey(string $hash, string $user): bool { // Pas de detournement possible car utilisation de _vocabulary/key .... - return !is_null($this->tripleStore->exist($user, self::KEY_VOCABULARY, $hash, '', '')); + return !is_null($this->tripleStore->exist($user, UserManager::KEY_VOCABULARY, $hash, '', '')); } /* End of Password recovery process (AKA reset password) */ } diff --git a/tools/login/actions/UserSettingsAction.php b/tools/login/actions/UserSettingsAction.php index a778474dd..d4385f82b 100644 --- a/tools/login/actions/UserSettingsAction.php +++ b/tools/login/actions/UserSettingsAction.php @@ -43,6 +43,7 @@ class UserSettingsAction extends YesWikiAction private $referrer; private $wantedEmail; private $wantedUserName; + private $userlink; public function formatArguments($arg) { @@ -60,6 +61,11 @@ public function run() $this->errorPasswordChange = ''; $this->referrer = ''; $user = $this->getUser($_GET ?? []); + if (!boolval($this->wiki->config['contact_disable_email_for_password']) && !empty($user)) { + $this->userlink = $this->userManager->getLastUserLink($user); + } else { + $this->userlink = ''; + } $this->doPrerenderingActions($_POST ?? [], $user); @@ -159,6 +165,7 @@ private function displayForm(?User $user = null) 'referrer' => $this->referrer, 'user' => $user, 'userLoggedIn' => $this->userLoggedIn, + 'userlink' => $this->userlink ]); } else { $captcha = $this->securityController->renderCaptchaField(); @@ -178,6 +185,7 @@ private function displayForm(?User $user = null) 'name' => $this->wantedUserName, 'email' => $this->wantedEmail, 'captcha' => $captcha, + 'userlink' => '' ]); } } @@ -226,6 +234,12 @@ private function update(array $post, User $user) $user, $sanitizedPost ); + $this->userlink = ''; + if (!boolval($this->wiki->config['contact_disable_email_for_password'])) { + if ($this->userManager->sendPasswordRecoveryEmail($user, _t('LOGIN_PASSWORD_FOR'))) { + $this->userlink = $this->userManager->getUserLink(); + } + } $user = $this->userManager->getOneByEmail($sanitizedPost['email']); @@ -277,7 +291,7 @@ private function changePassword(?User $user, array $post) $this->wiki->Redirect($this->wiki->href()); } catch (TokenNotFoundException $th) { $this->errorPasswordChange = _t('USERSETTINGS_PASSWORD_NOT_CHANGED') . ' ' . $th->getMessage(); - } catch (BadFormatPasswordException|Throwable $ex) { + } catch (BadFormatPasswordException | Throwable $ex) { // Something when wrong when updating the user in DB $this->errorPasswordChange = _t('USERSETTINGS_PASSWORD_NOT_CHANGED') . ' ' . $ex->getMessage(); } @@ -303,8 +317,10 @@ private function signup(array $post) $password = isset($post['password']) && is_string($post['password']) ? $post['password'] : ''; if (!empty($emptyInputsParametersNames)) { $this->error = str_replace('{parameters}', implode(',', $emptyInputsParametersNames), _t('USERSETTINGS_SIGNUP_MISSING_INPUT')); - } elseif ($this->authController->checkPasswordValidateRequirements($password) && - $post['confpassword'] !== $password) { + } elseif ( + $this->authController->checkPasswordValidateRequirements($password) && + $post['confpassword'] !== $password + ) { $this->error = _t('USER_PASSWORDS_NOT_IDENTICAL') . '.'; } else { // Password is correct $_POST['submit'] = SecurityController::EDIT_PAGE_SUBMIT_VALUE; @@ -346,4 +362,4 @@ private function checklogged(array $post) { $this->error = _t('USER_MUST_ACCEPT_COOKIES_TO_GET_CONNECTED') . '.'; } -} +} \ No newline at end of file diff --git a/tools/login/lang/login_en.inc.php b/tools/login/lang/login_en.inc.php index 85b4e28c9..07dea20e4 100644 --- a/tools/login/lang/login_en.inc.php +++ b/tools/login/lang/login_en.inc.php @@ -37,7 +37,9 @@ 'LOGIN_DEAR' => 'Dear', 'LOGIN_CLICK_FOLLOWING_LINK' => 'Click on the link below to reset your password', 'LOGIN_THE_TEAM' => 'The team from', + 'LOGIN_PASSWORD_FOR' => 'Password for', 'LOGIN_PASSWORD_LOST_FOR' => 'Lost password for', + 'LAST_LINK_TO_CHANGE_PASSWORD' => 'Last link to change password', // 'LOGIN_NO_SIGNUP_IN_THIS_PERIOD' => 'Il n\'y a pas d\'inscription pour cette période.', // actions/login.php // 'LOGIN_COOKIES_ERROR' => 'Vous devez accepter les cookies pour pouvoir vous connecter.', diff --git a/tools/login/lang/login_fr.inc.php b/tools/login/lang/login_fr.inc.php index 3a90550b0..2b53fa414 100755 --- a/tools/login/lang/login_fr.inc.php +++ b/tools/login/lang/login_fr.inc.php @@ -37,10 +37,12 @@ 'LOGIN_DEAR' => 'Cher', 'LOGIN_CLICK_FOLLOWING_LINK' => 'Cliquez sur le lien suivant pour ré-initialiser votre mot de passe', 'LOGIN_THE_TEAM' => 'L\'équipe de', + 'LOGIN_PASSWORD_FOR' => 'Mot de passe pour', 'LOGIN_PASSWORD_LOST_FOR' => 'Mot de passe perdu pour', 'LOGIN_NO_SIGNUP_IN_THIS_PERIOD' => 'Il n\'y a pas d\'inscription pour cette période.', 'LOGIN_MY_OPTIONS' => 'Mes options', 'LOGIN_MY_CONTENTS' => 'Mes contenus', + 'LINK_TO_CHANGE_PASSWORD' => 'Lien pour changer le mot de passe', // actions/login.php 'LOGIN_COOKIES_ERROR' => 'Vous devez accepter les cookies pour pouvoir vous connecter.', diff --git a/tools/login/templates/usersettings.twig b/tools/login/templates/usersettings.twig index e532879e8..1be274aeb 100644 --- a/tools/login/templates/usersettings.twig +++ b/tools/login/templates/usersettings.twig @@ -1,4 +1,5 @@ -

{{ _t('USER_SETTINGS') }}{{ adminIsActing ? ' — ' ~ user.name : ''}}

+{% if adminIsActing %} {{ _t('USER_GOTOADMIN')}}{% endif %} +

{{ _t('USER_SETTINGS') }}{% if adminIsActing %} — {{user.name}}{% endif %}

{% if errorUpdate is not empty %}
{{ errorUpdate }}
@@ -25,6 +26,14 @@ #} +{% if userlink is not empty %} +
+
+
+ {{ userlink }} +
+
+{% endif %}
@@ -85,4 +94,4 @@
-{% endif %} +{% endif %} \ No newline at end of file