Skip to content

Commit

Permalink
Merge pull request #1178 from YesWiki/send_email_to_reset_password_wh…
Browse files Browse the repository at this point in the history
…en_user_created

When you create a user in YesWiki, a password is created but you do n…
  • Loading branch information
mrflos authored Jul 31, 2024
2 parents 7f0b298 + cd2d3e4 commit bf5f87f
Show file tree
Hide file tree
Showing 16 changed files with 167 additions and 71 deletions.
8 changes: 7 additions & 1 deletion includes/controllers/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,21 @@ 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']],
'user' => [
'name' => $user['name'],
'email' => $user['email'],
'signuptime' => $user['signuptime'],
'link' => $link
],
];
} catch (UserNameAlreadyUsedException $th) {
Expand Down
9 changes: 9 additions & 0 deletions includes/controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 97 additions & 2 deletions includes/services/UserManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,23 @@
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;
protected $dbService;
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,
Expand All @@ -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
Expand Down Expand Up @@ -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'));
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -423,4 +518,4 @@ public function logout()
{
$this->wiki->services->get(AuthController::class)->logout();
}
}
}
4 changes: 4 additions & 0 deletions javascripts/users-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -33,6 +34,9 @@ const usersTableService = {
'',
''
]).draw()
if (userLink !== '') {
$(`#users-table-link-change-password`).html("<br/><label>"+_t('LINK_TO_CHANGE_PASSWORD')+"</label><br/><a href='"+userLink+"' target='_blank'>"+userLink+"</a>")
}
toastMessage(_t('USERSTABLE_USER_CREATED', { name: userName }), 1100, 'alert alert-success')
},
error(e) {
Expand Down
3 changes: 2 additions & 1 deletion lang/yeswiki_fr.php
Original file line number Diff line number Diff line change
Expand Up @@ -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é',
Expand Down Expand Up @@ -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',
];
];
1 change: 1 addition & 0 deletions lang/yeswikijs_en.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
1 change: 1 addition & 0 deletions lang/yeswikijs_fr.php
Original file line number Diff line number Diff line change
Expand Up @@ -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é.',
Expand Down
1 change: 1 addition & 0 deletions templates/users-table.twig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
</div>
</form>
<div class="clearfix"></div>
<div id='users-table-link-change-password' class="form-row"></div>
{% endif %}
<table class="table table-striped" data-order='[[ {{ isAdmin ? 4 : 2 }}, "desc" ]]'>
<thead>
Expand Down
2 changes: 2 additions & 0 deletions tools/contact/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -19,3 +20,4 @@ parameters:
- 'contact_from'
- 'contact_reply_to'
- 'contact_debug'
- 'contact_disable_email_for_password'
1 change: 1 addition & 0 deletions tools/contact/lang/contact_fr.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
];
9 changes: 4 additions & 5 deletions tools/login/actions/LoginAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'] : '',
Expand Down Expand Up @@ -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,
Expand Down
59 changes: 3 additions & 56 deletions tools/login/actions/LostPasswordAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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;
}
Expand All @@ -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) */
}
Loading

0 comments on commit bf5f87f

Please sign in to comment.