From c7a28e836edf4e681fe2e0395ab9f11cc90032c9 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 11 Jan 2014 15:38:29 +0100 Subject: README blibliothèques incluses + ajout librairies pour le mot de passe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Préparation https://github.com/marienfressinaud/FreshRSS/issues/104 --- lib/password_compat.php | 279 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 lib/password_compat.php (limited to 'lib') diff --git a/lib/password_compat.php b/lib/password_compat.php new file mode 100644 index 000000000..e8ec02885 --- /dev/null +++ b/lib/password_compat.php @@ -0,0 +1,279 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @copyright 2012 The Authors + */ + +namespace { + +if (!defined('PASSWORD_DEFAULT')) { + + define('PASSWORD_BCRYPT', 1); + define('PASSWORD_DEFAULT', PASSWORD_BCRYPT); + + /** + * Hash the password using the specified algorithm + * + * @param string $password The password to hash + * @param int $algo The algorithm to use (Defined by PASSWORD_* constants) + * @param array $options The options for the algorithm to use + * + * @return string|false The hashed password, or false on error. + */ + function password_hash($password, $algo, array $options = array()) { + if (!function_exists('crypt')) { + trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING); + return null; + } + if (!is_string($password)) { + trigger_error("password_hash(): Password must be a string", E_USER_WARNING); + return null; + } + if (!is_int($algo)) { + trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING); + return null; + } + $resultLength = 0; + switch ($algo) { + case PASSWORD_BCRYPT: + // Note that this is a C constant, but not exposed to PHP, so we don't define it here. + $cost = 10; + if (isset($options['cost'])) { + $cost = $options['cost']; + if ($cost < 4 || $cost > 31) { + trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING); + return null; + } + } + // The length of salt to generate + $raw_salt_len = 16; + // The length required in the final serialization + $required_salt_len = 22; + $hash_format = sprintf("$2y$%02d$", $cost); + // The expected length of the final crypt() output + $resultLength = 60; + break; + default: + trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING); + return null; + } + $salt_requires_encoding = false; + if (isset($options['salt'])) { + switch (gettype($options['salt'])) { + case 'NULL': + case 'boolean': + case 'integer': + case 'double': + case 'string': + $salt = (string) $options['salt']; + break; + case 'object': + if (method_exists($options['salt'], '__tostring')) { + $salt = (string) $options['salt']; + break; + } + case 'array': + case 'resource': + default: + trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING); + return null; + } + if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) { + trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING); + return null; + } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) { + $salt_requires_encoding = true; + } + } else { + $buffer = ''; + $buffer_valid = false; + if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) { + $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM); + if ($buffer) { + $buffer_valid = true; + } + } + if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) { + $buffer = openssl_random_pseudo_bytes($raw_salt_len); + if ($buffer) { + $buffer_valid = true; + } + } + if (!$buffer_valid && @is_readable('/dev/urandom')) { + $f = fopen('/dev/urandom', 'r'); + $read = PasswordCompat\binary\_strlen($buffer); + while ($read < $raw_salt_len) { + $buffer .= fread($f, $raw_salt_len - $read); + $read = PasswordCompat\binary\_strlen($buffer); + } + fclose($f); + if ($read >= $raw_salt_len) { + $buffer_valid = true; + } + } + if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) { + $bl = PasswordCompat\binary\_strlen($buffer); + for ($i = 0; $i < $raw_salt_len; $i++) { + if ($i < $bl) { + $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255)); + } else { + $buffer .= chr(mt_rand(0, 255)); + } + } + } + $salt = $buffer; + $salt_requires_encoding = true; + } + if ($salt_requires_encoding) { + // encode string with the Base64 variant used by crypt + $base64_digits = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + $bcrypt64_digits = + './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $base64_string = base64_encode($salt); + $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits); + } + $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len); + + $hash = $hash_format . $salt; + + $ret = crypt($password, $hash); + + if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) { + return false; + } + + return $ret; + } + + /** + * Get information about the password hash. Returns an array of the information + * that was used to generate the password hash. + * + * array( + * 'algo' => 1, + * 'algoName' => 'bcrypt', + * 'options' => array( + * 'cost' => 10, + * ), + * ) + * + * @param string $hash The password hash to extract info from + * + * @return array The array of information about the hash. + */ + function password_get_info($hash) { + $return = array( + 'algo' => 0, + 'algoName' => 'unknown', + 'options' => array(), + ); + if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) { + $return['algo'] = PASSWORD_BCRYPT; + $return['algoName'] = 'bcrypt'; + list($cost) = sscanf($hash, "$2y$%d$"); + $return['options']['cost'] = $cost; + } + return $return; + } + + /** + * Determine if the password hash needs to be rehashed according to the options provided + * + * If the answer is true, after validating the password using password_verify, rehash it. + * + * @param string $hash The hash to test + * @param int $algo The algorithm used for new password hashes + * @param array $options The options array passed to password_hash + * + * @return boolean True if the password needs to be rehashed. + */ + function password_needs_rehash($hash, $algo, array $options = array()) { + $info = password_get_info($hash); + if ($info['algo'] != $algo) { + return true; + } + switch ($algo) { + case PASSWORD_BCRYPT: + $cost = isset($options['cost']) ? $options['cost'] : 10; + if ($cost != $info['options']['cost']) { + return true; + } + break; + } + return false; + } + + /** + * Verify a password against a hash using a timing attack resistant approach + * + * @param string $password The password to verify + * @param string $hash The hash to verify against + * + * @return boolean If the password matches the hash + */ + function password_verify($password, $hash) { + if (!function_exists('crypt')) { + trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING); + return false; + } + $ret = crypt($password, $hash); + if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) { + return false; + } + + $status = 0; + for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) { + $status |= (ord($ret[$i]) ^ ord($hash[$i])); + } + + return $status === 0; + } +} + +} + +namespace PasswordCompat\binary { + /** + * Count the number of bytes in a string + * + * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension. + * In this case, strlen() will count the number of *characters* based on the internal encoding. A + * sequence of bytes might be regarded as a single multibyte character. + * + * @param string $binary_string The input string + * + * @internal + * @return int The number of bytes + */ + function _strlen($binary_string) { + if (function_exists('mb_strlen')) { + return mb_strlen($binary_string, '8bit'); + } + return strlen($binary_string); + } + + /** + * Get a substring based on byte limits + * + * @see _strlen() + * + * @param string $binary_string The input string + * @param int $start + * @param int $length + * + * @internal + * @return string The substring + */ + function _substr($binary_string, $start, $length) { + if (function_exists('mb_substr')) { + return mb_substr($binary_string, $start, $length, '8bit'); + } + return substr($binary_string, $start, $length); + } + +} -- cgit v1.2.3 From d58886a937cbe425163526fc2ba3d2a118602035 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 12 Jan 2014 03:10:31 +0100 Subject: Implémentation de l'indentification par mot de passe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implémentation de https://github.com/marienfressinaud/FreshRSS/issues/104 --- CHANGELOG | 3 ++ app/Controllers/indexController.php | 52 +++++++++++++++++++++++++++-- app/Controllers/javascriptController.php | 12 +++---- app/Controllers/usersController.php | 26 ++++++++++++--- app/FreshRSS.php | 27 ++++++++++++--- app/i18n/en.php | 7 ++-- app/i18n/fr.php | 7 ++-- app/layout/header.phtml | 51 +++++++++++++++++++--------- app/views/configure/users.phtml | 16 ++++++--- app/views/helpers/javascript_vars.phtml | 4 +-- app/views/helpers/view/login.phtml | 43 ++++++++++++++++++++++++ app/views/index/index.phtml | 14 ++------ lib/Minz/Configuration.php | 3 +- lib/Minz/FrontController.php | 2 +- p/scripts/main.js | 57 ++++++++++++++++++++++++++++++-- 15 files changed, 265 insertions(+), 59 deletions(-) create mode 100644 app/views/helpers/view/login.phtml (limited to 'lib') diff --git a/CHANGELOG b/CHANGELOG index fe856fe4a..5c9b56465 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,8 @@ * Nouveau mode multi-utilisateur * L’utilisateur par défaut (administrateur) peut créer et supprimer d’autres utilisateurs * Nécessite un contrôle d’accès, soit : + * par le nouveau mode de connexion par formulaire (nom d’utilisateur + mot de passe) + * relativement sûr même sans HTTPS (le mot de passe n’est pas transmis en clair) * par HTTP (par exemple sous Apache en créant un fichier ./p/i/.htaccess et .htpasswd) * le nom d’utilisateur HTTP doit correspondre au nom d’utilisateur FreshRSS * par Mozilla Persona, en renseignant l’adresse courriel des utilisateurs @@ -68,6 +70,7 @@ * Réorganisation des fichiers et répertoires, en particulier : * Tous les fichiers utilisateur sont dans “./data/” (y compris “cache”, “favicons”, et “log”) * Déplacement de “./app/configuration/application.ini” vers “./data/config.php” + * Meilleure sécurité et compatibilité * Déplacement de “./public/data/Configuration.array.php” vers “./data/*_user.php” * Déplacement de “./public/” vers “./p/” * Déplacement de “./public/index.php” vers “./p/i/index.php” (voir cookie ci-dessous) diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index 81dfefabb..772a08f30 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -47,8 +47,6 @@ class FreshRSS_index_Controller extends Minz_ActionController { $this->view->_useLayout (false); header('Content-Type: application/rss+xml; charset=utf-8'); } else { - Minz_View::appendScript (Minz_Url::display ('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js'))); - if ($output === 'global') { Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js'))); } @@ -290,8 +288,56 @@ class FreshRSS_index_Controller extends Minz_ActionController { } public function logoutAction () { + $this->view->_useLayout(false); + invalidateHttpCache(); + Minz_Session::_param('currentUser'); + Minz_Session::_param('mail'); + Minz_Session::_param('passwordHash'); + } + + public function formLoginAction () { $this->view->_useLayout (false); - Minz_Session::_param ('mail'); + if (Minz_Request::isPost()) { + $ok = false; + $nonce = Minz_Session::param('nonce'); + $username = Minz_Request::param('username', ''); + $c = Minz_Request::param('challenge', ''); + if (ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce)) { + if (!function_exists('password_verify')) { + include_once(LIB_PATH . '/password_compat.php'); + } + try { + $conf = new FreshRSS_Configuration($username); + $s = $conf->passwordHash; + $ok = password_verify($nonce . $s, $c); + if ($ok) { + Minz_Session::_param('currentUser', $username); + Minz_Session::_param('passwordHash', $s); + } else { + Minz_Log::record('Password mismatch for user ' . $username . ', nonce=' . $nonce . ', c=' . $c, Minz_Log::WARNING); + } + } catch (Minz_Exception $me) { + Minz_Log::record('Login failure: ' . $me->getMessage(), Minz_Log::WARNING); + } + } + if (!$ok) { + $notif = array( + 'type' => 'bad', + 'content' => Minz_Translate::t('invalid_login') + ); + Minz_Session::_param('notification', $notif); + } + } + invalidateHttpCache(); + Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); + } + + public function formLogoutAction () { + $this->view->_useLayout(false); invalidateHttpCache(); + Minz_Session::_param('currentUser'); + Minz_Session::_param('mail'); + Minz_Session::_param('passwordHash'); + Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); } } diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index e29f439d8..02e424437 100755 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -17,7 +17,7 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { $this->view->categories = $catDAO->listCategories(true, false); } - // For Web-form login + //For Web-form login public function nonceAction() { header('Content-Type: application/json; charset=UTF-8'); header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T')); @@ -29,15 +29,15 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { if (ctype_alnum($user)) { try { $conf = new FreshRSS_Configuration($user); - $hash = $conf->passwordHash; //CRYPT_BLOWFISH - Blowfish hashing with a salt as follows: "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z". - if (strlen($hash) >= 60) { - $this->view->salt1 = substr($hash, 0, 29); + $s = $conf->passwordHash; + if (strlen($s) >= 60) { + $this->view->salt1 = substr($s, 0, 29); //CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z". $this->view->nonce = sha1(Minz_Configuration::salt() . uniqid(mt_rand(), true)); - Minz_Session::_param ('nonce', $this->view->nonce); + Minz_Session::_param('nonce', $this->view->nonce); return; //Success } } catch (Minz_Exception $me) { - Minz_Log::record ('Login failure: ' . $me->getMessage(), Minz_Log::WARNING); + Minz_Log::record('Login failure: ' . $me->getMessage(), Minz_Log::WARNING); } } $this->view->nonce = ''; //Failure diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php index 8954c845d..cb5ebd209 100644 --- a/app/Controllers/usersController.php +++ b/app/Controllers/usersController.php @@ -21,18 +21,20 @@ class FreshRSS_users_Controller extends Minz_ActionController { if (!function_exists('password_hash')) { include_once(LIB_PATH . '/password_compat.php'); } - $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT); //A bit expensive, on purpose + $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => 8)); //This will also have to be computed client side on mobile devices, so do not use a too high cost $passwordPlain = ''; + $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js $this->view->conf->_passwordHash($passwordHash); } - $mail = Minz_Request::param('mail_login', false); - $this->view->conf->_mail_login($mail); + $email = Minz_Request::param('mail_login', false); + $this->view->conf->_mail_login($email); $ok &= $this->view->conf->save(); $email = $this->view->conf->mail_login; Minz_Session::_param('mail', $email); + Minz_Session::_param('passwordHash', $this->view->conf->passwordHash); if ($email != '') { $personaFile = DATA_PATH . '/persona/' . $email . '.txt'; @@ -89,10 +91,25 @@ class FreshRSS_users_Controller extends Minz_ActionController { $ok &= !file_exists($configPath); } if ($ok) { + + $passwordPlain = Minz_Request::param('new_user_passwordPlain', false); + $passwordHash = ''; + if ($passwordPlain != '') { + Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP + $_POST['new_user_passwordPlain'] = ''; + if (!function_exists('password_hash')) { + include_once(LIB_PATH . '/password_compat.php'); + } + $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => 8)); + $passwordPlain = ''; + } + if (empty($passwordHash)) { + $passwordHash = ''; + } + $new_user_email = filter_var($_POST['new_user_email'], FILTER_VALIDATE_EMAIL); if (empty($new_user_email)) { $new_user_email = ''; - $ok &= Minz_Configuration::authType() !== 'persona'; } else { $personaFile = DATA_PATH . '/persona/' . $new_user_email . '.txt'; @unlink($personaFile); @@ -102,6 +119,7 @@ class FreshRSS_users_Controller extends Minz_ActionController { if ($ok) { $config_array = array( 'language' => $new_user_language, + 'passwordHash' => $passwordHash, 'mail_login' => $new_user_email, ); $ok &= (file_put_contents($configPath, "accessControl(Minz_Session::param('currentUser', '')); + $loginOk = $this->accessControl(Minz_Session::param('currentUser', '')); $this->loadParamsView(); - $this->loadStylesAndScripts(); //TODO: Do not load that when not needed, e.g. some Ajax requests + $this->loadStylesAndScripts($loginOk); //TODO: Do not load that when not needed, e.g. some Ajax requests $this->loadNotifications(); } private function accessControl($currentUser) { if ($currentUser == '') { switch (Minz_Configuration::authType()) { + case 'form': + $currentUser = Minz_Configuration::defaultUser(); + Minz_Session::_param('passwordHash'); + $loginOk = false; + break; case 'http_auth': $currentUser = httpAuthUser(); $loginOk = $currentUser != ''; @@ -73,6 +78,9 @@ class FreshRSS extends Minz_FrontController { if ($loginOk) { switch (Minz_Configuration::authType()) { + case 'form': + $loginOk = Minz_Session::param('passwordHash') === $this->conf->passwordHash; + break; case 'http_auth': $loginOk = strcasecmp($currentUser, httpAuthUser()) === 0; break; @@ -92,6 +100,7 @@ class FreshRSS extends Minz_FrontController { } } Minz_View::_param ('loginOk', $loginOk); + return $loginOk; } private function loadParamsView () { @@ -104,7 +113,7 @@ class FreshRSS extends Minz_FrontController { } } - private function loadStylesAndScripts () { + private function loadStylesAndScripts ($loginOk) { $theme = FreshRSS_Themes::get_infos($this->conf->theme); if ($theme) { foreach($theme['files'] as $file) { @@ -112,14 +121,22 @@ class FreshRSS extends Minz_FrontController { } } - if (Minz_Configuration::authType() === 'persona') { - Minz_View::appendScript ('https://login.persona.org/include.js'); + switch (Minz_Configuration::authType()) { + case 'form': + if (!$loginOk) { + Minz_View::appendScript(Minz_Url::display ('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js'))); + } + break; + case 'persona': + Minz_View::appendScript('https://login.persona.org/include.js'); + break; } $includeLazyLoad = $this->conf->lazyload && ($this->conf->display_posts || Minz_Request::param ('output') === 'reader'); Minz_View::appendScript (Minz_Url::display ('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')), false, !$includeLazyLoad, !$includeLazyLoad); if ($includeLazyLoad) { Minz_View::appendScript (Minz_Url::display ('/scripts/jquery.lazyload.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.lazyload.min.js'))); } + Minz_View::appendScript (Minz_Url::display ('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js'))); Minz_View::appendScript (Minz_Url::display ('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js'))); } diff --git a/app/i18n/en.php b/app/i18n/en.php index 3b9936e8e..71ca9538f 100644 --- a/app/i18n/en.php +++ b/app/i18n/en.php @@ -170,6 +170,9 @@ return array ( 'is_admin' => 'is administrator', 'auth_type' => 'Authentication method', 'auth_none' => 'None (dangerous)', + 'auth_form' => 'Web form (traditional, requires JavaScript)', + 'http_auth' => 'HTTP (for advanced users with HTTPS)', + 'auth_persona' => 'Mozilla Persona (modern, requires JavaScript)', 'users_list' => 'List of users', 'create_user' => 'Create new user', 'username' => 'Username', @@ -258,8 +261,8 @@ return array ( 'logs_empty' => 'Log file is empty', 'clear_logs' => 'Clear the logs', - 'forbidden_access' => 'Forbidden access', - 'forbidden_access_description' => 'Access is password protected, please to read your feeds.', + 'forbidden_access' => 'Access forbidden! (%s)', + 'login_required' => 'Login required:', 'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!', diff --git a/app/i18n/fr.php b/app/i18n/fr.php index 7e71cbb6d..8ffc5ec88 100644 --- a/app/i18n/fr.php +++ b/app/i18n/fr.php @@ -170,6 +170,9 @@ return array ( 'is_admin' => 'est administrateur', 'auth_type' => 'Méthode d’authentification', 'auth_none' => 'Aucune (dangereux)', + 'auth_form' => 'Formulaire (traditionnel, requiert JavaScript)', + 'http_auth' => 'HTTP (pour utilisateurs avancés avec HTTPS)', + 'auth_persona' => 'Mozilla Persona (moderne, requiert JavaScript)', 'users_list' => 'Liste des utilisateurs', 'create_user' => 'Créer un nouvel utilisateur', 'username' => 'Nom d’utilisateur', @@ -258,8 +261,8 @@ return array ( 'logs_empty' => 'Les logs sont vides', 'clear_logs' => 'Effacer les logs', - 'forbidden_access' => 'Accès interdit', - 'forbidden_access_description' => 'L’accès est protégé par un mot de passe, veuillez pour accéder aux flux.', + 'forbidden_access' => 'Accès interdit ! (%s)', + 'login_required' => 'Accès protégé par mot de passe :', 'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !', diff --git a/app/layout/header.phtml b/app/layout/header.phtml index 0f2c524c4..e90da6196 100644 --- a/app/layout/header.phtml +++ b/app/layout/header.phtml @@ -1,12 +1,25 @@ - - - +
@@ -62,16 +75,24 @@
  • - -
  • -
  • - +
  • - +
    - + diff --git a/app/views/configure/users.phtml b/app/views/configure/users.phtml index 68111bdbe..1597004e1 100644 --- a/app/views/configure/users.phtml +++ b/app/views/configure/users.phtml @@ -20,7 +20,7 @@
    - +
    @@ -52,11 +52,11 @@
    - $_SERVER['REMOTE_USER'] = ``
    @@ -141,6 +141,14 @@ +
    + +
    + + +
    +
    +
    conf->mail_login; ?> diff --git a/app/views/helpers/javascript_vars.phtml b/app/views/helpers/javascript_vars.phtml index 92c068f7e..935294e60 100644 --- a/app/views/helpers/javascript_vars.phtml +++ b/app/views/helpers/javascript_vars.phtml @@ -30,8 +30,8 @@ if ($mail != 'null') { $mail = '"' . $mail . '"'; } - echo 'use_persona=', Minz_Configuration::authType() === 'persona' ? 'true' : 'false', - ',url_freshrss="', _url ('index', 'index'), '",', + echo 'authType="', Minz_Configuration::authType(), '",', + 'url_freshrss="', _url ('index', 'index'), '",', 'url_login="', _url ('index', 'login'), '",', 'url_logout="', _url ('index', 'logout'), '",', 'current_user_mail=', $mail, ",\n"; diff --git a/app/views/helpers/view/login.phtml b/app/views/helpers/view/login.phtml new file mode 100644 index 000000000..e4a24f9ed --- /dev/null +++ b/app/views/helpers/view/login.phtml @@ -0,0 +1,43 @@ +
    + +

    +
    + +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    + +
    +
    +

    FreshRSS

    +

    + +

    +
    diff --git a/app/views/index/index.phtml b/app/views/index/index.phtml index 549d0b61e..9b69233e9 100644 --- a/app/views/index/index.phtml +++ b/app/views/index/index.phtml @@ -1,15 +1,5 @@
    -

    -

    -

    -
    loginOk || Minz_Configuration::allowAnonymous()) { @@ -31,8 +21,8 @@ if ($this->loginOk || Minz_Configuration::allowAnonymous()) { if ($token_is_ok) { $this->renderHelper ('view/rss_view'); } else { - showForbidden(); + $this->renderHelper ('view/login'); } } else { - showForbidden(); + $this->renderHelper ('view/login'); } diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 2c30661ed..433992e0d 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -109,7 +109,7 @@ class Minz_Configuration { return self::$auth_type !== 'none'; } public static function canLogIn() { - return self::$auth_type === 'persona'; + return self::$auth_type === 'form' || self::$auth_type === 'persona'; } public static function _allowAnonymous($allow = false) { @@ -118,6 +118,7 @@ class Minz_Configuration { public static function _authType($value) { $value = strtolower($value); switch ($value) { + case 'form': case 'http_auth': case 'persona': case 'none': diff --git a/lib/Minz/FrontController.php b/lib/Minz/FrontController.php index 7b8526bc8..80eda8877 100644 --- a/lib/Minz/FrontController.php +++ b/lib/Minz/FrontController.php @@ -34,7 +34,7 @@ class Minz_FrontController { */ public function __construct () { if (LOG_PATH === false) { - $this->killApp ('Path doesn’t exist : LOG_PATH'); + $this->killApp ('Path not found: LOG_PATH'); } try { diff --git a/p/scripts/main.js b/p/scripts/main.js index 24af1b210..0c4c3f1b2 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -587,6 +587,54 @@ function init_load_more(box) { } // +// +function poormanSalt() { //If crypto.getRandomValues is not available + var text = '$2a$04$', + base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789/abcdefghijklmnopqrstuvwxyz'; + for (var i = 22; i > 0; i--) { + text += base.charAt(Math.floor(Math.random() * 64)); + } + return text; +} + +function init_loginForm() { + var $loginForm = $('#loginForm'); + if ($loginForm.length === 0) { + return; + } + if (!(window.dcodeIO)) { + if (window.console) { + console.log('FreshRSS waiting for bcrypt.js…'); + } + window.setTimeout(init_loginForm, 100); + return; + } + $loginForm.on('submit', function() { + $('#loginButton').attr('disabled', ''); + var success = false; + $.ajax({ + url: './?c=javascript&a=nonce&user=' + $('#username').val(), + dataType: 'json', + async: false + }).done(function (data) { + if (data.salt1 == '' || data.nonce == '') { + alert('Invalid user!'); + } else { + var strong = window.Uint32Array && window.crypto && (typeof window.crypto.getRandomValues === 'function'), + s = dcodeIO.bcrypt.hashSync($('#passwordPlain').val(), data.salt1), + c = dcodeIO.bcrypt.hashSync(data.nonce + s, strong ? 4 : poormanSalt()); + $('#challenge').val(c); + success = true; + } + }).fail(function() { + alert('Communication error!'); + }); + $('#loginButton').removeAttr('disabled'); + return success; + }); +} +// + // function init_persona() { if (!(navigator.id)) { @@ -696,8 +744,13 @@ function init_all() { init_notifications(); init_actualize(); init_load_more($stream); - if (use_persona) { - init_persona(); + switch (authType) { + case 'form': + init_loginForm(); + break; + case 'persona': + init_persona(); + break; } init_confirm_action(); init_print_action(); -- cgit v1.2.3 From 41033768c3eacbd564c3ec15455587e4f725a055 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 18 Jan 2014 01:00:17 +0100 Subject: Mode anonyme pour connexion avec formulaire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contribue à https://github.com/marienfressinaud/FreshRSS/issues/361 --- app/Controllers/usersController.php | 2 +- app/views/configure/users.phtml | 18 ++++-------------- lib/Minz/Configuration.php | 9 +++++---- 3 files changed, 10 insertions(+), 19 deletions(-) (limited to 'lib') diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php index 7e44b3d35..a044cd25b 100644 --- a/app/Controllers/usersController.php +++ b/app/Controllers/usersController.php @@ -57,8 +57,8 @@ class FreshRSS_users_Controller extends Minz_ActionController { $auth_type = Minz_Request::param('auth_type', 'none'); if ($anon != Minz_Configuration::allowAnonymous() || $auth_type != Minz_Configuration::authType()) { - Minz_Configuration::_allowAnonymous($anon); Minz_Configuration::_authType($auth_type); + Minz_Configuration::_allowAnonymous($anon); $ok &= Minz_Configuration::writeFile(); } } diff --git a/app/views/configure/users.phtml b/app/views/configure/users.phtml index 602dfaf62..3f352f9bf 100644 --- a/app/views/configure/users.phtml +++ b/app/views/configure/users.phtml @@ -58,20 +58,11 @@
    -
    -
    - - -
    -
    - - - - Mozilla Persona
    @@ -81,7 +72,8 @@ conf->token; ?>
    - + />
    @@ -92,8 +84,6 @@ - -
    diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 433992e0d..72e2cedc0 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -113,7 +113,7 @@ class Minz_Configuration { } public static function _allowAnonymous($allow = false) { - self::$allow_anonymous = (bool)$allow; + self::$allow_anonymous = ((bool)$allow) && self::canLogIn(); } public static function _authType($value) { $value = strtolower($value); @@ -125,6 +125,7 @@ class Minz_Configuration { self::$auth_type = $value; break; } + self::_allowAnonymous(self::$allow_anonymous); } /** @@ -252,12 +253,12 @@ class Minz_Configuration { if (isset ($general['default_user'])) { self::$default_user = $general['default_user']; } - if (isset ($general['allow_anonymous'])) { - self::$allow_anonymous = ((bool)($general['allow_anonymous'])) && ($general['allow_anonymous'] !== 'no'); - } if (isset ($general['auth_type'])) { self::_authType($general['auth_type']); } + if (isset ($general['allow_anonymous'])) { + self::$allow_anonymous = ((bool)($general['allow_anonymous'])) && ($general['allow_anonymous'] !== 'no'); + } // Base de données if (isset ($ini_array['db'])) { -- cgit v1.2.3 From fdd179d344dbe8734480b83bb7a0fba189275d5a Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 18 Jan 2014 19:29:44 +0100 Subject: Corrections vue globale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contribue à https://github.com/marienfressinaud/FreshRSS/issues/353 --- app/Controllers/entryController.php | 2 +- app/Controllers/indexController.php | 6 ++---- app/views/helpers/view/global_view.phtml | 16 ++++++++++------ lib/Minz/View.php | 6 +++--- p/scripts/global_view.js | 10 ++++++++++ p/scripts/main.js | 8 +++++--- 6 files changed, 31 insertions(+), 17 deletions(-) (limited to 'lib') diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index ded7598d9..1756c91e5 100755 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -11,7 +11,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { $this->params = array (); $output = Minz_Request::param('output', ''); - if (($output != '') && ($this->conf->view_mode !== $output)) { + if (($output != '') && ($this->view->conf->view_mode !== $output)) { $this->params['output'] = $output; } diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index d05a106bb..45ded6fd4 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -46,10 +46,8 @@ class FreshRSS_index_Controller extends Minz_ActionController { // no layout for RSS output $this->view->_useLayout (false); header('Content-Type: application/rss+xml; charset=utf-8'); - } else { - if ($output === 'global') { - Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js'))); - } + } elseif ($output === 'global') { + Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js'))); } $this->view->cat_aside = $this->catDAO->listCategories (); diff --git a/app/views/helpers/view/global_view.phtml b/app/views/helpers/view/global_view.phtml index ca7b66e0c..4fb807649 100644 --- a/app/views/helpers/view/global_view.phtml +++ b/app/views/helpers/view/global_view.phtml @@ -2,18 +2,22 @@
    'index', 'a' => 'index', 'params' => array()); + if ($this->conf->view_mode !== 'normal') { + $arUrl['params']['output'] = 'normal'; } + $p = Minz_Request::param('state', ''); + if (($p != '') && ($this->conf->default_view !== $p)) { + $arUrl['params']['state'] = $p; + } + foreach ($this->cat_aside as $cat) { $feeds = $cat->feeds (); if (!empty ($feeds)) { ?>
    @@ -22,7 +26,7 @@ nbNotRead (); ?>
  • ✇ - + name(); ?>
  • diff --git a/lib/Minz/View.php b/lib/Minz/View.php index ba9555cd7..e170bd406 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -63,7 +63,7 @@ class Minz_View { * Affiche la Vue en elle-même */ public function render () { - if ((@include($this->view_filename)) === false) { + if ((include($this->view_filename)) === false) { Minz_Log::record ('File not found: `' . $this->view_filename . '`', Minz_Log::NOTICE); @@ -79,7 +79,7 @@ class Minz_View { . self::LAYOUT_PATH_NAME . '/' . $part . '.phtml'; - if ((@include($fic_partial)) === false) { + if ((include($fic_partial)) === false) { Minz_Log::record ('File not found: `' . $fic_partial . '`', Minz_Log::WARNING); @@ -95,7 +95,7 @@ class Minz_View { . '/views/helpers/' . $helper . '.phtml'; - if ((@include($fic_helper)) === false) {; + if ((include($fic_helper)) === false) {; Minz_Log::record ('File not found: `' . $fic_helper . '`', Minz_Log::WARNING); diff --git a/p/scripts/global_view.js b/p/scripts/global_view.js index c34fbf1a7..3973cb2ec 100644 --- a/p/scripts/global_view.js +++ b/p/scripts/global_view.js @@ -24,6 +24,16 @@ function load_panel(link) { // en en ouvrant une autre ensuite, on se retrouve au même point de scroll $("#panel").scrollTop(0); + $('#nav_menu_read_all a, #bigMarkAsRead').click(function () { + $.ajax({ + url: $(this).attr("href"), + async: false + }); + //$("#panel .close").first().click(); + window.location.reload(false); + return false; + }); + panel_loading = false; }); } diff --git a/p/scripts/main.js b/p/scripts/main.js index 5bdc316db..3f6352baf 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -137,7 +137,9 @@ function mark_favorite(active) { if (active.closest('div').hasClass('not_read')) { var elem = $('#aside_flux .favorites').children(':first').get(0), feed_unreads = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0; - elem.setAttribute('data-unread', Math.max(0, feed_unreads + inc)); + if (elem) { + elem.setAttribute('data-unread', Math.max(0, feed_unreads + inc)); + } } }); } @@ -559,6 +561,8 @@ function load_more_posts() { } function init_load_more(box) { + box_load_more = box; + var $next_link = $("#load_more"); if (!$next_link.length) { // no more article to load @@ -566,8 +570,6 @@ function init_load_more(box) { return; } - box_load_more = box; - url_load_more = $next_link.attr("href"); var $prefetch = $('#prefetch'); if ($prefetch.attr('href') !== url_load_more) { -- cgit v1.2.3 From 6f117abfb6010150f95d2d0830a9715cd6f270fc Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 18 Jan 2014 22:54:53 +0100 Subject: Bugs chargement des articles complets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrige https://github.com/marienfressinaud/FreshRSS/issues/365 Ajoute le nettoyage correct du HTML par SimplePie, ainsi que le décodage des caractères HTML dans l'URL, et enfin un message dans le syslog pour chaque article téléchargé --- app/Models/Entry.php | 2 +- lib/lib_rss.php | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) (limited to 'lib') diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 83f68ce78..a6c67221b 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -159,7 +159,7 @@ class FreshRSS_Entry extends Minz_Model { try { // l'article n'est pas en BDD, on va le chercher sur le site $this->content = get_content_by_parsing( - $this->link(), $pathEntries + htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries ); } catch (Exception $e) { // rien à faire, on garde l'ancien contenu (requête a échoué) diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 4f98ed14a..33d7ebc32 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -110,6 +110,7 @@ function sanitizeHTML($data) { static $simplePie = null; if ($simplePie == null) { $simplePie = new SimplePie(); + $simplePie->init(); } return html_only_entity_decode($simplePie->sanitize->sanitize($data, SIMPLEPIE_CONSTRUCT_MAYBE_HTML)); } @@ -118,22 +119,13 @@ function sanitizeHTML($data) { function get_content_by_parsing ($url, $path) { require_once (LIB_PATH . '/lib_phpQuery.php'); + syslog(LOG_INFO, 'FreshRSS GET ' . $url); $html = file_get_contents ($url); if ($html) { $doc = phpQuery::newDocument ($html); $content = $doc->find ($path); - $content->find ('*')->removeAttr ('style') - ->removeAttr ('id') - ->removeAttr ('class') - ->removeAttr ('onload') - ->removeAttr ('target'); - $content->removeAttr ('style') - ->removeAttr ('id') - ->removeAttr ('class') - ->removeAttr ('onload') - ->removeAttr ('target'); - return $content->__toString (); + return sanitizeHTML($content->__toString()); } else { throw new Exception (); } -- cgit v1.2.3