diff --git a/common.php b/common.php
index daf62d6..509064a 100644
--- a/common.php
+++ b/common.php
@@ -23,12 +23,14 @@ require __DIR__.'/models/item.php';
require __DIR__.'/models/schema.php';
require __DIR__.'/models/auto_update.php';
require __DIR__.'/models/database.php';
+require __DIR__.'/models/remember_me.php';
if (file_exists('config.php')) require 'config.php';
defined('APP_VERSION') or define('APP_VERSION', 'master');
defined('HTTP_TIMEOUT') or define('HTTP_TIMEOUT', 20);
+defined('BASE_URL_DIRECTORY') or define('BASE_URL_DIRECTORY', dirname($_SERVER['PHP_SELF']));
defined('ROOT_DIRECTORY') or define('ROOT_DIRECTORY', __DIR__);
defined('DATA_DIRECTORY') or define('DATA_DIRECTORY', 'data');
diff --git a/controllers/common.php b/controllers/common.php
index 3752eac..ef2adfc 100644
--- a/controllers/common.php
+++ b/controllers/common.php
@@ -9,7 +9,7 @@ use PicoFarad\Template;
// Called before each action
Router\before(function($action) {
- Session\open(dirname($_SERVER['PHP_SELF']), SESSION_SAVE_PATH);
+ Session\open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH);
// Select another database
if (! empty($_SESSION['database'])) {
@@ -20,7 +20,13 @@ Router\before(function($action) {
$ignore_actions = array('login', 'google-auth', 'google-redirect-auth', 'mozilla-auth', 'bookmark-feed', 'select-db');
if (! isset($_SESSION['user']) && ! in_array($action, $ignore_actions)) {
- Response\redirect('?action=login');
+
+ if (! Model\RememberMe\authenticate()) {
+ Response\redirect('?action=login');
+ }
+ }
+ else if (Model\RememberMe\has_cookie()) {
+ Model\RememberMe\refresh();
}
// Load translations
diff --git a/controllers/user.php b/controllers/user.php
index 14154af..03eb646 100644
--- a/controllers/user.php
+++ b/controllers/user.php
@@ -11,6 +11,7 @@ use PicoFarad\Template;
// Logout and destroy session
Router\get_action('logout', function() {
+ Model\RememberMe\destroy();
Session\close();
Response\redirect('?action=login');
});
@@ -18,7 +19,9 @@ Router\get_action('logout', function() {
// Display form login
Router\get_action('login', function() {
- if (isset($_SESSION['user'])) Response\redirect('?action=unread');
+ if (isset($_SESSION['user'])) {
+ Response\redirect('?action=unread');
+ }
Response\html(Template\load('login', array(
'google_auth_enable' => Model\Config\get('auth_google_token') !== '',
@@ -36,7 +39,9 @@ Router\post_action('login', function() {
$values = Request\values();
list($valid, $errors) = Model\User\validate_login($values);
- if ($valid) Response\redirect('?action=unread');
+ if ($valid) {
+ Response\redirect('?action=unread');
+ }
Response\html(Template\load('login', array(
'google_auth_enable' => Model\Config\get('auth_google_token') !== '',
diff --git a/locales/fr_FR/translations.php b/locales/fr_FR/translations.php
index aec30d7..35df327 100644
--- a/locales/fr_FR/translations.php
+++ b/locales/fr_FR/translations.php
@@ -223,4 +223,6 @@ return array(
'Unable to create the new database.' => 'Impossible créer la nouvelle base de données.',
'Add a new database (new user)' => 'Ajouter une nouvelle base de données (nouvel utilisateur)',
'Create' => 'Créer',
+ 'Unknown' => 'Inconnu',
+ 'Remember Me' => 'Connexion automatique',
);
diff --git a/models/config.php b/models/config.php
index 3b1ec67..0ecd111 100644
--- a/models/config.php
+++ b/models/config.php
@@ -8,7 +8,7 @@ use PicoDb\Database;
use PicoFeed\Config as ReaderConfig;
use PicoFeed\Logging;
-const DB_VERSION = 24;
+const DB_VERSION = 25;
const HTTP_USER_AGENT = 'Miniflux (http://miniflux.net)';
// Get PicoFeed config
@@ -297,3 +297,48 @@ function save(array $values)
return Database::get('db')->table('config')->update($values);
}
+
+// Get the user agent of the connected user
+function get_user_agent()
+{
+ return empty($_SERVER['HTTP_USER_AGENT']) ? t('Unknown') : $_SERVER['HTTP_USER_AGENT'];
+}
+
+// Get the real IP address of the connected user
+function get_ip_address($only_public = false)
+{
+ $keys = array(
+ 'HTTP_CLIENT_IP',
+ 'HTTP_X_FORWARDED_FOR',
+ 'HTTP_X_FORWARDED',
+ 'HTTP_X_CLUSTER_CLIENT_IP',
+ 'HTTP_FORWARDED_FOR',
+ 'HTTP_FORWARDED',
+ 'REMOTE_ADDR'
+ );
+
+ foreach ($keys as $key) {
+
+ if (isset($_SERVER[$key])) {
+
+ foreach (explode(',', $_SERVER[$key]) as $ip_address) {
+
+ $ip_address = trim($ip_address);
+
+ if ($only_public) {
+
+ // Return only public IP address
+ if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
+ return $ip_address;
+ }
+ }
+ else {
+
+ return $ip_address;
+ }
+ }
+ }
+ }
+
+ return t('Unknown');
+}
diff --git a/models/remember_me.php b/models/remember_me.php
new file mode 100644
index 0000000..dbbc01b
--- /dev/null
+++ b/models/remember_me.php
@@ -0,0 +1,307 @@
+table(TABLE)
+ ->eq('token', $token)
+ ->eq('sequence', $sequence)
+ ->gt('expiration', time())
+ ->findOne();
+}
+
+/**
+ * Get all sessions
+ *
+ * @access public
+ * @return array
+ */
+function get_all()
+{
+ return Database::get('db')
+ ->table(TABLE)
+ ->desc('date_creation')
+ ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
+ ->findAll();
+}
+
+/**
+ * Authenticate the user with the cookie
+ *
+ * @access public
+ * @return bool
+ */
+function authenticate()
+{
+ $credentials = read_cookie();
+
+ if ($credentials !== false) {
+
+ $record = find($credentials['token'], $credentials['sequence']);
+
+ if ($record) {
+
+ // Update the sequence
+ write_cookie(
+ $record['token'],
+ update($record['token'], $record['sequence']),
+ $record['expiration']
+ );
+
+ // Create the session
+ $_SESSION['user'] = User\get($record['username']);
+ $_SESSION['config'] = Config\get_all();
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Update the database and the cookie with a new sequence
+ *
+ * @access public
+ */
+function refresh()
+{
+ $credentials = read_cookie();
+
+ if ($credentials !== false) {
+
+ $record = find($credentials['token'], $credentials['sequence']);
+
+ if ($record) {
+
+ // Update the sequence
+ write_cookie(
+ $record['token'],
+ update($record['token'], $record['sequence']),
+ $record['expiration']
+ );
+ }
+ }
+}
+
+/**
+ * Remove a session record
+ *
+ * @access public
+ * @param integer $session_id Session id
+ * @return mixed
+ */
+function remove($session_id)
+{
+ return Database::get('db')
+ ->table(TABLE)
+ ->eq('id', $session_id)
+ ->remove();
+}
+
+/**
+ * Remove the current RememberMe session and the cookie
+ *
+ * @access public
+ * @param integer $user_id User id
+ */
+function destroy()
+{
+ $credentials = read_cookie();
+
+ if ($credentials !== false) {
+
+ delete_cookie();
+
+ Database::get('db')
+ ->table(TABLE)
+ ->eq('token', $credentials['token'])
+ ->remove();
+ }
+}
+
+/**
+ * Create a new RememberMe session
+ *
+ * @access public
+ * @param integer $dbname Database name
+ * @param integer $username Username
+ * @param string $ip IP Address
+ * @param string $user_agent User Agent
+ * @return array
+ */
+function create($dbname, $username, $ip, $user_agent)
+{
+ $token = hash('sha256', $dbname.$username.$user_agent.$ip.Config\generate_token());
+ $sequence = Config\generate_token();
+ $expiration = time() + EXPIRATION;
+
+ cleanup();
+
+ Database::get('db')
+ ->table(TABLE)
+ ->insert(array(
+ 'username' => $username,
+ 'ip' => $ip,
+ 'user_agent' => $user_agent,
+ 'token' => $token,
+ 'sequence' => $sequence,
+ 'expiration' => $expiration,
+ 'date_creation' => time(),
+ ));
+
+ return array(
+ 'token' => $token,
+ 'sequence' => $sequence,
+ 'expiration' => $expiration,
+ );
+}
+
+/**
+ * Remove old sessions
+ *
+ * @access public
+ * @return bool
+ */
+function cleanup()
+{
+ return Database::get('db')
+ ->table(TABLE)
+ ->lt('expiration', time())
+ ->remove();
+}
+
+/**
+ * Return a new sequence token and update the database
+ *
+ * @access public
+ * @param string $token Session token
+ * @param string $sequence Sequence token
+ * @return string
+ */
+function update($token, $sequence)
+{
+ $new_sequence = Config\generate_token();
+
+ Database::get('db')
+ ->table(TABLE)
+ ->eq('token', $token)
+ ->eq('sequence', $sequence)
+ ->update(array('sequence' => $new_sequence));
+
+ return $new_sequence;
+}
+
+/**
+ * Encode the cookie
+ *
+ * @access public
+ * @param string $token Session token
+ * @param string $sequence Sequence token
+ * @return string
+ */
+function encode_cookie($token, $sequence)
+{
+ return implode('|', array(base64_encode(DatabaseModel\select()), $token, $sequence));
+}
+
+/**
+ * Decode the value of a cookie
+ *
+ * @access public
+ * @param string $value Raw cookie data
+ * @return array
+ */
+function decode_cookie($value)
+{
+ @list($database, $token, $sequence) = explode('|', $value);
+
+ DatabaseModel\select(base64_decode($database));
+
+ return array(
+ 'token' => $token,
+ 'sequence' => $sequence,
+ );
+}
+
+/**
+ * Return true if the current user has a RememberMe cookie
+ *
+ * @access public
+ * @return bool
+ */
+function has_cookie()
+{
+ return ! empty($_COOKIE[COOKIE_NAME]);
+}
+
+/**
+ * Write and encode the cookie
+ *
+ * @access public
+ * @param string $token Session token
+ * @param string $sequence Sequence token
+ * @param string $expiration Cookie expiration
+ */
+function write_cookie($token, $sequence, $expiration)
+{
+ setcookie(
+ COOKIE_NAME,
+ encode_cookie($token, $sequence),
+ $expiration,
+ BASE_URL_DIRECTORY,
+ null,
+ ! empty($_SERVER['HTTPS']),
+ true
+ );
+}
+
+/**
+ * Read and decode the cookie
+ *
+ * @access public
+ * @return mixed
+ */
+function read_cookie()
+{
+ if (empty($_COOKIE[COOKIE_NAME])) {
+ return false;
+ }
+
+ return decode_cookie($_COOKIE[COOKIE_NAME]);
+}
+
+/**
+ * Remove the cookie
+ *
+ * @access public
+ */
+function delete_cookie()
+{
+ setcookie(
+ COOKIE_NAME,
+ '',
+ time() - 3600,
+ BASE_URL_DIRECTORY,
+ null,
+ ! empty($_SERVER['HTTPS']),
+ true
+ );
+}
diff --git a/models/schema.php b/models/schema.php
index 5d1caa2..0e6fdf6 100644
--- a/models/schema.php
+++ b/models/schema.php
@@ -2,6 +2,24 @@
namespace Schema;
+
+function version_25($pdo)
+{
+ $pdo->exec(
+ 'CREATE TABLE remember_me (
+ id INTEGER PRIMARY KEY,
+ username TEXT,
+ ip TEXT,
+ user_agent TEXT,
+ token TEXT,
+ sequence TEXT,
+ expiration INTEGER,
+ date_creation INTEGER
+ )'
+ );
+}
+
+
function version_24($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN auto_update_url TEXT DEFAULT 'https://github.com/fguillot/miniflux/archive/master.zip'");
diff --git a/models/user.php b/models/user.php
index cad2945..f47ce9a 100644
--- a/models/user.php
+++ b/models/user.php
@@ -5,6 +5,9 @@ namespace Model\User;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use PicoDb\Database;
+use Model\Config;
+use Model\RememberMe;
+use Model\Database as DatabaseModel;
// Get a user by username
function get($username)
@@ -37,7 +40,13 @@ function validate_login(array $values)
unset($user['password']);
$_SESSION['user'] = $user;
- $_SESSION['config'] = \Model\Config\get_all();
+ $_SESSION['config'] = Config\get_all();
+
+ // Setup the remember me feature
+ if (! empty($values['remember_me'])) {
+ $credentials = RememberMe\create(DatabaseModel\select(), $values['username'], Config\get_ip_address(), Config\get_user_agent());
+ RememberMe\write_cookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
+ }
}
else {
diff --git a/templates/login.php b/templates/login.php
index 9d67198..f768844 100644
--- a/templates/login.php
+++ b/templates/login.php
@@ -36,6 +36,8 @@
= Helper\form_label(t('Password'), 'password') ?>
= Helper\form_password('password', $values, $errors, array('required')) ?>
+ = Helper\form_checkbox('remember_me', t('Remember Me'), 1) ?>
+