2013-12-23 03:25:54 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Model\Config;
|
|
|
|
|
2015-01-18 00:53:40 +01:00
|
|
|
use Translator;
|
2014-10-30 02:28:23 +01:00
|
|
|
use DirectoryIterator;
|
2013-12-23 03:25:54 +01:00
|
|
|
use SimpleValidator\Validator;
|
|
|
|
use SimpleValidator\Validators;
|
2014-02-08 20:13:14 +01:00
|
|
|
use PicoDb\Database;
|
2014-12-24 03:28:26 +01:00
|
|
|
use PicoFeed\Config\Config as ReaderConfig;
|
|
|
|
use PicoFeed\Logging\Logger;
|
2014-05-20 20:20:27 +02:00
|
|
|
|
|
|
|
const HTTP_USER_AGENT = 'Miniflux (http://miniflux.net)';
|
|
|
|
|
|
|
|
// Get PicoFeed config
|
|
|
|
function get_reader_config()
|
|
|
|
{
|
|
|
|
$config = new ReaderConfig;
|
|
|
|
$config->setTimezone(get('timezone'));
|
2013-12-23 03:25:54 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
// Client
|
2014-05-20 20:20:27 +02:00
|
|
|
$config->setClientTimeout(HTTP_TIMEOUT);
|
|
|
|
$config->setClientUserAgent(HTTP_USER_AGENT);
|
|
|
|
$config->setGrabberUserAgent(HTTP_USER_AGENT);
|
2013-12-23 03:25:54 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
// Proxy
|
2014-05-20 20:20:27 +02:00
|
|
|
$config->setProxyHostname(PROXY_HOSTNAME);
|
|
|
|
$config->setProxyPort(PROXY_PORT);
|
|
|
|
$config->setProxyUsername(PROXY_USERNAME);
|
|
|
|
$config->setProxyPassword(PROXY_PASSWORD);
|
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
// Filter
|
2014-05-20 20:20:27 +02:00
|
|
|
$config->setFilterIframeWhitelist(get_iframe_whitelist());
|
|
|
|
|
2015-01-29 03:57:34 +01:00
|
|
|
if ((bool) get('debug_mode')) {
|
|
|
|
Logger::enable();
|
|
|
|
}
|
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
// Parser
|
|
|
|
$config->setParserHashAlgo('crc32b');
|
|
|
|
|
2014-05-20 20:20:27 +02:00
|
|
|
return $config;
|
|
|
|
}
|
|
|
|
|
|
|
|
function get_iframe_whitelist()
|
|
|
|
{
|
|
|
|
return array(
|
|
|
|
'http://www.youtube.com',
|
|
|
|
'https://www.youtube.com',
|
|
|
|
'http://player.vimeo.com',
|
|
|
|
'https://player.vimeo.com',
|
|
|
|
'http://www.dailymotion.com',
|
|
|
|
'https://www.dailymotion.com',
|
|
|
|
);
|
|
|
|
}
|
2014-03-30 21:59:26 +02:00
|
|
|
|
|
|
|
// Send a debug message to the console
|
|
|
|
function debug($line)
|
|
|
|
{
|
2014-12-24 03:28:26 +01:00
|
|
|
Logger::setMessage($line);
|
2014-03-30 21:59:26 +02:00
|
|
|
write_debug();
|
|
|
|
}
|
|
|
|
|
2013-12-23 03:25:54 +01:00
|
|
|
// Write PicoFeed debug output to a file
|
|
|
|
function write_debug()
|
|
|
|
{
|
2015-01-29 03:57:34 +01:00
|
|
|
if ((bool) get('debug_mode')) {
|
2014-12-24 03:28:26 +01:00
|
|
|
file_put_contents(DEBUG_FILENAME, implode(PHP_EOL, Logger::getMessages()));
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-26 01:03:46 +01:00
|
|
|
// Get available timezone
|
|
|
|
function get_timezones()
|
|
|
|
{
|
2014-10-30 02:28:23 +01:00
|
|
|
$timezones = timezone_identifiers_list();
|
2014-02-26 01:03:46 +01:00
|
|
|
return array_combine(array_values($timezones), $timezones);
|
|
|
|
}
|
|
|
|
|
2013-12-23 03:25:54 +01:00
|
|
|
// Get all supported languages
|
|
|
|
function get_languages()
|
|
|
|
{
|
2014-10-30 02:28:23 +01:00
|
|
|
return array(
|
|
|
|
'cs_CZ' => 'Čeština',
|
|
|
|
'de_DE' => 'Deutsch',
|
|
|
|
'en_US' => 'English',
|
|
|
|
'es_ES' => 'Español',
|
|
|
|
'fr_FR' => 'Français',
|
|
|
|
'it_IT' => 'Italiano',
|
|
|
|
'pt_BR' => 'Português',
|
|
|
|
'zh_CN' => '简体中国',
|
2013-12-23 03:25:54 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get all skins
|
|
|
|
function get_themes()
|
|
|
|
{
|
|
|
|
$themes = array(
|
2014-11-14 18:48:27 +01:00
|
|
|
'original' => t('Default')
|
2013-12-23 03:25:54 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
if (file_exists(THEME_DIRECTORY)) {
|
|
|
|
|
2014-10-30 02:28:23 +01:00
|
|
|
$dir = new DirectoryIterator(THEME_DIRECTORY);
|
2013-12-23 03:25:54 +01:00
|
|
|
|
|
|
|
foreach ($dir as $fileinfo) {
|
|
|
|
|
|
|
|
if (! $fileinfo->isDot() && $fileinfo->isDir()) {
|
|
|
|
$themes[$dir->getFilename()] = ucfirst($dir->getFilename());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $themes;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sorting direction choices for items
|
|
|
|
function get_sorting_directions()
|
|
|
|
{
|
|
|
|
return array(
|
|
|
|
'asc' => t('Older items first'),
|
|
|
|
'desc' => t('Most recent first'),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2014-05-29 16:57:23 +02:00
|
|
|
// Display summaries or full contents on lists
|
|
|
|
function get_display_mode()
|
|
|
|
{
|
|
|
|
return array(
|
|
|
|
'summaries' => t('Summaries'),
|
|
|
|
'full' => t('Full contents')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2014-12-16 02:38:35 +01:00
|
|
|
// Autoflush choices for read items
|
|
|
|
function get_autoflush_read_options()
|
2013-12-23 03:25:54 +01:00
|
|
|
{
|
|
|
|
return array(
|
|
|
|
'0' => t('Never'),
|
|
|
|
'-1' => t('Immediately'),
|
|
|
|
'1' => t('After %d day', 1),
|
|
|
|
'5' => t('After %d days', 5),
|
|
|
|
'15' => t('After %d days', 15),
|
|
|
|
'30' => t('After %d days', 30)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2014-12-16 02:38:35 +01:00
|
|
|
// Autoflush choices for unread items
|
|
|
|
function get_autoflush_unread_options()
|
|
|
|
{
|
|
|
|
return array(
|
|
|
|
'0' => t('Never'),
|
|
|
|
'15' => t('After %d days', 15),
|
|
|
|
'30' => t('After %d days', 30),
|
|
|
|
'45' => t('After %d days', 45),
|
|
|
|
'60' => t('After %d days', 60),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2013-12-23 03:25:54 +01:00
|
|
|
// Number of items per pages
|
|
|
|
function get_paging_options()
|
|
|
|
{
|
|
|
|
return array(
|
|
|
|
50 => 50,
|
|
|
|
100 => 100,
|
|
|
|
150 => 150,
|
|
|
|
200 => 200,
|
|
|
|
250 => 250,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2013-12-23 19:33:16 +01:00
|
|
|
// Get redirect options when there is nothing to read
|
|
|
|
function get_nothing_to_read_redirections()
|
|
|
|
{
|
|
|
|
return array(
|
2014-11-14 18:48:27 +01:00
|
|
|
'feeds' => t('Subscriptions'),
|
|
|
|
'history' => t('History'),
|
|
|
|
'bookmarks' => t('Bookmarks'),
|
2013-12-23 19:33:16 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2014-11-08 03:44:20 +01:00
|
|
|
// Create a CSRF token
|
|
|
|
function generate_csrf()
|
|
|
|
{
|
|
|
|
if (empty($_SESSION['csrf'])) {
|
|
|
|
$_SESSION['csrf'] = array();
|
|
|
|
}
|
|
|
|
|
|
|
|
$token = generate_token();
|
|
|
|
$_SESSION['csrf'][$token] = true;
|
|
|
|
|
|
|
|
return $token;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check CSRF token (form values)
|
|
|
|
function check_csrf_values(array &$values)
|
|
|
|
{
|
|
|
|
if (empty($values['csrf']) || ! isset($_SESSION['csrf'][$values['csrf']])) {
|
|
|
|
$values = array();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
|
|
|
|
unset($_SESSION['csrf'][$values['csrf']]);
|
|
|
|
unset($values['csrf']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check CSRF token
|
|
|
|
function check_csrf($token)
|
|
|
|
{
|
|
|
|
if (isset($_SESSION['csrf'][$token])) {
|
2014-11-08 23:14:18 +01:00
|
|
|
unset($_SESSION['csrf'][$token]);
|
2014-11-08 03:44:20 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2013-12-23 03:25:54 +01:00
|
|
|
// Generate a token from /dev/urandom or with uniqid() if open_basedir is enabled
|
|
|
|
function generate_token()
|
|
|
|
{
|
2014-03-15 02:26:14 +01:00
|
|
|
if (function_exists('openssl_random_pseudo_bytes')) {
|
2015-01-29 03:57:34 +01:00
|
|
|
return bin2hex(openssl_random_pseudo_bytes(25));
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
2014-03-15 02:26:14 +01:00
|
|
|
else if (ini_get('open_basedir') === '' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
|
|
|
|
return hash('sha256', file_get_contents('/dev/urandom', false, null, 0, 30));
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
2014-03-15 02:26:14 +01:00
|
|
|
|
|
|
|
return hash('sha256', uniqid(mt_rand(), true));
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Regenerate tokens for the API and bookmark feed
|
|
|
|
function new_tokens()
|
|
|
|
{
|
|
|
|
$values = array(
|
|
|
|
'api_token' => generate_token(),
|
|
|
|
'feed_token' => generate_token(),
|
2014-05-28 22:44:25 +02:00
|
|
|
'bookmarklet_token' => generate_token(),
|
2014-10-30 02:28:23 +01:00
|
|
|
'fever_token' => substr(generate_token(), 0, 8),
|
2013-12-23 03:25:54 +01:00
|
|
|
);
|
|
|
|
|
2015-01-28 05:26:36 +01:00
|
|
|
return Database::get('db')->hashtable('settings')->put($values);
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get a config value from the DB or from the session
|
|
|
|
function get($name)
|
|
|
|
{
|
|
|
|
if (! isset($_SESSION)) {
|
2015-01-28 05:26:36 +01:00
|
|
|
return current(Database::get('db')->hashtable('settings')->get($name));
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
|
2014-02-26 01:03:46 +01:00
|
|
|
if (! isset($_SESSION['config'][$name])) {
|
2013-12-23 03:25:54 +01:00
|
|
|
$_SESSION['config'] = get_all();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($_SESSION['config'][$name])) {
|
|
|
|
return $_SESSION['config'][$name];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get all config parameters
|
|
|
|
function get_all()
|
|
|
|
{
|
2015-01-28 05:26:36 +01:00
|
|
|
$config = Database::get('db')->hashtable('settings')->get();
|
2014-12-24 23:54:27 +01:00
|
|
|
|
|
|
|
unset($config['password']);
|
|
|
|
|
|
|
|
return $config;
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validation for edit action
|
|
|
|
function validate_modification(array $values)
|
|
|
|
{
|
2014-03-30 21:59:26 +02:00
|
|
|
$rules = array(
|
|
|
|
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('autoflush', t('Value required')),
|
2014-12-16 02:38:35 +01:00
|
|
|
new Validators\Required('autoflush_unread', t('Value required')),
|
2014-03-30 21:59:26 +02:00
|
|
|
new Validators\Required('items_per_page', t('Value required')),
|
|
|
|
new Validators\Integer('items_per_page', t('Must be an integer')),
|
|
|
|
new Validators\Required('theme', t('Value required')),
|
implement frontend autoupdate
Only the unread counter is updated right know.
The AutoUpdate Feature is designed on the premise of don't wasting resources. A
distinction is made between updates when Miniflux is visible or hidden.
To determine the visibility status, the Page Visibility API is used. The API is
available starting with Chrome 33, Firefox 18 and IE10. [https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API]
As IE9 returns an undefined, it doesn't break the compatibility at least.
If Miniflux is visible, the unread counter on the web page is updated as soon as
a mismatch between the counter and the number of unread articles in the database
is found.
If Miniflux is hidden, the timestamp of the most recent article from each feed
is compared with the value from the last run. We have an update If the timestamp
of the latest article is greater than the stored one and the latest article is
unread. The web page title is updated with a ? symbol to notify the user and the
update check pauses till Miniflux gets visible again. If Miniflux gets visible
again, the number of unread articles is queried from the database, the unread
counter on the web page is updated and finally the ? symbol is removed from the
web page title.
This way I can use my fever API client to read new articles (or at least the
latest article) while Miniflux is hidden and as I've seen the new articles
already a new articles notification is prevented.
It's intentionally that the page does not reload automatically as long as
articles are visible. If I'm in hurry, I only scroll through the articles to
spot something interesting. Most of the time I don't reach the last article.
If the page is reloaded while I'm away, I would have to scan from the top again.
If we're on a nothing_to_read page and have unread articles in the database, a
redirect to the unread page will be done.
The default update check interval is 10 minutes and can be changed on the
settings page. A zero value disables the update check entirely.
fixes #213
2014-11-27 22:36:04 +01:00
|
|
|
new Validators\Integer('frontend_updatecheck_interval', t('Must be an integer')),
|
2015-01-29 03:57:34 +01:00
|
|
|
new Validators\Integer('debug_mode', t('Must be an integer')),
|
2015-01-29 23:28:18 +01:00
|
|
|
new Validators\Integer('nocontent', t('Must be an integer')),
|
|
|
|
new Validators\Integer('favicons', t('Must be an integer')),
|
|
|
|
new Validators\Integer('original_marks_read', t('Must be an integer')),
|
2014-03-30 21:59:26 +02:00
|
|
|
);
|
2013-12-23 03:25:54 +01:00
|
|
|
|
2014-03-30 21:59:26 +02:00
|
|
|
if (ENABLE_AUTO_UPDATE) {
|
|
|
|
$rules[] = new Validators\Required('auto_update_url', t('Value required'));
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
|
|
|
|
2014-03-30 21:59:26 +02:00
|
|
|
if (! empty($values['password'])) {
|
|
|
|
$rules[] = new Validators\Required('password', t('The password is required'));
|
|
|
|
$rules[] = new Validators\MinLength('password', t('The minimum length is 6 characters'), 6);
|
|
|
|
$rules[] = new Validators\Required('confirmation', t('The confirmation is required'));
|
2014-05-28 22:10:41 +02:00
|
|
|
$rules[] = new Validators\Equals('password', 'confirmation', t('Passwords don\'t match'));
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
|
|
|
|
2014-03-30 21:59:26 +02:00
|
|
|
$v = new Validator($values, $rules);
|
|
|
|
|
2013-12-23 03:25:54 +01:00
|
|
|
return array(
|
|
|
|
$v->execute(),
|
|
|
|
$v->getErrors()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save config into the database and update the session
|
|
|
|
function save(array $values)
|
|
|
|
{
|
|
|
|
// Update the password if needed
|
|
|
|
if (! empty($values['password'])) {
|
2015-01-18 00:53:40 +01:00
|
|
|
$values['password'] = password_hash($values['password'], PASSWORD_BCRYPT);
|
2013-12-23 03:25:54 +01:00
|
|
|
} else {
|
|
|
|
unset($values['password']);
|
|
|
|
}
|
|
|
|
|
|
|
|
unset($values['confirmation']);
|
|
|
|
|
|
|
|
// If the user does not want content of feeds, remove it in previous ones
|
|
|
|
if (isset($values['nocontent']) && (bool) $values['nocontent']) {
|
2014-02-08 20:13:14 +01:00
|
|
|
Database::get('db')->table('items')->update(array('content' => ''));
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
|
|
|
|
2015-01-28 05:26:36 +01:00
|
|
|
if (Database::get('db')->hashtable('settings')->put($values)) {
|
2014-12-24 16:47:24 +01:00
|
|
|
reload();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reload the cache in session
|
|
|
|
function reload()
|
|
|
|
{
|
|
|
|
$_SESSION['config'] = get_all();
|
2015-01-18 00:53:40 +01:00
|
|
|
Translator\load(get('language'));
|
2013-12-23 03:25:54 +01:00
|
|
|
}
|
2014-05-27 02:47:40 +02:00
|
|
|
|
|
|
|
// 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');
|
|
|
|
}
|