Add support for multiple users/databases

This commit is contained in:
Frédéric Guillot 2014-04-05 20:24:13 -04:00
parent 555fd9279d
commit eab942537f
12 changed files with 269 additions and 15 deletions

View File

@ -25,6 +25,7 @@ Features
- Alternative login with a Google Account or Mozilla Persona
- **Full article download for feeds that display only a summary** (website scraper based on Xpath rules)
- Auto-update from the user interface
- Multiple databases (each user has his own database)
Todo and known bugs
-------------------
@ -156,6 +157,7 @@ You just need to be inside the directory `miniflux` and run the script `cronjob.
Parameters | Type | Value
--------------------|--------------------------------|-----------------------------
--database | optional | Database filename, default is db.sqlite (ex: db2.sqlite)
--limit | optional | number of feeds
--call-interval | optional, excluded by --limit, require --update-interval | time in minutes < update interval time
--update-interval | optional, excluded by --limit, require --call-interval | time in minutes >= call interval time

View File

@ -643,6 +643,10 @@ iframe {
opacity: 0;
}
.login-form {
margin-bottom: 20px;
}
/* desktop design */
@media only screen and (min-width: 480px) {

View File

@ -11,25 +11,34 @@ require __DIR__.'/models/feed.php';
require __DIR__.'/models/item.php';
require __DIR__.'/models/schema.php';
require __DIR__.'/models/auto_update.php';
require __DIR__.'/models/database.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('DB_FILENAME') or define('DB_FILENAME', 'data/db.sqlite');
defined('ROOT_DIRECTORY') or define('ROOT_DIRECTORY', __DIR__);
defined('DATA_DIRECTORY') or define('DATA_DIRECTORY', 'data');
defined('ENABLE_MULTIPLE_DB') or define('ENABLE_MULTIPLE_DB', true);
defined('DB_FILENAME') or define('DB_FILENAME', 'db.sqlite');
defined('DEBUG') or define('DEBUG', true);
defined('DEBUG_FILENAME') or define('DEBUG_FILENAME', 'data/debug.log');
defined('DEBUG_FILENAME') or define('DEBUG_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'debug.log');
defined('THEME_DIRECTORY') or define('THEME_DIRECTORY', 'themes');
defined('SESSION_SAVE_PATH') or define('SESSION_SAVE_PATH', '');
defined('PROXY_HOSTNAME') or define('PROXY_HOSTNAME', '');
defined('PROXY_PORT') or define('PROXY_PORT', 3128);
defined('PROXY_USERNAME') or define('PROXY_USERNAME', '');
defined('PROXY_PASSWORD') or define('PROXY_PASSWORD', '');
defined('ROOT_DIRECTORY') or define('ROOT_DIRECTORY', __DIR__);
defined('ENABLE_AUTO_UPDATE') or define('ENABLE_AUTO_UPDATE', true);
defined('AUTO_UPDATE_DOWNLOAD_DIRECTORY') or define('AUTO_UPDATE_DOWNLOAD_DIRECTORY', 'data/download');
defined('AUTO_UPDATE_ARCHIVE_DIRECTORY') or define('AUTO_UPDATE_ARCHIVE_DIRECTORY', 'data/archive');
defined('AUTO_UPDATE_BACKUP_DIRECTORY') or define('AUTO_UPDATE_BACKUP_DIRECTORY', 'data/backup');
defined('AUTO_UPDATE_DOWNLOAD_DIRECTORY') or define('AUTO_UPDATE_DOWNLOAD_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'download');
defined('AUTO_UPDATE_ARCHIVE_DIRECTORY') or define('AUTO_UPDATE_ARCHIVE_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'archive');
defined('AUTO_UPDATE_BACKUP_DIRECTORY') or define('AUTO_UPDATE_BACKUP_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'backup');
PicoFeed\Client::proxy(PROXY_HOSTNAME, PROXY_PORT, PROXY_USERNAME, PROXY_PASSWORD);
@ -37,7 +46,7 @@ PicoDb\Database::bootstrap('db', function() {
$db = new PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => DB_FILENAME
'filename' => \Model\Database\get_path(),
));
if ($db->schema()->check(Model\Config\DB_VERSION)) {

View File

@ -11,7 +11,13 @@ Router\before(function($action) {
Session\open(dirname($_SERVER['PHP_SELF']), SESSION_SAVE_PATH);
$ignore_actions = array('login', 'google-auth', 'google-redirect-auth', 'mozilla-auth', 'bookmark-feed');
// Select another database
if (! empty($_SESSION['database'])) {
Model\Database\select($_SESSION['database']);
}
// Redirect to the login form if the user is not authenticated
$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');
@ -50,3 +56,13 @@ Router\get_action('more', function() {
Response\html(Template\layout('show_more', array('menu' => 'more')));
});
// Select another database
Router\get_action('select-db', function() {
if (ENABLE_MULTIPLE_DB) {
$_SESSION['database'] = \Model\Database\select(Request\param('database'));
}
Response\redirect('?action=login');
});

View File

@ -7,6 +7,47 @@ use PicoFarad\Session;
use PicoFarad\Template;
use PicoDb\Database;
// Display a form to add a new database
Router\get_action('new-db', function() {
if (ENABLE_MULTIPLE_DB) {
Response\html(Template\layout('new_db', array(
'errors' => array(),
'values' => array(),
'menu' => 'config',
'title' => t('New database')
)));
}
Response\redirect('?action=config');
});
// Create a new database
Router\post_action('new-db', function() {
$values = Request\values();
list($valid, $errors) = Model\Database\validate($values);
if ($valid) {
if (Model\Database\create(strtolower($values['name']).'.sqlite', $values['username'], $values['password'])) {
Session\flash(t('Database created successfully.'));
}
else {
Session\flash_error(t('Unable to create the new database.'));
}
Response\redirect('?action=config');
}
Response\html(Template\layout('new_db', array(
'errors' => $errors,
'values' => $values,
'menu' => 'config',
'title' => t('New database')
)));
});
// Auto-update
Router\get_action('auto-update', function() {
@ -41,7 +82,7 @@ Router\get_action('optimize-db', function() {
Router\get_action('download-db', function() {
Response\force_download('db.sqlite.gz');
Response\binary(gzencode(file_get_contents(DB_FILENAME)));
Response\binary(gzencode(file_get_contents(\Model\Database\get_path())));
});
// Display preferences page
@ -50,7 +91,7 @@ Router\get_action('config', function() {
Response\html(Template\layout('config', array(
'errors' => array(),
'values' => Model\Config\get_all(),
'db_size' => filesize(DB_FILENAME),
'db_size' => filesize(\Model\Database\get_path()),
'languages' => Model\Config\get_languages(),
'timezones' => Model\Config\get_timezones(),
'autoflush_options' => Model\Config\get_autoflush_options(),
@ -84,7 +125,7 @@ Router\post_action('config', function() {
Response\html(Template\layout('config', array(
'errors' => $errors,
'values' => Model\Config\get_all(),
'db_size' => filesize(DB_FILENAME),
'db_size' => filesize(\Model\Database\get_path()),
'languages' => Model\Config\get_languages(),
'timezones' => Model\Config\get_timezones(),
'autoflush_options' => Model\Config\get_autoflush_options(),

View File

@ -24,7 +24,9 @@ Router\get_action('login', function() {
'google_auth_enable' => Model\Config\get('auth_google_token') !== '',
'mozilla_auth_enable' => Model\Config\get('auth_mozilla_token') !== '',
'errors' => array(),
'values' => array()
'values' => array(),
'databases' => Model\Database\get_list(),
'current_database' => Model\Database\select()
)));
});
@ -40,7 +42,9 @@ Router\post_action('login', function() {
'google_auth_enable' => Model\Config\get('auth_google_token') !== '',
'mozilla_auth_enable' => Model\Config\get('auth_mozilla_token') !== '',
'errors' => $errors,
'values' => $values
'values' => $values,
'databases' => Model\Database\get_list(),
'current_database' => Model\Database\select()
)));
});

View File

@ -7,7 +7,8 @@ if (php_sapi_name() === 'cli') {
$options = getopt('', array(
'limit::',
'call-interval::',
'update-interval::'
'update-interval::',
'database::',
));
}
else {
@ -15,6 +16,10 @@ else {
$options = $_GET;
}
if (! empty($options['database'])) {
\Model\Database\select($options['database']);
}
$limit = ! empty($options['limit']) && ctype_digit($options['limit']) ? (int) $options['limit'] : Model\Feed\LIMIT_ALL;
$update_interval = ! empty($options['update-interval']) && ctype_digit($options['update-interval']) ? (int) $options['update-interval'] : null;
$call_interval = ! empty($options['call-interval']) && ctype_digit($options['call-interval']) ? (int) $options['call-interval'] : null;

View File

@ -213,4 +213,14 @@ return array(
'Miniflux is updated!' => 'Miniflux a été mis à jour avec succès !',
'Unable to update Miniflux, check the console for errors.' => 'Impossible de mettre à jour Miniflux, allez-voir les erreurs dans la console.',
'Don\'t forget to backup your database' => 'N\'oubliez pas de sauvegarder votre base de données',
'The name must have only alpha-numeric characters' => 'Le nom doit avoir seulement des caractères alphanumériques',
'New database' => 'Nouvelle base de données',
'Database name' => 'Nom de la base de données',
'Default database' => 'Base de données par défaut',
'Select another database' => 'Choisir une autre base de données',
'The database name is required' => 'Le nom de la base de données est obligatoire',
'Database created successfully.' => 'Base de données créée avec succès.',
'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',
);

115
models/database.php Normal file
View File

@ -0,0 +1,115 @@
<?php
namespace Model\Database;
require_once __DIR__.'/../vendor/SimpleValidator/Validator.php';
require_once __DIR__.'/../vendor/SimpleValidator/Base.php';
require_once __DIR__.'/../vendor/SimpleValidator/Validators/Required.php';
require_once __DIR__.'/../vendor/SimpleValidator/Validators/MaxLength.php';
require_once __DIR__.'/../vendor/SimpleValidator/Validators/MinLength.php';
require_once __DIR__.'/../vendor/SimpleValidator/Validators/Equals.php';
require_once __DIR__.'/../vendor/SimpleValidator/Validators/AlphaNumeric.php';
use SimpleValidator\Validator;
use SimpleValidator\Validators;
// Create a new database for a new user
function create($filename, $username, $password)
{
$filename = DATA_DIRECTORY.DIRECTORY_SEPARATOR.$filename;
if (! file_exists($filename)) {
$db = new \PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => $filename,
));
if ($db->schema()->check(\Model\Config\DB_VERSION)) {
$db->table('config')->update(array(
'username' => $username,
'password' => \password_hash($password, PASSWORD_BCRYPT)
));
return true;
}
}
return false;
}
// Get or set the current database
function select($filename = '')
{
static $current_filename = DB_FILENAME;
if ($filename !== '' && in_array($filename, get_all())) {
$current_filename = $filename;
$_SESSION['config'] = \Model\Config\get_all();
}
return $current_filename;
}
// Get database path
function get_path()
{
return DATA_DIRECTORY.DIRECTORY_SEPARATOR.\Model\Database\select();
}
// Get the list of available databases
function get_all()
{
$listing = array();
$dir = new \DirectoryIterator(DATA_DIRECTORY);
foreach ($dir as $fileinfo) {
if ($fileinfo->getExtension() === 'sqlite') {
$listing[] = $fileinfo->getFilename();
}
}
return $listing;
}
// Get the formated db list
function get_list()
{
$listing = array();
foreach (get_all() as $filename) {
if ($filename === DB_FILENAME) {
$label = t('Default database');
}
else {
$label = ucfirst(substr($filename, 0, -7));
}
$listing[$filename] = $label;
}
return $listing;
}
// Validate database form
function validate(array $values)
{
$v = new Validator($values, array(
new Validators\Required('name', t('The database name is required')),
new Validators\AlphaNumeric('name', t('The name must have only alpha-numeric characters')),
new Validators\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\Required('password', t('The password is required')),
new Validators\MinLength('password', t('The minimum length is 6 characters'), 6),
new Validators\Required('confirmation', t('The confirmation is required')),
new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match')),
));
return array(
$v->execute(),
$v->getErrors()
);
}

View File

@ -90,6 +90,11 @@
<li><?= t('Database size:') ?> <strong><?= Helper\format_bytes($db_size) ?></strong></li>
<li><a href="?action=optimize-db"><?= t('Optimize the database') ?></a> <?= t('(VACUUM command)') ?></li>
<li><a href="?action=download-db"><?= t('Download the entire database') ?></a> <?= t('(Gzip compressed Sqlite file)') ?></li>
<?php if (ENABLE_MULTIPLE_DB): ?>
<li>
<a href="?action=new-db"><?= t('Add a new database (new user)') ?></a></li>
</li>
<?php endif ?>
</ul>
</div>
<?= \PicoFarad\Template\load('keyboard_shortcuts') ?>

View File

@ -28,7 +28,7 @@
<p class="alert alert-error"><?= Helper\escape($errors['login']) ?></p>
<?php endif ?>
<form method="post" action="?action=login">
<form method="post" action="?action=login" class="login-form">
<?= Helper\form_label(t('Username'), 'username') ?>
<?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/>
@ -48,6 +48,24 @@
<input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
</div>
</form>
<?php if (ENABLE_MULTIPLE_DB && count($databases) > 1): ?>
<div class="alert alert-normal">
<h3><?= t('Select another database') ?></h3>
<ul>
<?php foreach ($databases as $filename => $dbname): ?>
<li>
<?php if ($current_database === $filename): ?>
<strong><?= Helper\escape($dbname) ?></strong>
<?php else: ?>
<a href="?action=select-db&amp;database=<?= Helper\escape($filename) ?>"><?= Helper\escape($dbname) ?></a>
<?php endif ?>
</li>
<?php endforeach ?>
</ul>
</div>
<?php endif ?>
</section>
</section>
</body>

25
templates/new_db.php Normal file
View File

@ -0,0 +1,25 @@
<div class="page-header">
<h2><?= t('New database') ?></h2>
<ul>
<li><a href="?action=config"><?= t('preferences') ?></a></li>
</ul>
</div>
<form method="post" action="?action=new-db" autocomplete="off">
<?= Helper\form_label(t('Database name'), 'name') ?>
<?= Helper\form_text('name', $values, $errors, array('required', 'autofocus')) ?>
<p class="form-help"><?= t('The name must have only alpha-numeric characters') ?></p>
<?= Helper\form_label(t('Username'), 'username') ?>
<?= Helper\form_text('username', $values, $errors, array('required')) ?><br/>
<?= Helper\form_label(t('Password'), 'password') ?>
<?= Helper\form_password('password', $values, $errors, array('required')) ?>
<?= Helper\form_label(t('Confirmation'), 'confirmation') ?>
<?= Helper\form_password('confirmation', $values, $errors, array('required')) ?><br/>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Create') ?></button>
</div>
</form>