diff --git a/CHANGES.md b/CHANGES.md index 66f671d6..d421e9c6 100755 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,13 +2,15 @@ ## Versions 13.5.00 **Améliorations :** +- Validation de la connexion au site grâce à l'envoi d'un code par email. L'option est activée depuis la configuration du site, onglet connexion. Elle s'active par groupe montant, exemple "éditeur" pour éditeurs et administrateurs. - Optimisation du chargement des variables de classe. - Suppression de redondance de déclaration des charset. -**Corrections : ** +**Corrections :** - Corrige un bug de changement de mot de passe pour les comptes non admin. - Blog 7.12, corrige un bug d'affichage des articles lorsque le thème Moderne est sélectionné. - Corrige un dysfonctionnement de la fonction de tronquage subword qui perturbait l'affichage des articles de blog. +- Activation de la mémorisation de l'onglet actif dans la configuration après validation du formulaire ou visite d'une autre page du site. ## Versions 13.4.00 ** Améliorations :** diff --git a/core/class/helper.class.php b/core/class/helper.class.php index 09365b25..f8853c3c 100644 --- a/core/class/helper.class.php +++ b/core/class/helper.class.php @@ -343,7 +343,7 @@ class helper public static function checkRewrite() { // N'interroge que le serveur Apache - if (strpos($_SERVER["SERVER_SOFTWARE"], 'Apache') > 0) { + if ((helper::checkServerSoftware() === false)) { self::$rewriteStatus = false; } else { // Ouvre et scinde le fichier .htaccess @@ -353,6 +353,14 @@ class helper } return self::$rewriteStatus; } + + /** + * Retourne vrai ou faux selon que le serveur est comptatible avec htaccess + * @return bool + */ + public static function checkServerSoftware() { + return (stripos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false || stripos($_SERVER['SERVER_SOFTWARE'], 'LiteSpeed') !== false); + } /** * Renvoie le numéro de version de Zwii est en ligne diff --git a/core/module/config/config.php b/core/module/config/config.php index 674daa7b..f431f1d6 100644 --- a/core/module/config/config.php +++ b/core/module/config/config.php @@ -496,7 +496,8 @@ class config extends common 'autoDisconnect' => $this->getInput('connectAutoDisconnect', helper::FILTER_BOOLEAN), 'captchaType' => $this->getInput('connectCaptchaType'), 'showPassword' => $this->getInput('connectShowPassword', helper::FILTER_BOOLEAN), - 'redirectLogin' => $this->getInput('connectRedirectLogin', helper::FILTER_BOOLEAN) + 'redirectLogin' => $this->getInput('connectRedirectLogin', helper::FILTER_BOOLEAN), + 'mailAuth' => $this->getInput('connectAuthMail', helper::FILTER_BOOLEAN), ] ] ]); diff --git a/core/module/config/view/connect/connect.php b/core/module/config/view/connect/connect.php index 2f6ee633..86368471 100644 --- a/core/module/config/view/connect/connect.php +++ b/core/module/config/view/connect/connect.php @@ -3,13 +3,7 @@

-

-
-
+
'Limitation des tentatives', 'selected' => $this->getData(['config', 'connect', 'attempt']) ]); ?>
-
+
'Blocage après échecs', 'selected' => $this->getData(['config', 'connect', 'timeout']) ]); ?>
-
- - helper::baseUrl() . 'config/blacklistDownload', - 'value' => 'Télécharger la liste', - 'ico' => 'download' - ]); ?> -
-
- 'buttonRed', - 'href' => helper::baseUrl() . 'config/blacklistReset', - 'value' => 'Réinitialiser la liste', - 'ico' => 'trash' +
+ 'Aucune'], self::$groupNews), [ + 'label' => 'Validation par messagerie', + 'selected' => $this->getData(['config', 'connect', 'mailAuth']), + 'help' => 'La connexion est confirmée par une clé adressée par messagerie. Depuis le groupe sélectionnée et les groupes supérieurs.' ]); ?>
+
+
+
+
+
+

+

- $this->getData(['config', 'connect', 'captcha']) ]); ?>
@@ -92,40 +78,66 @@

-

-
- $this->getData(['config', 'connect', 'log']) - ]); ?> +
+
+
+ $this->getData(['config', 'connect', 'log']) + ]); ?> +
+
+ 'Anonymat des adresses IP', + 'selected' => $this->getData(['config', 'connect', 'anonymousIp']), + 'help' => 'La règlementation française impose un anonymat de niveau 2' + ]); ?> +
+
+
+
+ helper::baseUrl() . 'config/logDownload', + 'value' => 'Télécharger le journal', + 'ico' => 'download' + ]); ?> +
+
+ 'buttonRed', + 'href' => helper::baseUrl() . 'config/logReset', + 'value' => 'Réinitialiser le journal', + 'ico' => 'trash' + ]); ?> +
+
-
- 'Anonymat des adresses IP', - 'selected' => $this->getData(['config', 'connect', 'anonymousIp']), - 'help' => 'La règlementation française impose un anonymat de niveau 2' - ]); ?> -
-
- helper::baseUrl() . 'config/logDownload', - 'value' => 'Télécharger le journal', - 'ico' => 'download' - ]); ?> -
-
- 'buttonRed', - 'href' => helper::baseUrl() . 'config/logReset', - 'value' => 'Réinitialiser le journal', - 'ico' => 'trash' - ]); ?> +
+
+
+ + helper::baseUrl() . 'config/blacklistDownload', + 'value' => 'Télécharger la liste', + 'ico' => 'download' + ]); ?> +
+
+ 'buttonRed', + 'href' => helper::baseUrl() . 'config/blacklistReset', + 'value' => 'Réinitialiser la liste', + 'ico' => 'trash' + ]); ?> +
+
diff --git a/core/module/config/view/index/index.php b/core/module/config/view/index/index.php index e17d4ae0..5551fb4c 100644 --- a/core/module/config/view/index/index.php +++ b/core/module/config/view/index/index.php @@ -22,23 +22,23 @@ 'Configuration', 'class' => 'buttonTab', - //'href' => helper::baseUrl() . 'config/register/setup' + 'href' => helper::baseUrl() . 'config/register/setup' ]); ?> 'Référencement', 'class' => 'buttonTab', - //'href' => helper::baseUrl() . 'config/register/social' + 'href' => helper::baseUrl() . 'config/register/social' ]); ?> 'Connexion', 'class' => 'buttonTab', - //'href' => helper::baseUrl() . 'config/register/connect' + 'href' => helper::baseUrl() . 'config/register/connect' ]); ?> 'Réseau', 'class' => 'buttonTab', - //'href' => helper::baseUrl() . 'config/register/network' + 'href' => helper::baseUrl() . 'config/register/network' ]); ?>
diff --git a/core/module/config/view/setup/setup.php b/core/module/config/view/setup/setup.php index e400b866..d254279a 100644 --- a/core/module/config/view/setup/setup.php +++ b/core/module/config/view/setup/setup.php @@ -44,7 +44,7 @@ helper::checkRewrite(), 'help' => 'Supprime le point d\'interrogation dans les URL, l\'option est indisponible avec les autres serveurs Web', - 'disabled' => stripos($_SERVER["SERVER_SOFTWARE"], 'Apache') === false and $module->isModRewriteEnabled() + 'disabled' => helper::checkServerSoftware() === false and $module->isModRewriteEnabled() ]); ?>
diff --git a/core/module/user/user.php b/core/module/user/user.php index 20c69b36..928e5556 100644 --- a/core/module/user/user.php +++ b/core/module/user/user.php @@ -26,6 +26,7 @@ class user extends common 'logout' => self::GROUP_MEMBER, 'forgot' => self::GROUP_VISITOR, 'login' => self::GROUP_VISITOR, + 'auth' => self::GROUP_VISITOR, 'reset' => self::GROUP_VISITOR, 'profil' => self::GROUP_ADMIN, 'profilEdit' => self::GROUP_ADMIN, @@ -66,7 +67,7 @@ class user extends common public static $groupProfils = [ self::GROUP_MEMBER => 'Membre', - self::GROUP_EDITOR => 'Éditeur' + self::GROUP_EDITOR => 'Éditeur', ]; public static $listModules = []; @@ -1043,37 +1044,14 @@ class user extends common and $this->getData(['user', $userId, 'group']) >= self::GROUP_MEMBER and $captcha === true ) { - // RAZ + + // RAZ des compteurs de blocage $this->setData(['user', $userId, 'connectFail', 0], false); $this->setData(['user', $userId, 'connectTimeout', 0], false); - // Clé d'authenfication - $authKey = uniqid('', true) . bin2hex(random_bytes(8)); - $this->setData(['user', $userId, 'authKey', $authKey]); - - // Validité du cookie - $expire = $this->getInput('userLoginLongTime', helper::FILTER_BOOLEAN) === true ? strtotime("+1 year") : 0; - switch ($this->getInput('userLoginLongTime', helper::FILTER_BOOLEAN)) { - case false: - // Cookie de session - setcookie('ZWII_USER_ID', $userId, $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); - //setcookie('ZWII_USER_PASSWORD', $this->getData(['user', $userId, 'password']), $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); - - // Connexion par clé - setcookie('ZWII_AUTH_KEY', $authKey, $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); - break; - default: - // Cookie persistant - setcookie('ZWII_USER_ID', $userId, $expire, helper::baseUrl(false, false)); - //setcookie('ZWII_USER_PASSWORD', $this->getData(['user', $userId, 'password']), $expire, helper::baseUrl(false, false)); - - // Connexion par clé - setcookie('ZWII_AUTH_KEY', $authKey, $expire, helper::baseUrl(false, false)); - break; - } - // Accès multiples avec le même compte $this->setData(['user', $userId, 'accessCsrf', $_SESSION['csrf']], false); + // Valeurs en sortie lorsque le site est en maintenance et que l'utilisateur n'est pas administrateur if ( $this->getData(['config', 'maintenance']) @@ -1085,7 +1063,49 @@ class user extends common 'state' => false ]); } else { - $logStatus = 'Connexion réussie'; + /** + * Le site n'est pas en maintenance + * Double authentification en cas de saisie correcte + */ + + // Clé d'authenfication utlisée pour lié le compte au cookie au lieu de stocke le hash du mot de passe + $authKey = uniqid('', true) . bin2hex(random_bytes(8)); + if ($this->getData(['config', 'connect', 'mailAuth']) >= $this->getData(['user', $userId, 'group'])) { + $logStatus = 'Envoi du mail d\'authentification'; + // Redirection vers la page d'authentification + $authRedirect = 'user/auth/'; + // Stocker la clé envoyée par email + $this->setData(['user', $userId, 'authKey', rand(100000, 999999)]); + + } else { + $logStatus = 'Connexion réussie'; + // La page d'autentification est vide + $authRedirect = ''; + $this->setData(['user', $userId, 'authKey', $authKey]); + + } + + // Validité du cookie + $expire = $this->getInput('userLoginLongTime', helper::FILTER_BOOLEAN) === true ? strtotime("+1 year") : 0; + switch ($this->getInput('userLoginLongTime', helper::FILTER_BOOLEAN)) { + case false: + // Cookie de session + setcookie('ZWII_USER_ID', $userId, $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); + //setcookie('ZWII_USER_PASSWORD', $this->getData(['user', $userId, 'password']), $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); + + // Connexion par clé + setcookie('ZWII_AUTH_KEY', $authKey, $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); + break; + default: + // Cookie persistant + setcookie('ZWII_USER_ID', $userId, $expire, helper::baseUrl(false, false)); + //setcookie('ZWII_USER_PASSWORD', $this->getData(['user', $userId, 'password']), $expire, helper::baseUrl(false, false)); + + // Connexion par clé + setcookie('ZWII_AUTH_KEY', $authKey, $expire, helper::baseUrl(false, false)); + break; + } + $pageId = $this->getUrl(2); if ( $this->getData(['config', 'page404']) === $pageId @@ -1093,7 +1113,7 @@ class user extends common ) { $pageId = ''; } - $redirect = ($pageId && strpos($pageId, 'user_reset') !== 0) ? helper::baseUrl() . str_replace('_', '/', str_replace('__', '#', $pageId)) : helper::baseUrl(); + $redirect = ($pageId && strpos($pageId, 'user_reset') !== 0) ? helper::baseUrl() . $authRedirect . str_replace('_', '/', str_replace('__', '#', $pageId)) : helper::baseUrl() . $authRedirect; // Valeurs en sortie $this->addOutput([ 'notification' => sprintf(helper::translate('Bienvenue %s %s'), $this->getData(['user', $userId, 'firstname']), $this->getData(['user', $userId, 'lastname'])), @@ -1142,16 +1162,107 @@ class user extends common ]); } + /** + * + * Validation de la connexion par email + * @return void + */ + public function auth() + { + // Soumission du formulaire + if ( + $this->isPost() + ) { + // Vérifier la clé saisie + $targetKey = $this->getData(['user', $this->getUser('id'), 'authKey']); + $inputKey = $this->getInput('userAuthKey', helper::FILTER_INT); + if ( + $targetKey === $inputKey && + $this->getData(['user', $this->getUser('id'), 'connectTimeout']) + 3600 >= time() + ) { + $pageId = $this->getUrl(2); + // La fiche de l'utilisateur contient la clé d'authentification + $this->setData(['user', $this->getUser('id'), 'authKey', $this->getInput('ZWII_AUTH_KEY')]); + $redirect = ($pageId && strpos($pageId, 'user_reset') !== 0) ? helper::baseUrl() . str_replace('_', '/', str_replace('__', '#', $pageId)) : helper::baseUrl(); + // Journalisation + $this->saveLog('Connexion réussie'); + // Réinitialiser le compteur de temps + $this->setData(['user', $this->getUser('id'), 'connectTimeout', 0]); + // Valeurs en sortie + $this->addOutput([ + 'redirect' => $redirect, + 'notification' => helper::translate('Connexion réussie'), + 'state' => true + ]); + } else { + + // Supprime la clé stockée et le temps limite + $this->deleteData(['user', $this->getUser('id'), 'authKey']); + // Réinitialiser le compteur de temps + $this->setData(['user', $this->getUser('id'), 'connectTimeout', 0]); + + // Détruit les cookies d'authenfication + helper::deleteCookie('ZWII_USER_ID'); + helper::deleteCookie('ZWII_AUTH_KEY'); + + // Détruit la session + session_destroy(); + + // Journalisation + $this->saveLog('Erreur de vérification de la clé envoyée par email ' . $this->getUser('id')); + + // Valeurs en sortie + $this->addOutput([ + 'redirect' => helper::baseUrl(), + 'notification' => helper::translate('La clé est incorrecte'), + 'state' => false + ]); + } + } else { + /** + * Envoi d'un email contenant une clé + * Stockage de la clé dans le compte de l'utilisateur + */ + // La clé est envoyée une seule fois + $sent = false; + if ( + $this->getData(['user', $this->getUser('id'), 'authKey']) + && $this->getData(['user', $this->getUser('id'), 'connectTimeout']) === 0 + ) { + $sent = $this->sendMail( + $this->getUser('mail'), + 'Tentative de connexion à votre', + //'Bonjour ' . $item['prenom'] . ' ' . $item['nom'] . ',

' . + '

Clé de validation à saisir dans le formulaire :

' . + '

' . $this->getData(['user', $this->getUser('id'), 'authKey']) . '

', + null, + $this->getData(['config', 'smtp', 'from']) + ); + // Stocker l'envoi de l'email + $this->setData(['user', $this->getUser('id'), 'connectTimeout', time()]); + } + + // Message envoyé sinon la connexion est réalisée pour ne pas bloquer. + if ($sent === false) { + + } + // Valeurs en sortie + $this->addOutput([ + 'title' => helper::translate('Double authentification'), + 'view' => 'auth', + 'display' => self::DISPLAY_LAYOUT_LIGHT, + ]); + } + } /** * Déconnexion */ public function logout() { + // Détruit les cookies d'authenfication helper::deleteCookie('ZWII_USER_ID'); - //helper::deleteCookie('ZWII_USER_PASSWORD'); helper::deleteCookie('ZWII_AUTH_KEY'); - // Détruit la session session_destroy(); diff --git a/core/module/user/view/auth/auth.css b/core/module/user/view/auth/auth.css new file mode 100644 index 00000000..e2179322 --- /dev/null +++ b/core/module/user/view/auth/auth.css @@ -0,0 +1,36 @@ +/** + * This file is part of Zwii. + * + * For full copyright and license information, please see the LICENSE + * file that was distributed with this source code. + * + * @author Rémi Jean + * @copyright Copyright (C) 2008-2018, Rémi Jean + * @author Frédéric Tempez + * @copyright Copyright (C) 2018-2024, Frédéric Tempez + * @license CC Attribution-NonCommercial-NoDerivatives 4.0 International + * @link http://zwiicms.fr/ + */ + +/** @import url("site/data/admin.css"); */ + +/** NE PAS EFFACER +* admin.css +*/ + +@media screen and (max-width: 768px) { + #buttonsContainer { + display: grid; + } + #loginContainer { + order: 1; + } + #backContainer{ + order: 2; + } +} + +#userAuthKey { + text-align: center; + font-size: 1.3rem; +} \ No newline at end of file diff --git a/core/module/user/view/auth/auth.php b/core/module/user/view/auth/auth.php new file mode 100644 index 00000000..ca948b6f --- /dev/null +++ b/core/module/user/view/auth/auth.php @@ -0,0 +1,23 @@ + +
+
+ helper::translate('Clé reçue par couriel') + ]); ?> +
+
+
+
+ $this->getUrl(2) ? helper::baseUrl() . ' user/login' . str_replace('_', '/', str_replace('__', '#', $this->getUrl(2))) : helper::baseUrl() . ' user/login', + 'value' => template::ico('left') + ]); ?> +
+
+ helper::translate('Authentification'), + 'ico' => '' + ]); ?> +
+
+ \ No newline at end of file