Change the database structure to have a single database

This is a major change for the next release of Miniflux.

- There is now only one database that can supports multiple users
- There is no automated schema migration for this release
- A migration procedure is available in the ChangeLog file
This commit is contained in:
Frederic Guillot 2016-12-26 09:44:53 -05:00
parent 3e5a1bc524
commit 82df35a59b
111 changed files with 3713 additions and 3097 deletions

1
.gitignore vendored
View File

@ -17,6 +17,7 @@ Thumbs.db
.nbproject .nbproject
nbproject nbproject
config.php config.php
!app/helpers/*
!app/models/* !app/models/*
!app/controllers/* !app/controllers/*
!app/templates/* !app/templates/*

View File

@ -16,4 +16,4 @@ before_script:
- composer install - composer install
script: script:
- phpunit -c tests/phpunit.unit.xml - ./vendor/bin/phpunit -c tests/phpunit.unit.xml

18
ChangeLog Normal file
View File

@ -0,0 +1,18 @@
Version 1.2.0 (unreleased)
-------------
* Major change to the database structure to have a single database for multiple users
* Web access token for the cronjob
* New config parameter to disable web access for the cronjob
* Debug mode parameter is moved to the config file
* The console web page have been removed
* New API methods (not backward compatible)
* Fever API tokens are longer than before
* Add support for Wallabag service
* Add unit tests
Migration procedure from 1.1.x to 1.2.0:
To import your old database to the new database format, use this script:
php scripts/migrate-db.php --sqlite-db=/path/to/my/db.sqlite --admin==1

View File

@ -1,9 +1,10 @@
.PHONY: archive
.PHONY: docker-image .PHONY: docker-image
.PHONY: docker-push .PHONY: docker-push
.PHONY: docker-destroy .PHONY: docker-destroy
.PHONY: docker-run .PHONY: docker-run
.PHONY: archive
.PHONY: js .PHONY: js
.PHONY: unit-test-sqlite
JS_FILE = assets/js/all.js JS_FILE = assets/js/all.js
CONTAINER = miniflux CONTAINER = miniflux
@ -36,3 +37,6 @@ $(JS_FILE): assets/js/app.js \
# Build a new archive: make archive version=1.2.3 dst=/tmp # Build a new archive: make archive version=1.2.3 dst=/tmp
archive: archive:
@ git archive --format=zip --prefix=miniflux/ v${version} -o ${dst}/miniflux-${version}.zip @ git archive --format=zip --prefix=miniflux/ v${version} -o ${dst}/miniflux-${version}.zip
unit-test-sqlite:
@ ./vendor/bin/phpunit -c tests/phpunit.unit.xml

View File

@ -6,25 +6,27 @@ if (file_exists(__DIR__.'/../config.php')) {
require __DIR__.'/../config.php'; require __DIR__.'/../config.php';
} }
require __DIR__.'/constants.php'; require_once __DIR__.'/constants.php';
require __DIR__.'/check_setup.php'; require_once __DIR__.'/check_setup.php';
require __DIR__.'/functions.php'; require_once __DIR__.'/functions.php';
PicoDb\Database::setInstance('db', function() { PicoDb\Database::setInstance('db', function() {
$db = new PicoDb\Database(array( $db = new PicoDb\Database(array(
'driver' => 'sqlite', 'driver' => 'sqlite',
'filename' => Miniflux\Model\Database\get_path(), 'filename' => DB_FILENAME,
)); ));
$db->getStatementHandler()->withLogging();
if ($db->schema('\Miniflux\Schema')->check(Miniflux\Schema\VERSION)) { if ($db->schema('\Miniflux\Schema')->check(Miniflux\Schema\VERSION)) {
return $db; return $db;
} } else {
else {
$errors = $db->getLogMessages(); $errors = $db->getLogMessages();
$nb_errors = count($errors);
$html = 'Unable to migrate the database schema, <strong>please copy and paste this message and create a bug report:</strong><hr/>'; $html = 'Unable to migrate the database schema, <strong>please copy and paste this message and create a bug report:</strong><hr/>';
$html .= '<pre><code>'; $html .= '<pre><code>';
$html .= (isset($errors[0]) ? $errors[0] : 'Unknown SQL error').PHP_EOL.PHP_EOL; $html .= (isset($errors[$nb_errors - 1]) ? $errors[$nb_errors - 1] : 'Unknown SQL error').PHP_EOL.PHP_EOL;
$html .= '- PHP version: '.phpversion().PHP_EOL; $html .= '- PHP version: '.phpversion().PHP_EOL;
$html .= '- SAPI: '.php_sapi_name().PHP_EOL; $html .= '- SAPI: '.php_sapi_name().PHP_EOL;
$html .= '- PDO Sqlite version: '.phpversion('pdo_sqlite').PHP_EOL; $html .= '- PDO Sqlite version: '.phpversion('pdo_sqlite').PHP_EOL;

View File

@ -2,6 +2,7 @@
defined('APP_VERSION') or define('APP_VERSION', Miniflux\Helper\parse_app_version('$Format:%d$','$Format:%H$')); defined('APP_VERSION') or define('APP_VERSION', Miniflux\Helper\parse_app_version('$Format:%d$','$Format:%H$'));
define('HTTP_USER_AGENT', 'Miniflux (https://miniflux.net)');
defined('HTTP_TIMEOUT') or define('HTTP_TIMEOUT', 20); defined('HTTP_TIMEOUT') or define('HTTP_TIMEOUT', 20);
defined('HTTP_MAX_RESPONSE_SIZE') or define('HTTP_MAX_RESPONSE_SIZE', 2097152); defined('HTTP_MAX_RESPONSE_SIZE') or define('HTTP_MAX_RESPONSE_SIZE', 2097152);
@ -12,9 +13,9 @@ defined('DATA_DIRECTORY') or define('DATA_DIRECTORY', ROOT_DIRECTORY.DIRECTORY_S
defined('FAVICON_DIRECTORY') or define('FAVICON_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'favicons'); defined('FAVICON_DIRECTORY') or define('FAVICON_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'favicons');
defined('FAVICON_URL_PATH') or define('FAVICON_URL_PATH', 'data/favicons'); defined('FAVICON_URL_PATH') or define('FAVICON_URL_PATH', 'data/favicons');
defined('ENABLE_MULTIPLE_DB') or define('ENABLE_MULTIPLE_DB', true); defined('DB_FILENAME') or define('DB_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'db.sqlite');
defined('DB_FILENAME') or define('DB_FILENAME', 'db.sqlite');
defined('DEBUG_MODE') or define('DEBUG_MODE', false);
defined('DEBUG_FILENAME') or define('DEBUG_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'debug.log'); defined('DEBUG_FILENAME') or define('DEBUG_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'debug.log');
defined('THEME_DIRECTORY') or define('THEME_DIRECTORY', 'themes'); defined('THEME_DIRECTORY') or define('THEME_DIRECTORY', 'themes');
@ -35,6 +36,7 @@ defined('SUBSCRIPTION_CONCURRENT_REQUESTS') or define('SUBSCRIPTION_CONCURRENT_R
defined('RULES_DIRECTORY') or define('RULES_DIRECTORY', ROOT_DIRECTORY.DIRECTORY_SEPARATOR.'rules'); defined('RULES_DIRECTORY') or define('RULES_DIRECTORY', ROOT_DIRECTORY.DIRECTORY_SEPARATOR.'rules');
defined('ENABLE_HSTS') or define('ENABLE_HSTS', true); defined('ENABLE_HSTS') or define('ENABLE_HSTS', true);
defined('ENABLE_CRONJOB_HTTP_ACCESS') or define('ENABLE_CRONJOB_HTTP_ACCESS', true);
defined('BEANSTALKD_HOST') or define('BEANSTALKD_HOST', '127.0.0.1'); defined('BEANSTALKD_HOST') or define('BEANSTALKD_HOST', '127.0.0.1');
defined('BEANSTALKD_QUEUE') or define('BEANSTALKD_QUEUE', 'feeds'); defined('BEANSTALKD_QUEUE') or define('BEANSTALKD_QUEUE', 'feeds');

22
app/controllers/about.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace Miniflux\Controller;
use Miniflux\Model;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Session\SessionStorage;
use Miniflux\Template;
use Miniflux\Helper;
Router\get_action('about', function () {
$user_id = SessionStorage::getInstance()->getUserId();
Response\html(Template\layout('about', array(
'csrf' => Helper\generate_csrf(),
'config' => Model\Config\get_all($user_id),
'user' => Model\User\get_user_by_id_without_password($user_id),
'menu' => 'config',
'title' => t('About'),
)));
});

20
app/controllers/api.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace Miniflux\Controller;
use Miniflux\Model;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Session\SessionStorage;
use Miniflux\Template;
Router\get_action('api', function () {
$user_id = SessionStorage::getInstance()->getUserId();
Response\html(Template\layout('api', array(
'config' => Model\Config\get_all($user_id),
'user' => Model\User\get_user_by_id_without_password($user_id),
'menu' => 'config',
'title' => t('Preferences'),
)));
});

View File

@ -1,8 +1,13 @@
<?php <?php
namespace Miniflux\Controller;
use Miniflux\Session\SessionManager;
use Miniflux\Session\SessionStorage;
use Miniflux\Validator; use Miniflux\Validator;
use Miniflux\Router; use Miniflux\Router;
use Miniflux\Response; use Miniflux\Response;
use Miniflux\Model\RememberMe;
use Miniflux\Request; use Miniflux\Request;
use Miniflux\Template; use Miniflux\Template;
use Miniflux\Helper; use Miniflux\Helper;
@ -10,13 +15,15 @@ use Miniflux\Model;
// Logout and destroy session // Logout and destroy session
Router\get_action('logout', function () { Router\get_action('logout', function () {
Model\User\logout(); SessionStorage::getInstance()->flush();
SessionManager::close();
RememberMe\destroy();
Response\redirect('?action=login'); Response\redirect('?action=login');
}); });
// Display form login // Display form login
Router\get_action('login', function () { Router\get_action('login', function () {
if (Model\User\is_loggedin()) { if (SessionStorage::getInstance()->isLogged()) {
Response\redirect('?action=unread'); Response\redirect('?action=unread');
} }
@ -25,8 +32,6 @@ Router\get_action('login', function () {
'values' => array( 'values' => array(
'csrf' => Helper\generate_csrf(), 'csrf' => Helper\generate_csrf(),
), ),
'databases' => Model\Database\get_list(),
'current_database' => Model\Database\select()
))); )));
}); });
@ -43,7 +48,5 @@ Router\post_action('login', function () {
Response\html(Template\load('login', array( Response\html(Template\load('login', array(
'errors' => $errors, 'errors' => $errors,
'values' => $values + array('csrf' => Helper\generate_csrf()), 'values' => $values + array('csrf' => Helper\generate_csrf()),
'databases' => Model\Database\get_list(),
'current_database' => Model\Database\select()
))); )));
}); });

View File

@ -1,120 +1,127 @@
<?php <?php
use PicoFeed\Syndication\AtomFeedBuilder; namespace Miniflux\Controller;
use PicoFeed\Syndication\AtomItemBuilder;
use DateTime;
use Miniflux\Session\SessionStorage;
use Miniflux\Router; use Miniflux\Router;
use Miniflux\Response; use Miniflux\Response;
use Miniflux\Request; use Miniflux\Request;
use Miniflux\Template; use Miniflux\Template;
use Miniflux\Helper; use Miniflux\Helper;
use Miniflux\Model; use Miniflux\Model;
use Miniflux\Handler\Service;
use PicoFeed\Syndication\AtomFeedBuilder;
use PicoFeed\Syndication\AtomItemBuilder;
// Ajax call to add or remove a bookmark // Ajax call to add or remove a bookmark
Router\post_action('bookmark', function () { Router\post_action('bookmark', function () {
$id = Request\param('id'); $user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
$value = Request\int_param('value'); $value = Request\int_param('value');
if ($value == 1) {
Service\sync($user_id, $item_id);
}
Response\json(array( Response\json(array(
'id' => $id, 'id' => $item_id,
'value' => $value, 'value' => $value,
'result' => Model\Bookmark\set_flag($id, $value), 'result' => Model\Bookmark\set_flag($user_id, $item_id, $value),
)); ));
}); });
// Add new bookmark // Add new bookmark
Router\get_action('bookmark', function () { Router\get_action('bookmark', function () {
$id = Request\param('id'); $user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
$menu = Request\param('menu'); $menu = Request\param('menu');
$redirect = Request\param('redirect', 'unread'); $redirect = Request\param('redirect', 'unread');
$offset = Request\int_param('offset', 0); $offset = Request\int_param('offset', 0);
$feed_id = Request\int_param('feed_id', 0); $feed_id = Request\int_param('feed_id', 0);
$value = Request\int_param('value');
Model\Bookmark\set_flag($id, Request\int_param('value')); if ($value == 1) {
Service\sync($user_id, $item_id);
if ($redirect === 'show') {
Response\redirect('?action=show&menu='.$menu.'&id='.$id);
} }
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id); Model\Bookmark\set_flag($user_id, $item_id, $value);
if ($redirect === 'show') {
Response\redirect('?action=show&menu='.$menu.'&id='.$item_id);
}
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$item_id);
}); });
// Display bookmarks page // Display bookmarks page
Router\get_action('bookmarks', function () { Router\get_action('bookmarks', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$offset = Request\int_param('offset', 0); $offset = Request\int_param('offset', 0);
$group_id = Request\int_param('group_id', null); $group_id = Request\int_param('group_id', null);
$feed_ids = array(); $feed_ids = array();
if ($group_id !== null) { if ($group_id !== null) {
$feed_ids = Model\Group\get_feeds_by_group($group_id); $feed_ids = Model\Group\get_feed_ids_by_group($group_id);
} }
$nb_items = Model\Bookmark\count_items($feed_ids); $nb_items = Model\Bookmark\count_bookmarked_items($user_id, $feed_ids);
$items = Model\Bookmark\get_all_items( $items = Model\Bookmark\get_bookmarked_items(
$user_id,
$offset, $offset,
Model\Config\get('items_per_page'), Helper\config('items_per_page'),
$feed_ids $feed_ids
); );
Response\html(Template\layout('bookmarks', array( Response\html(Template\layout('bookmarks', array(
'favicons' => Model\Favicon\get_item_favicons($items), 'favicons' => Model\Favicon\get_items_favicons($items),
'original_marks_read' => Model\Config\get('original_marks_read'), 'original_marks_read' => Helper\config('original_marks_read'),
'order' => '', 'order' => '',
'direction' => '', 'direction' => '',
'display_mode' => Model\Config\get('items_display_mode'), 'display_mode' => Helper\config('items_display_mode'),
'item_title_link' => Model\Config\get('item_title_link'), 'item_title_link' => Helper\config('item_title_link'),
'group_id' => $group_id, 'group_id' => $group_id,
'items' => $items, 'items' => $items,
'nb_items' => $nb_items, 'nb_items' => $nb_items,
'offset' => $offset, 'offset' => $offset,
'items_per_page' => Model\Config\get('items_per_page'), 'items_per_page' => Helper\config('items_per_page'),
'nothing_to_read' => Request\int_param('nothing_to_read'), 'nothing_to_read' => Request\int_param('nothing_to_read'),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'bookmarks', 'menu' => 'bookmarks',
'groups' => Model\Group\get_all(), 'groups' => Model\Group\get_all($user_id),
'title' => t('Bookmarks').' ('.$nb_items.')' 'title' => t('Bookmarks').' ('.$nb_items.')'
))); )));
}); });
// Display bookmark feeds // Display bookmark feeds
Router\get_action('bookmark-feed', function () { Router\get_action('bookmark-feed', function () {
// Select database if the parameter is set $token = Request\param('token');
$database = Request\param('database'); $user = Model\User\get_user_by_token('feed_token', $token);
if (!empty($database)) { if (empty($user)) {
Model\Database\select($database); Response\text('Unauthorized', 401);
} }
// Check token $bookmarks = Model\Bookmark\get_bookmarked_items($user['id']);
$feed_token = Model\Config\get('feed_token');
$request_token = Request\param('token');
if ($feed_token !== $request_token) {
Response\text('Access Forbidden', 403);
}
// Build Feed
$bookmarks = Model\Bookmark\get_all_items();
$feedBuilder = AtomFeedBuilder::create() $feedBuilder = AtomFeedBuilder::create()
->withTitle(t('Bookmarks').' - Miniflux') ->withTitle(t('Bookmarks').' - Miniflux')
->withFeedUrl(Helper\get_current_base_url().'?action=bookmark-feed&token='.urlencode($feed_token)) ->withFeedUrl(Helper\get_current_base_url().'?action=bookmark-feed&token='.urlencode($user['feed_token']))
->withSiteUrl(Helper\get_current_base_url()) ->withSiteUrl(Helper\get_current_base_url())
->withDate(new DateTime()) ->withDate(new DateTime())
; ;
foreach ($bookmarks as $bookmark) { foreach ($bookmarks as $bookmark) {
$article = Model\Item\get($bookmark['id']);
$articleDate = new DateTime(); $articleDate = new DateTime();
$articleDate->setTimestamp($article['updated']); $articleDate->setTimestamp($bookmark['updated']);
$feedBuilder $feedBuilder
->withItem(AtomItemBuilder::create($feedBuilder) ->withItem(AtomItemBuilder::create($feedBuilder)
->withId($article['id']) ->withId($bookmark['id'])
->withTitle($article['title']) ->withTitle($bookmark['title'])
->withUrl($article['url']) ->withUrl($bookmark['url'])
->withUpdatedDate($articleDate) ->withUpdatedDate($articleDate)
->withPublishedDate($articleDate) ->withPublishedDate($articleDate)
->withContent($article['content']) ->withContent($bookmark['content'])
); );
} }

View File

@ -1,10 +1,12 @@
<?php <?php
namespace Miniflux\Controller;
use Miniflux\Router; use Miniflux\Router;
use Miniflux\Response; use Miniflux\Response;
use Miniflux\Request; use Miniflux\Request;
use Miniflux\Session; use Miniflux\Session\SessionManager;
use Miniflux\Template; use Miniflux\Session\SessionStorage;
use Miniflux\Helper; use Miniflux\Helper;
use Miniflux\Model; use Miniflux\Model;
use Miniflux\Translator; use Miniflux\Translator;
@ -12,39 +14,22 @@ use Miniflux\Handler;
// Called before each action // Called before each action
Router\before(function ($action) { Router\before(function ($action) {
Session\open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH, 0); SessionManager::open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH, 0);
// Select the requested database either from post param database or from the
// session variable. If it fails, logout to destroy session and
// 'remember me' cookie
if (Request\value('database') !== null && ! Model\Database\select(Request\value('database'))) {
Model\User\logout();
Response\redirect('?action=login');
} elseif (! empty($_SESSION['database'])) {
if (! Model\Database\select($_SESSION['database'])) {
Model\User\logout();
Response\redirect('?action=login');
}
}
// These actions are considered to be safe even for unauthenticated users // These actions are considered to be safe even for unauthenticated users
$safe_actions = array('login', 'bookmark-feed', 'select-db', 'logout', 'notfound'); $safe_actions = array('login', 'bookmark-feed', 'logout', 'notfound');
if (! SessionStorage::getInstance()->isLogged() && ! in_array($action, $safe_actions)) {
if (! Model\User\is_loggedin() && ! in_array($action, $safe_actions)) {
if (! Model\RememberMe\authenticate()) { if (! Model\RememberMe\authenticate()) {
Model\User\logout();
Response\redirect('?action=login'); Response\redirect('?action=login');
} }
} elseif (Model\RememberMe\has_cookie()) {
Model\RememberMe\refresh();
} }
// Load translations // Load translations
$language = Model\Config\get('language') ?: 'en_US'; $language = Helper\config('language', 'en_US');
Translator\load($language); Translator\load($language);
// Set timezone // Set timezone
date_default_timezone_set(Model\Config\get('timezone') ?: 'UTC'); date_default_timezone_set(Helper\config('timezone', 'UTC'));
// HTTP secure headers // HTTP secure headers
Response\csp(array( Response\csp(array(
@ -64,13 +49,53 @@ Router\before(function ($action) {
} }
}); });
// Show help
Router\get_action('show-help', function () {
Response\html(Template\load('show_help'));
});
// Image proxy (avoid SSL mixed content warnings) // Image proxy (avoid SSL mixed content warnings)
Router\get_action('proxy', function () { Router\get_action('proxy', function () {
Handler\Proxy\download(rawurldecode(Request\param('url'))); Handler\Proxy\download(rawurldecode(Request\param('url')));
exit; exit;
}); });
function items_list($status)
{
$order = Request\param('order', 'updated');
$direction = Request\param('direction', Helper\config('items_sorting_direction'));
$offset = Request\int_param('offset', 0);
$group_id = Request\int_param('group_id', null);
$nb_items_page = Helper\config('items_per_page');
$user_id = SessionStorage::getInstance()->getUserId();
$feed_ids = array();
if ($group_id !== null) {
$feed_ids = Model\Group\get_feed_ids_by_group($group_id);
}
$items = Model\Item\get_items_by_status(
$user_id,
$status,
$feed_ids,
$offset,
$nb_items_page,
$order,
$direction
);
$nb_items = Model\Item\count_by_status($user_id, $status, $feed_ids);
$nb_unread_items = Model\Item\count_by_status($user_id, $status);
return array(
'nothing_to_read' => Request\int_param('nothing_to_read'),
'favicons' => Model\Favicon\get_items_favicons($items),
'original_marks_read' => Helper\bool_config('original_marks_read'),
'display_mode' => Helper\config('items_display_mode'),
'item_title_link' => Helper\config('item_title_link'),
'items_per_page' => $nb_items_page,
'offset' => $offset,
'direction' => $direction,
'order' => $order,
'items' => $items,
'nb_items' => $nb_items,
'nb_unread_items' => $nb_unread_items,
'group_id' => $group_id,
'groups' => Model\Group\get_all($user_id),
);
}

View File

@ -1,65 +1,19 @@
<?php <?php
use PicoDb\Database; namespace Miniflux\Controller;
use Miniflux\Session\SessionStorage;
use Miniflux\Validator; use Miniflux\Validator;
use Miniflux\Router; use Miniflux\Router;
use Miniflux\Response; use Miniflux\Response;
use Miniflux\Request; use Miniflux\Request;
use Miniflux\Session;
use Miniflux\Template; use Miniflux\Template;
use Miniflux\Helper; use Miniflux\Helper;
use Miniflux\Model; use Miniflux\Model;
// 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(
'csrf' => Helper\generate_csrf(),
),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
'title' => t('New database')
)));
}
Response\redirect('?action=database');
});
// Create a new database
Router\post_action('new-db', function () {
if (ENABLE_MULTIPLE_DB) {
$values = Request\values();
Helper\check_csrf_values($values);
list($valid, $errors) = Validator\User\validate_creation($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=database');
}
Response\html(Template\layout('new_db', array(
'errors' => $errors,
'values' => $values + array('csrf' => Helper\generate_csrf()),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
'title' => t('New database')
)));
}
Response\redirect('?action=database');
});
// Confirmation box before auto-update // Confirmation box before auto-update
Router\get_action('confirm-auto-update', function () { Router\get_action('confirm-auto-update', function () {
Response\html(Template\layout('confirm_auto_update', array( Response\html(Template\layout('confirm_auto_update', array(
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config', 'menu' => 'config',
'title' => t('Confirmation') 'title' => t('Confirmation')
))); )));
@ -68,10 +22,10 @@ Router\get_action('confirm-auto-update', function () {
// Auto-update // Auto-update
Router\get_action('auto-update', function () { Router\get_action('auto-update', function () {
if (ENABLE_AUTO_UPDATE) { if (ENABLE_AUTO_UPDATE) {
if (Model\AutoUpdate\execute(Model\Config\get('auto_update_url'))) { if (Model\AutoUpdate\execute(Helper\config('auto_update_url'))) {
Session\flash(t('Miniflux is updated!')); SessionStorage::getInstance()->setFlashMessage(t('Miniflux is updated!'));
} else { } else {
Session\flash_error(t('Unable to update Miniflux, check the console for errors.')); SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to update Miniflux, check the console for errors.'));
} }
} }
@ -80,35 +34,22 @@ Router\get_action('auto-update', function () {
// Re-generate tokens // Re-generate tokens
Router\get_action('generate-tokens', function () { Router\get_action('generate-tokens', function () {
$user_id = SessionStorage::getInstance()->getUserId();
if (Helper\check_csrf(Request\param('csrf'))) { if (Helper\check_csrf(Request\param('csrf'))) {
Model\Config\new_tokens(); Model\User\regenerate_tokens($user_id);
} }
Response\redirect('?action=config'); Response\redirect('?action=config');
}); });
// Optimize the database manually
Router\get_action('optimize-db', function () {
if (Helper\check_csrf(Request\param('csrf'))) {
Database::getInstance('db')->getConnection()->exec('VACUUM');
}
Response\redirect('?action=database');
});
// Download the compressed database
Router\get_action('download-db', function () {
if (Helper\check_csrf(Request\param('csrf'))) {
Response\force_download('db.sqlite.gz');
Response\binary(gzencode(file_get_contents(Model\Database\get_path())));
}
});
// Display preferences page // Display preferences page
Router\get_action('config', function () { Router\get_action('config', function () {
$user_id = SessionStorage::getInstance()->getUserId();
Response\html(Template\layout('config', array( Response\html(Template\layout('config', array(
'errors' => array(), 'errors' => array(),
'values' => Model\Config\get_all() + array('csrf' => Helper\generate_csrf()), 'values' => Model\Config\get_all($user_id) + array('csrf' => Helper\generate_csrf()),
'languages' => Model\Config\get_languages(), 'languages' => Model\Config\get_languages(),
'timezones' => Model\Config\get_timezones(), 'timezones' => Model\Config\get_timezones(),
'autoflush_read_options' => Model\Config\get_autoflush_read_options(), 'autoflush_read_options' => Model\Config\get_autoflush_read_options(),
@ -119,7 +60,6 @@ Router\get_action('config', function () {
'display_mode' => Model\Config\get_display_mode(), 'display_mode' => Model\Config\get_display_mode(),
'item_title_link' => Model\Config\get_item_title_link(), 'item_title_link' => Model\Config\get_item_title_link(),
'redirect_nothing_to_read_options' => Model\Config\get_nothing_to_read_redirections(), 'redirect_nothing_to_read_options' => Model\Config\get_nothing_to_read_redirections(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config', 'menu' => 'config',
'title' => t('Preferences') 'title' => t('Preferences')
))); )));
@ -127,15 +67,16 @@ Router\get_action('config', function () {
// Update preferences // Update preferences
Router\post_action('config', function () { Router\post_action('config', function () {
$values = Request\values() + array('nocontent' => 0, 'image_proxy' => 0, 'favicons' => 0, 'debug_mode' => 0, 'original_marks_read' => 0); $user_id = SessionStorage::getInstance()->getUserId();
$values = Request\values() + array('nocontent' => 0, 'image_proxy' => 0, 'favicons' => 0, 'original_marks_read' => 0);
Helper\check_csrf_values($values); Helper\check_csrf_values($values);
list($valid, $errors) = Validator\Config\validate_modification($values); list($valid, $errors) = Validator\Config\validate_modification($values);
if ($valid) { if ($valid) {
if (Model\Config\save($values)) { if (Model\Config\save($user_id, $values)) {
Session\flash(t('Your preferences are updated.')); SessionStorage::getInstance()->setFlashMessage(t('Your preferences are updated.'));
} else { } else {
Session\flash_error(t('Unable to update your preferences.')); SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to update your preferences.'));
} }
Response\redirect('?action=config'); Response\redirect('?action=config');
@ -143,7 +84,7 @@ Router\post_action('config', function () {
Response\html(Template\layout('config', array( Response\html(Template\layout('config', array(
'errors' => $errors, 'errors' => $errors,
'values' => Model\Config\get_all() + array('csrf' => Helper\generate_csrf()), 'values' => Model\Config\get_all($user_id) + array('csrf' => Helper\generate_csrf()),
'languages' => Model\Config\get_languages(), 'languages' => Model\Config\get_languages(),
'timezones' => Model\Config\get_timezones(), 'timezones' => Model\Config\get_timezones(),
'autoflush_read_options' => Model\Config\get_autoflush_read_options(), 'autoflush_read_options' => Model\Config\get_autoflush_read_options(),
@ -154,7 +95,6 @@ Router\post_action('config', function () {
'redirect_nothing_to_read_options' => Model\Config\get_nothing_to_read_redirections(), 'redirect_nothing_to_read_options' => Model\Config\get_nothing_to_read_redirections(),
'display_mode' => Model\Config\get_display_mode(), 'display_mode' => Model\Config\get_display_mode(),
'item_title_link' => Model\Config\get_item_title_link(), 'item_title_link' => Model\Config\get_item_title_link(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config', 'menu' => 'config',
'title' => t('Preferences') 'title' => t('Preferences')
))); )));
@ -162,84 +102,17 @@ Router\post_action('config', function () {
// Get configuration parameters (AJAX request) // Get configuration parameters (AJAX request)
Router\post_action('get-config', function () { Router\post_action('get-config', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$return = array(); $return = array();
$options = Request\values(); $options = Request\values();
if (empty($options)) { if (empty($options)) {
$return = Model\Config\get_all(); $return = Model\Config\get_all($user_id);
} else { } else {
foreach ($options as $name) { foreach ($options as $name) {
$return[$name] = Model\Config\get($name); $return[$name] = Helper\config($name);
} }
} }
Response\json($return); Response\json($return);
}); });
// Display help page
Router\get_action('help', function () {
Response\html(Template\layout('help', array(
'config' => Model\Config\get_all(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
'title' => t('Preferences')
)));
});
// Display about page
Router\get_action('about', function () {
Response\html(Template\layout('about', array(
'csrf' => Helper\generate_csrf(),
'config' => Model\Config\get_all(),
'db_name' => Model\Database\select(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
'title' => t('Preferences')
)));
});
// Display database page
Router\get_action('database', function () {
Response\html(Template\layout('database', array(
'csrf' => Helper\generate_csrf(),
'config' => Model\Config\get_all(),
'db_size' => filesize(Model\Database\get_path()),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
'title' => t('Preferences')
)));
});
// Display API page
Router\get_action('api', function () {
Response\html(Template\layout('api', array(
'config' => Model\Config\get_all(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
'title' => t('Preferences')
)));
});
// Display bookmark services page
Router\get_action('services', function () {
Response\html(Template\layout('services', array(
'errors' => array(),
'values' => Model\Config\get_all() + array('csrf' => Helper\generate_csrf()),
'menu' => 'config',
'title' => t('Preferences')
)));
});
// Update bookmark services
Router\post_action('services', function () {
$values = Request\values() + array('pinboard_enabled' => 0, 'instapaper_enabled' => 0, 'wallabag_enabled' => 0);
Helper\check_csrf_values($values);
if (Model\Config\save($values)) {
Session\flash(t('Your preferences are updated.'));
} else {
Session\flash_error(t('Unable to update your preferences.'));
}
Response\redirect('?action=services');
});

View File

@ -1,22 +0,0 @@
<?php
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Template;
use Miniflux\Model;
// Flush console messages
Router\get_action('flush-console', function () {
@unlink(DEBUG_FILENAME);
Response\redirect('?action=console');
});
// Display console
Router\get_action('console', function () {
Response\html(Template\layout('console', array(
'content' => @file_get_contents(DEBUG_FILENAME),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
'title' => t('Console')
)));
});

View File

@ -1,11 +1,12 @@
<?php <?php
use PicoFeed\Parser\MalformedXmlException; namespace Miniflux\Controller;
use Miniflux\Session\SessionStorage;
use Miniflux\Validator; use Miniflux\Validator;
use Miniflux\Router; use Miniflux\Router;
use Miniflux\Response; use Miniflux\Response;
use Miniflux\Request; use Miniflux\Request;
use Miniflux\Session;
use Miniflux\Template; use Miniflux\Template;
use Miniflux\Helper; use Miniflux\Helper;
use Miniflux\Handler; use Miniflux\Handler;
@ -13,25 +14,26 @@ use Miniflux\Model;
// Refresh all feeds, used when Javascript is disabled // Refresh all feeds, used when Javascript is disabled
Router\get_action('refresh-all', function () { Router\get_action('refresh-all', function () {
Model\Feed\refresh_all(); $user_id = SessionStorage::getInstance()->getUserId();
Session\flash(t('Your subscriptions are updated')); Handler\Feed\update_feeds($user_id);
SessionStorage::getInstance()->setFlashErrorMessage(t('Your subscriptions are updated'));
Response\redirect('?action=unread'); Response\redirect('?action=unread');
}); });
// Edit feed form // Edit feed form
Router\get_action('edit-feed', function () { Router\get_action('edit-feed', function () {
$id = Request\int_param('feed_id'); $user_id = SessionStorage::getInstance()->getUserId();
$feed_id = Request\int_param('feed_id');
$values = Model\Feed\get($id); $values = Model\Feed\get_feed($user_id, $feed_id);
$values += array( $values += array(
'feed_group_ids' => Model\Group\get_feed_group_ids($id) 'feed_group_ids' => Model\Group\get_feed_group_ids($feed_id)
); );
Response\html(Template\layout('edit_feed', array( Response\html(Template\layout('edit_feed', array(
'values' => $values, 'values' => $values,
'errors' => array(), 'errors' => array(),
'nb_unread_items' => Model\Item\count_by_status('unread'), 'groups' => Model\Group\get_all($user_id),
'groups' => Model\Group\get_all(),
'menu' => 'feeds', 'menu' => 'feeds',
'title' => t('Edit subscription') 'title' => t('Edit subscription')
))); )));
@ -39,32 +41,32 @@ Router\get_action('edit-feed', function () {
// Submit edit feed form // Submit edit feed form
Router\post_action('edit-feed', function () { Router\post_action('edit-feed', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$values = Request\values(); $values = Request\values();
$values += array( $values += array(
'enabled' => 0, 'enabled' => 0,
'download_content' => 0, 'download_content' => 0,
'rtl' => 0, 'rtl' => 0,
'cloak_referrer' => 0, 'cloak_referrer' => 0,
'parsing_error' => 0,
'feed_group_ids' => array(), 'feed_group_ids' => array(),
'create_group' => ''
); );
list($valid, $errors) = Validator\Feed\validate_modification($values); list($valid, $errors) = Validator\Feed\validate_modification($values);
if ($valid) { if ($valid) {
if (Model\Feed\update($values)) { if (Model\Feed\update_feed($user_id, $values['id'], $values)) {
Session\flash(t('Your subscription has been updated.')); SessionStorage::getInstance()->setFlashMessage(t('Your subscription has been updated.'));
Response\redirect('?action=feeds'); Response\redirect('?action=feeds');
} else { } else {
Session\flash_error(t('Unable to edit your subscription.')); SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to edit your subscription.'));
} }
} }
Response\html(Template\layout('edit_feed', array( Response\html(Template\layout('edit_feed', array(
'values' => $values, 'values' => $values,
'errors' => $errors, 'errors' => $errors,
'nb_unread_items' => Model\Item\count_by_status('unread'), 'groups' => Model\Group\get_all($user_id),
'groups' => Model\Group\get_all(),
'menu' => 'feeds', 'menu' => 'feeds',
'title' => t('Edit subscription') 'title' => t('Edit subscription')
))); )));
@ -72,11 +74,11 @@ Router\post_action('edit-feed', function () {
// Confirmation box to remove a feed // Confirmation box to remove a feed
Router\get_action('confirm-remove-feed', function () { Router\get_action('confirm-remove-feed', function () {
$id = Request\int_param('feed_id'); $user_id = SessionStorage::getInstance()->getUserId();
$feed_id = Request\int_param('feed_id');
Response\html(Template\layout('confirm_remove_feed', array( Response\html(Template\layout('confirm_remove_feed', array(
'feed' => Model\Feed\get($id), 'feed' => Model\Feed\get_feed($user_id, $feed_id),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'feeds', 'menu' => 'feeds',
'title' => t('Confirmation') 'title' => t('Confirmation')
))); )));
@ -84,12 +86,13 @@ Router\get_action('confirm-remove-feed', function () {
// Remove a feed // Remove a feed
Router\get_action('remove-feed', function () { Router\get_action('remove-feed', function () {
$id = Request\int_param('feed_id'); $user_id = SessionStorage::getInstance()->getUserId();
$feed_id = Request\int_param('feed_id');
if ($id && Model\Feed\remove($id)) { if (Model\Feed\remove_feed($user_id, $feed_id)) {
Session\flash(t('This subscription has been removed successfully.')); SessionStorage::getInstance()->setFlashMessage(t('This subscription has been removed successfully.'));
} else { } else {
Session\flash_error(t('Unable to remove this subscription.')); SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to remove this subscription.'));
} }
Response\redirect('?action=feeds'); Response\redirect('?action=feeds');
@ -97,40 +100,43 @@ Router\get_action('remove-feed', function () {
// Refresh one feed and redirect to unread items // Refresh one feed and redirect to unread items
Router\get_action('refresh-feed', function () { Router\get_action('refresh-feed', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$feed_id = Request\int_param('feed_id'); $feed_id = Request\int_param('feed_id');
$redirect = Request\param('redirect', 'unread'); $redirect = Request\param('redirect', 'unread');
Model\Feed\refresh($feed_id); Handler\Feed\update_feed($user_id, $feed_id);
Response\redirect('?action='.$redirect.'&feed_id='.$feed_id); Response\redirect('?action='.$redirect.'&feed_id='.$feed_id);
}); });
// Ajax call to refresh one feed // Ajax call to refresh one feed
Router\post_action('refresh-feed', function () { Router\post_action('refresh-feed', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$feed_id = Request\int_param('feed_id', 0); $feed_id = Request\int_param('feed_id', 0);
Response\json(array( Response\json(array(
'feed_id' => $feed_id, 'feed_id' => $feed_id,
'result' => Model\Feed\refresh($feed_id), 'result' => Handler\Feed\update_feed($user_id, $feed_id),
'items_count' => Model\Feed\count_items($feed_id), 'items_count' => Model\ItemFeed\count_items_by_status($user_id, $feed_id),
)); ));
}); });
// Display all feeds // Display all feeds
Router\get_action('feeds', function () { Router\get_action('feeds', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$nothing_to_read = Request\int_param('nothing_to_read'); $nothing_to_read = Request\int_param('nothing_to_read');
$nb_unread_items = Model\Item\count_by_status('unread'); $nb_unread_items = Model\Item\count_by_status($user_id, 'unread');
$feeds = Model\Feed\get_feeds_with_items_count($user_id);
// possible with remember me function
if ($nothing_to_read === 1 && $nb_unread_items > 0) { if ($nothing_to_read === 1 && $nb_unread_items > 0) {
Response\redirect('?action=unread'); Response\redirect('?action=unread');
} }
Response\html(Template\layout('feeds', array( Response\html(Template\layout('feeds', array(
'favicons' => Model\Favicon\get_all_favicons(), 'favicons' => Model\Favicon\get_feeds_favicons($feeds),
'feeds' => Model\Feed\get_all_item_counts(), 'feeds' => $feeds,
'nothing_to_read' => $nothing_to_read, 'nothing_to_read' => $nothing_to_read,
'nb_unread_items' => $nb_unread_items, 'nb_unread_items' => $nb_unread_items,
'nb_failed_feeds' => Model\Feed\count_failed_feeds(), 'nb_failed_feeds' => Model\Feed\count_failed_feeds($user_id),
'menu' => 'feeds', 'menu' => 'feeds',
'title' => t('Subscriptions') 'title' => t('Subscriptions')
))); )));
@ -138,21 +144,21 @@ Router\get_action('feeds', function () {
// Display form to add one feed // Display form to add one feed
Router\get_action('add', function () { Router\get_action('add', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$values = array( $values = array(
'download_content' => 0, 'download_content' => 0,
'rtl' => 0, 'rtl' => 0,
'cloak_referrer' => 0, 'cloak_referrer' => 0,
'create_group' => '', 'create_group' => '',
'feed_group_ids' => array() 'feed_group_ids' => array(),
); );
Response\html(Template\layout('add', array( Response\html(Template\layout('add', array(
'values' => $values + array('csrf' => Helper\generate_csrf()), 'values' => $values + array('csrf' => Helper\generate_csrf()),
'errors' => array(), 'errors' => array(),
'nb_unread_items' => Model\Item\count_by_status('unread'), 'groups' => Model\Group\get_all($user_id),
'groups' => Model\Group\get_all(),
'menu' => 'feeds', 'menu' => 'feeds',
'title' => t('New subscription') 'title' => t('New subscription'),
))); )));
}); });
@ -162,14 +168,18 @@ Router\action('subscribe', function () {
$values = Request\values(); $values = Request\values();
Helper\check_csrf_values($values); Helper\check_csrf_values($values);
$url = isset($values['url']) ? $values['url'] : ''; $url = isset($values['url']) ? $values['url'] : '';
$user_id = SessionStorage::getInstance()->getUserId();
} else { } else {
$values = array();
$url = Request\param('url'); $url = Request\param('url');
$token = Request\param('token'); $token = Request\param('token');
$user = Model\User\get_user_by_token('bookmarklet_token', $token);
$values = array();
if ($token !== Model\Config\get('bookmarklet_token')) { if (empty($user)) {
Response\text('Access Forbidden', 403); Response\text('Unauthorized', 401);
} }
$user_id = $user['id'];
} }
$values += array( $values += array(
@ -177,85 +187,30 @@ Router\action('subscribe', function () {
'download_content' => 0, 'download_content' => 0,
'rtl' => 0, 'rtl' => 0,
'cloak_referrer' => 0, 'cloak_referrer' => 0,
'create_group' => '', 'feed_group_ids' => array(),
'feed_group_ids' => array()
); );
try { list($feed_id, $error_message) = Handler\Feed\create_feed(
$feed_id = Model\Feed\create( $user_id,
$values['url'], $values['url'],
$values['download_content'], $values['download_content'],
$values['rtl'], $values['rtl'],
$values['cloak_referrer'], $values['cloak_referrer'],
$values['feed_group_ids'], $values['feed_group_ids'],
$values['create_group'] $values['groups']
); );
} catch (UnexpectedValueException $e) {
$error_message = t('This subscription already exists.');
} catch (PicoFeed\Client\InvalidCertificateException $e) {
$error_message = t('Invalid SSL certificate.');
} catch (PicoFeed\Client\InvalidUrlException $e) {
$error_message = $e->getMessage();
} catch (PicoFeed\Client\MaxRedirectException $e) {
$error_message = t('Maximum number of HTTP redirections exceeded.');
} catch (PicoFeed\Client\MaxSizeException $e) {
$error_message = t('The content size exceeds to maximum allowed size.');
} catch (PicoFeed\Client\TimeoutException $e) {
$error_message = t('Connection timeout.');
} catch (PicoFeed\Parser\MalformedXmlException $e) {
$error_message = t('Feed is malformed.');
} catch (PicoFeed\Reader\SubscriptionNotFoundException $e) {
$error_message = t('Unable to find a subscription.');
} catch (PicoFeed\Reader\UnsupportedFeedFormatException $e) {
$error_message = t('Unable to detect the feed format.');
}
Model\Config\write_debug(); if ($feed_id >= 1) {
SessionStorage::getInstance()->setFlashMessage(t('Subscription added successfully.'));
if (isset($feed_id) && $feed_id !== false) {
Session\flash(t('Subscription added successfully.'));
Response\redirect('?action=feed-items&feed_id='.$feed_id); Response\redirect('?action=feed-items&feed_id='.$feed_id);
} else { } else {
if (! isset($error_message)) { SessionStorage::getInstance()->setFlashErrorMessage($error_message);
$error_message = t('Error occured.');
}
Session\flash_error($error_message);
} }
Response\html(Template\layout('add', array( Response\html(Template\layout('add', array(
'values' => $values + array('csrf' => Helper\generate_csrf()), 'values' => $values + array('csrf' => Helper\generate_csrf()),
'nb_unread_items' => Model\Item\count_by_status('unread'), 'groups' => Model\Group\get_all($user_id),
'groups' => Model\Group\get_all(),
'menu' => 'feeds', 'menu' => 'feeds',
'title' => t('Subscriptions') 'title' => t('Subscriptions'),
))); )));
}); });
// OPML export
Router\get_action('export', function () {
Response\force_download('feeds.opml');
Response\xml(Handler\Opml\export_all_feeds());
});
// OPML import form
Router\get_action('import', function () {
Response\html(Template\layout('import', array(
'errors' => array(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'feeds',
'title' => t('OPML Import')
)));
});
// OPML importation
Router\post_action('import', function () {
try {
Model\Feed\import_opml(Request\file_content('file'));
Session\flash(t('Your feeds have been imported.'));
Response\redirect('?action=feeds');
} catch (MalformedXmlException $e) {
Session\flash_error(t('Unable to import your OPML file.').' ('.$e->getMessage().')');
Response\redirect('?action=import');
}
});

26
app/controllers/help.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace Miniflux\Controller;
use Miniflux\Model;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Session\SessionStorage;
use Miniflux\Template;
// Display help page
Router\get_action('help', function () {
$user_id = SessionStorage::getInstance()->getUserId();
Response\html(Template\layout('help', array(
'config' => Model\Config\get_all($user_id),
'menu' => 'config',
'title' => t('Preferences')
)));
});
// Show help
Router\get_action('show-help', function () {
Response\html(Template\load('show_help'));
});

View File

@ -1,62 +1,30 @@
<?php <?php
namespace Miniflux\Controller;
use Miniflux\Router; use Miniflux\Router;
use Miniflux\Response; use Miniflux\Response;
use Miniflux\Request; use Miniflux\Request;
use Miniflux\Session\SessionStorage;
use Miniflux\Template; use Miniflux\Template;
use Miniflux\Helper;
use Miniflux\Model; use Miniflux\Model;
// Display history page // Display history page
Router\get_action('history', function () { Router\get_action('history', function () {
$order = Request\param('order', 'updated'); $params = items_list(Model\Item\STATUS_READ);
$direction = Request\param('direction', Model\Config\get('items_sorting_direction'));
$offset = Request\int_param('offset', 0);
$group_id = Request\int_param('group_id', null);
$feed_ids = array();
if ($group_id !== null) { Response\html(Template\layout('history', $params + array(
$feed_ids = Model\Group\get_feeds_by_group($group_id); 'title' => t('History') . ' (' . $params['nb_items'] . ')',
}
$items = Model\Item\get_all_by_status(
'read',
$feed_ids,
$offset,
Model\Config\get('items_per_page'),
$order,
$direction
);
$nb_items = Model\Item\count_by_status('read', $feed_ids);
Response\html(Template\layout('history', array(
'favicons' => Model\Favicon\get_item_favicons($items),
'original_marks_read' => Model\Config\get('original_marks_read'),
'items' => $items,
'order' => $order,
'direction' => $direction,
'display_mode' => Model\Config\get('items_display_mode'),
'item_title_link' => Model\Config\get('item_title_link'),
'group_id' => $group_id,
'nb_items' => $nb_items,
'nb_unread_items' => Model\Item\count_by_status('unread'),
'offset' => $offset,
'items_per_page' => Model\Config\get('items_per_page'),
'nothing_to_read' => Request\int_param('nothing_to_read'),
'menu' => 'history', 'menu' => 'history',
'groups' => Model\Group\get_all(),
'title' => t('History').' ('.$nb_items.')'
))); )));
}); });
// Confirmation box to flush history // Confirmation box to flush history
Router\get_action('confirm-flush-history', function () { Router\get_action('confirm-flush-history', function () {
$group_id = Request\int_param('group_id', null); $group_id = Request\int_param('group_id');
Response\html(Template\layout('confirm_flush_items', array( Response\html(Template\layout('confirm_flush_items', array(
'group_id' => $group_id, 'group_id' => $group_id,
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'history', 'menu' => 'history',
'title' => t('Confirmation') 'title' => t('Confirmation')
))); )));
@ -64,12 +32,13 @@ Router\get_action('confirm-flush-history', function () {
// Flush history // Flush history
Router\get_action('flush-history', function () { Router\get_action('flush-history', function () {
$group_id = Request\int_param('group_id', null); $user_id = SessionStorage::getInstance()->getUserId();
$group_id = Request\int_param('group_id');
if ($group_id !== null) { if ($group_id !== 0) {
Model\ItemGroup\mark_all_as_removed($group_id); Model\ItemGroup\change_items_status($user_id, $group_id, Model\Item\STATUS_READ, Model\Item\STATUS_REMOVED);
} else { } else {
Model\Item\mark_all_as_removed(); Model\Item\change_items_status($user_id, Model\Item\STATUS_READ, Model\Item\STATUS_REMOVED);
} }
Response\redirect('?action=history'); Response\redirect('?action=history');

View File

@ -1,101 +1,73 @@
<?php <?php
namespace Miniflux\Controller;
use Miniflux\Helper;
use Miniflux\Router; use Miniflux\Router;
use Miniflux\Response; use Miniflux\Response;
use Miniflux\Request; use Miniflux\Request;
use Miniflux\Session\SessionStorage;
use Miniflux\Template; use Miniflux\Template;
use Miniflux\Helper;
use Miniflux\Handler; use Miniflux\Handler;
use Miniflux\Model; use Miniflux\Model;
// Display unread items // Display unread items
Router\get_action('unread', function () { Router\get_action('unread', function () {
Model\Item\autoflush_read(); $user_id = SessionStorage::getInstance()->getUserId();
Model\Item\autoflush_unread();
$order = Request\param('order', 'updated'); Model\Item\autoflush_read($user_id);
$direction = Request\param('direction', Model\Config\get('items_sorting_direction')); Model\Item\autoflush_unread($user_id);
$offset = Request\int_param('offset', 0);
$group_id = Request\int_param('group_id', null);
$feed_ids = array();
if ($group_id !== null) { $params = items_list(Model\Item\STATUS_UNREAD);
$feed_ids = Model\Group\get_feeds_by_group($group_id);
}
$items = Model\Item\get_all_by_status( if ($params['nb_unread_items'] === 0) {
'unread', $action = Helper\config('redirect_nothing_to_read', 'feeds');
$feed_ids,
$offset,
Model\Config\get('items_per_page'),
$order,
$direction
);
$nb_items = Model\Item\count_by_status('unread', $feed_ids);
$nb_unread_items = Model\Item\count_by_status('unread');
if ($nb_unread_items === 0) {
$action = Model\Config\get('redirect_nothing_to_read');
Response\redirect('?action='.$action.'&nothing_to_read=1'); Response\redirect('?action='.$action.'&nothing_to_read=1');
} }
Response\html(Template\layout('unread_items', array( Response\html(Template\layout('unread_items', $params + array(
'favicons' => Model\Favicon\get_item_favicons($items), 'title' => 'Miniflux (' . $params['nb_items'] . ')',
'original_marks_read' => Model\Config\get('original_marks_read'),
'order' => $order,
'direction' => $direction,
'display_mode' => Model\Config\get('items_display_mode'),
'item_title_link' => Model\Config\get('item_title_link'),
'group_id' => $group_id,
'items' => $items,
'nb_items' => $nb_items,
'nb_unread_items' => $nb_unread_items,
'offset' => $offset,
'items_per_page' => Model\Config\get('items_per_page'),
'title' => 'Miniflux ('.$nb_items.')',
'menu' => 'unread', 'menu' => 'unread',
'groups' => Model\Group\get_all()
))); )));
}); });
// Show item // Show item
Router\get_action('show', function () { Router\get_action('show', function () {
$id = Request\param('id'); $user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
$menu = Request\param('menu'); $menu = Request\param('menu');
$item = Model\Item\get($id); $item = Model\Item\get_item($user_id, $item_id);
$feed = Model\Feed\get($item['feed_id']); $feed = Model\Feed\get_feed($user_id, $item['feed_id']);
$group_id = Request\int_param('group_id', null); $group_id = Request\int_param('group_id', null);
Model\Item\set_read($id); Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_READ);
$item['status'] = 'read'; $item['status'] = 'read';
switch ($menu) { switch ($menu) {
case 'unread': case 'unread':
$nav = Model\Item\get_nav($item, array('unread'), array(1, 0), null, $group_id); $nav = Model\Item\get_item_nav($user_id, $item, array('unread'), array(1, 0), null, $group_id);
break; break;
case 'history': case 'history':
$nav = Model\Item\get_nav($item, array('read')); $nav = Model\Item\get_item_nav($user_id, $item, array('read'));
break; break;
case 'feed-items': case 'feed-items':
$nav = Model\Item\get_nav($item, array('unread', 'read'), array(1, 0), $item['feed_id']); $nav = Model\Item\get_item_nav($user_id, $item, array('unread', 'read'), array(1, 0), $item['feed_id']);
break; break;
case 'bookmarks': case 'bookmarks':
$nav = Model\Item\get_nav($item, array('unread', 'read'), array(1)); $nav = Model\Item\get_item_nav($user_id, $item, array('unread', 'read'), array(1));
break; break;
} }
$image_proxy = (bool) Model\Config\get('image_proxy'); $image_proxy = (bool) Helper\config('image_proxy');
// add the image proxy if requested and required // add the image proxy if requested and required
$item['content'] = Handler\Proxy\rewrite_html($item['content'], $item['url'], $image_proxy, $feed['cloak_referrer']); $item['content'] = Handler\Proxy\rewrite_html($item['content'], $item['url'], $image_proxy, $feed['cloak_referrer']);
if ($image_proxy && strpos($item['enclosure_type'], 'image') === 0) { if ($image_proxy && strpos($item['enclosure_type'], 'image') === 0) {
$item['enclosure'] = Handler\Proxy\rewrite_link($item['enclosure']); $item['enclosure_url'] = Handler\Proxy\rewrite_link($item['enclosure_url']);
} }
Response\html(Template\layout('show_item', array( Response\html(Template\layout('show_item', array(
'nb_unread_items' => Model\Item\count_by_status('unread'),
'item' => $item, 'item' => $item,
'feed' => $feed, 'feed' => $feed,
'item_nav' => isset($nav) ? $nav : null, 'item_nav' => isset($nav) ? $nav : null,
@ -107,27 +79,27 @@ Router\get_action('show', function () {
// Display feed items page // Display feed items page
Router\get_action('feed-items', function () { Router\get_action('feed-items', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$feed_id = Request\int_param('feed_id', 0); $feed_id = Request\int_param('feed_id', 0);
$offset = Request\int_param('offset', 0); $offset = Request\int_param('offset', 0);
$nb_items = Model\ItemFeed\count_items($feed_id); $feed = Model\Feed\get_feed($user_id, $feed_id);
$feed = Model\Feed\get($feed_id);
$order = Request\param('order', 'updated'); $order = Request\param('order', 'updated');
$direction = Request\param('direction', Model\Config\get('items_sorting_direction')); $direction = Request\param('direction', Helper\config('items_sorting_direction'));
$items = Model\ItemFeed\get_all_items($feed_id, $offset, Model\Config\get('items_per_page'), $order, $direction); $items = Model\ItemFeed\get_all_items($user_id, $feed_id, $offset, Helper\config('items_per_page'), $order, $direction);
$nb_items = Model\ItemFeed\count_items($user_id, $feed_id);
Response\html(Template\layout('feed_items', array( Response\html(Template\layout('feed_items', array(
'favicons' => Model\Favicon\get_favicons(array($feed['id'])), 'favicons' => Model\Favicon\get_favicons_by_feed_ids(array($feed['id'])),
'original_marks_read' => Model\Config\get('original_marks_read'), 'original_marks_read' => Helper\config('original_marks_read'),
'order' => $order, 'order' => $order,
'direction' => $direction, 'direction' => $direction,
'display_mode' => Model\Config\get('items_display_mode'), 'display_mode' => Helper\config('items_display_mode'),
'feed' => $feed, 'feed' => $feed,
'items' => $items, 'items' => $items,
'nb_items' => $nb_items, 'nb_items' => $nb_items,
'nb_unread_items' => Model\Item\count_by_status('unread'),
'offset' => $offset, 'offset' => $offset,
'items_per_page' => Model\Config\get('items_per_page'), 'items_per_page' => Helper\config('items_per_page'),
'item_title_link' => Model\Config\get('item_title_link'), 'item_title_link' => Helper\config('item_title_link'),
'menu' => 'feed-items', 'menu' => 'feed-items',
'title' => '('.$nb_items.') '.$feed['title'] 'title' => '('.$nb_items.') '.$feed['title']
))); )));
@ -135,43 +107,56 @@ Router\get_action('feed-items', function () {
// Ajax call to download an item (fetch the full content from the original website) // Ajax call to download an item (fetch the full content from the original website)
Router\post_action('download-item', function () { Router\post_action('download-item', function () {
$id = Request\param('id'); $user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
$item = Model\Item\get($id); $item = Model\Item\get_item($user_id, $item_id);
$feed = Model\Feed\get($item['feed_id']); $feed = Model\Feed\get_feed($user_id, $item['feed_id']);
$download = Model\Item\download_contents($id); $download = Handler\Item\download_item_content($user_id, $item_id);
$download['content'] = Handler\Proxy\rewrite_html($download['content'], $item['url'], Model\Config\get('image_proxy'), $feed['cloak_referrer']); $download['content'] = Handler\Proxy\rewrite_html(
$download['content'],
$item['url'],
Helper\bool_config('image_proxy'),
(bool) $feed['cloak_referrer']
);
Response\json($download); Response\json($download);
}); });
// Ajax call to mark item read // Ajax call to mark item read
Router\post_action('mark-item-read', function () { Router\post_action('mark-item-read', function () {
Model\Item\set_read(Request\param('id')); $user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_READ);
Response\json(array('Ok')); Response\json(array('Ok'));
}); });
// Ajax call to mark item as removed // Ajax call to mark item as removed
Router\post_action('mark-item-removed', function () { Router\post_action('mark-item-removed', function () {
Model\Item\set_removed(Request\param('id')); $user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_REMOVED);
Response\json(array('Ok')); Response\json(array('Ok'));
}); });
// Ajax call to mark item unread // Ajax call to mark item unread
Router\post_action('mark-item-unread', function () { Router\post_action('mark-item-unread', function () {
Model\Item\set_unread(Request\param('id')); $user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_UNREAD);
Response\json(array('Ok')); Response\json(array('Ok'));
}); });
// Mark unread items as read // Mark unread items as read
Router\get_action('mark-all-read', function () { Router\get_action('mark-all-read', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$group_id = Request\int_param('group_id', null); $group_id = Request\int_param('group_id', null);
if ($group_id !== null) { if ($group_id !== null) {
Model\ItemGroup\mark_all_as_read($group_id); Model\ItemGroup\change_items_status($user_id, $group_id, Model\Item\STATUS_UNREAD, Model\Item\STATUS_READ);
} else { } else {
Model\Item\mark_all_as_read(); Model\Item\change_items_status($user_id, Model\Item\STATUS_UNREAD, Model\Item\STATUS_READ);
} }
Response\redirect('?action=unread'); Response\redirect('?action=unread');
@ -179,9 +164,11 @@ Router\get_action('mark-all-read', function () {
// Mark all unread items as read for a specific feed // Mark all unread items as read for a specific feed
Router\get_action('mark-feed-as-read', function () { Router\get_action('mark-feed-as-read', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$feed_id = Request\int_param('feed_id'); $feed_id = Request\int_param('feed_id');
Model\ItemFeed\mark_all_as_read($feed_id); Model\ItemFeed\change_items_status($user_id, $feed_id, Model\Item\STATUS_UNREAD, Model\Item\STATUS_READ);
Response\redirect('?action=feed-items&feed_id='.$feed_id); Response\redirect('?action=feed-items&feed_id='.$feed_id);
}); });
@ -190,48 +177,55 @@ Router\get_action('mark-feed-as-read', function () {
// that where marked read from the frontend, since the number of unread items // that where marked read from the frontend, since the number of unread items
// on page 2+ is unknown. // on page 2+ is unknown.
Router\post_action('mark-feed-as-read', function () { Router\post_action('mark-feed-as-read', function () {
Model\ItemFeed\mark_all_as_read(Request\int_param('feed_id')); $user_id = SessionStorage::getInstance()->getUserId();
$nb_items = Model\Item\count_by_status('unread'); $feed_id = Request\int_param('feed_id');
Model\ItemFeed\change_items_status($user_id, $feed_id, Model\Item\STATUS_UNREAD, Model\Item\STATUS_READ);
$nb_items = Model\Item\count_by_status($user_id, Model\Item\STATUS_READ);
Response\raw($nb_items); Response\raw($nb_items);
}); });
// Mark item as read and redirect to the listing page // Mark item as read and redirect to the listing page
Router\get_action('mark-item-read', function () { Router\get_action('mark-item-read', function () {
$id = Request\param('id'); $user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
$redirect = Request\param('redirect', 'unread'); $redirect = Request\param('redirect', 'unread');
$offset = Request\int_param('offset', 0); $offset = Request\int_param('offset', 0);
$feed_id = Request\int_param('feed_id', 0); $feed_id = Request\int_param('feed_id', 0);
Model\Item\set_read($id); Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_READ);
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id); Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$item_id);
}); });
// Mark item as unread and redirect to the listing page // Mark item as unread and redirect to the listing page
Router\get_action('mark-item-unread', function () { Router\get_action('mark-item-unread', function () {
$id = Request\param('id'); $user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
$redirect = Request\param('redirect', 'history'); $redirect = Request\param('redirect', 'history');
$offset = Request\int_param('offset', 0); $offset = Request\int_param('offset', 0);
$feed_id = Request\int_param('feed_id', 0); $feed_id = Request\int_param('feed_id', 0);
Model\Item\set_unread($id); Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_UNREAD);
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id); Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$item_id);
}); });
// Mark item as removed and redirect to the listing page // Mark item as removed and redirect to the listing page
Router\get_action('mark-item-removed', function () { Router\get_action('mark-item-removed', function () {
$id = Request\param('id'); $user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
$redirect = Request\param('redirect', 'history'); $redirect = Request\param('redirect', 'history');
$offset = Request\int_param('offset', 0); $offset = Request\int_param('offset', 0);
$feed_id = Request\int_param('feed_id', 0); $feed_id = Request\int_param('feed_id', 0);
Model\Item\set_removed($id); Model\Item\change_item_status($user_id, $item_id, Model\Item\STATUS_REMOVED);
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id); Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id);
}); });
Router\post_action('latest-feeds-items', function () { Router\post_action('latest-feeds-items', function () {
$items = Model\Item\get_latest_feeds_items(); $user_id = SessionStorage::getInstance()->getUserId();
$nb_unread_items = Model\Item\count_by_status('unread'); $items = Model\Item\get_latest_feeds_items($user_id);
$nb_unread_items = Model\Item\count_by_status($user_id, 'unread');
$feeds = array_reduce($items, function ($result, $item) { $feeds = array_reduce($items, function ($result, $item) {
$result[$item['id']] = array( $result[$item['id']] = array(

40
app/controllers/opml.php Normal file
View File

@ -0,0 +1,40 @@
<?php
namespace Miniflux\Controller;
use Exception;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Request;
use Miniflux\Session\SessionStorage;
use Miniflux\Template;
use Miniflux\Handler;
// OPML export
Router\get_action('export', function () {
$user_id = SessionStorage::getInstance()->getUserId();
Response\force_download('feeds.opml');
Response\xml(Handler\Opml\export_all_feeds($user_id));
});
// OPML import form
Router\get_action('import', function () {
Response\html(Template\layout('import', array(
'errors' => array(),
'menu' => 'feeds',
'title' => t('OPML Import')
)));
});
// OPML importation
Router\post_action('import', function () {
try {
$user_id = SessionStorage::getInstance()->getUserId();
Handler\Opml\import_opml($user_id, Request\file_content('file'));
SessionStorage::getInstance()->setFlashMessage(t('Your feeds have been imported.'));
Response\redirect('?action=feeds');
} catch (Exception $e) {
SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to import your OPML file.').' ('.$e->getMessage().')');
Response\redirect('?action=import');
}
});

View File

@ -0,0 +1,48 @@
<?php
namespace Miniflux\Controller;
use Miniflux\Model;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Request;
use Miniflux\Session\SessionStorage;
use Miniflux\Template;
use Miniflux\Helper;
use Miniflux\Validator;
Router\get_action('profile', function () {
$user_id = SessionStorage::getInstance()->getUserId();
Response\html(Template\layout('profile', array(
'errors' => array(),
'values' => Model\User\get_user_by_id_without_password($user_id) + array('csrf' => Helper\generate_csrf()),
'menu' => 'config',
'title' => t('User Profile')
)));
});
Router\post_action('profile', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$values = Request\values();
Helper\check_csrf_values($values);
list($valid, $errors) = Validator\User\validate_modification($values);
if ($valid) {
$new_password = empty($values['password']) ? null : $values['password'];
if (Model\User\update_user($user_id, $values['username'], $new_password)) {
SessionStorage::getInstance()->setFlashMessage(t('Your preferences are updated.'));
} else {
SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to update your preferences.'));
}
Response\redirect('?action=profile');
}
Response\html(Template\layout('profile', array(
'errors' => $errors,
'values' => Model\User\get_user_by_id_without_password($user_id) + array('csrf' => Helper\generate_csrf()),
'menu' => 'config',
'title' => t('User Profile')
)));
});

View File

@ -1,39 +1,40 @@
<?php <?php
namespace Miniflux\Controller;
use Miniflux\Helper;
use Miniflux\Router; use Miniflux\Router;
use Miniflux\Response; use Miniflux\Response;
use Miniflux\Request; use Miniflux\Request;
use Miniflux\Session\SessionStorage;
use Miniflux\Template; use Miniflux\Template;
use Miniflux\Helper;
use Miniflux\Model; use Miniflux\Model;
// Display search results page
Router\get_action('search', function() { Router\get_action('search', function() {
$user_id = SessionStorage::getInstance()->getUserId();
$text = Request\param('text', ''); $text = Request\param('text', '');
$offset = Request\int_param('offset', 0); $offset = Request\int_param('offset', 0);
$items = array(); $items = array();
$nb_items = 0; $nb_items = 0;
if ($text) { if ($text) {
$items = Model\Search\get_all_items($text, $offset, Model\Config\get('items_per_page')); $items = Model\ItemSearch\get_all_items($user_id, $text, $offset, Helper\config('items_per_page'));
$nb_items = Model\Search\count_items($text); $nb_items = Model\ItemSearch\count_items($user_id, $text);
} }
Response\html(Template\layout('search', array( Response\html(Template\layout('search', array(
'favicons' => Model\Favicon\get_item_favicons($items), 'favicons' => Model\Favicon\get_items_favicons($items),
'original_marks_read' => Model\Config\get('original_marks_read'), 'original_marks_read' => Helper\config('original_marks_read'),
'text' => $text, 'text' => $text,
'items' => $items, 'items' => $items,
'order' => '', 'order' => '',
'direction' => '', 'direction' => '',
'display_mode' => Model\Config\get('items_display_mode'), 'display_mode' => Helper\config('items_display_mode'),
'item_title_link' => Model\Config\get('item_title_link'), 'item_title_link' => Helper\config('item_title_link'),
'group_id' => array(), 'group_id' => array(),
'nb_items' => $nb_items, 'nb_items' => $nb_items,
'nb_unread_items' => Model\Item\count_by_status('unread'),
'offset' => $offset, 'offset' => $offset,
'items_per_page' => Model\Config\get('items_per_page'), 'items_per_page' => Helper\config('items_per_page'),
'nothing_to_read' => Request\int_param('nothing_to_read'), 'nothing_to_read' => Request\int_param('nothing_to_read'),
'menu' => 'search', 'menu' => 'search',
'title' => t('Search').' ('.$nb_items.')' 'title' => t('Search').' ('.$nb_items.')'

View File

@ -0,0 +1,38 @@
<?php
namespace Miniflux\Controller;
use Miniflux\Model;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Request;
use Miniflux\Session\SessionStorage;
use Miniflux\Template;
use Miniflux\Helper;
// Display bookmark services page
Router\get_action('services', function () {
$user_id = SessionStorage::getInstance()->getUserId();
Response\html(Template\layout('services', array(
'errors' => array(),
'values' => Model\Config\get_all($user_id) + array('csrf' => Helper\generate_csrf()),
'menu' => 'config',
'title' => t('Preferences')
)));
});
// Update bookmark services
Router\post_action('services', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$values = Request\values() + array('pinboard_enabled' => 0, 'instapaper_enabled' => 0, 'wallabag_enabled' => 0);
Helper\check_csrf_values($values);
if (Model\Config\save($user_id, $values)) {
SessionStorage::getInstance()->setFlashMessage(t('Your preferences are updated.'));
} else {
SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to update your preferences.'));
}
Response\redirect('?action=services');
});

134
app/controllers/users.php Normal file
View File

@ -0,0 +1,134 @@
<?php
namespace Miniflux\Controller;
use Miniflux\Model;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Request;
use Miniflux\Session\SessionStorage;
use Miniflux\Template;
use Miniflux\Helper;
use Miniflux\Validator;
Router\get_action('users', function () {
if (! SessionStorage::getInstance()->isAdmin()) {
Response\text('Access Forbidden', 403);
}
Response\html(Template\layout('users', array(
'users' => Model\User\get_all_users(),
'menu' => 'config',
'title' => t('Users'),
)));
});
Router\get_action('new-user', function () {
if (! SessionStorage::getInstance()->isAdmin()) {
Response\text('Access Forbidden', 403);
}
Response\html(Template\layout('new_user', array(
'values' => array('csrf' => Helper\generate_csrf()),
'errors' => array(),
'menu' => 'config',
'title' => t('New User'),
)));
});
Router\post_action('new-user', function () {
if (! SessionStorage::getInstance()->isAdmin()) {
Response\text('Access Forbidden', 403);
}
$values = Request\values() + array('is_admin' => 0);
Helper\check_csrf_values($values);
list($valid, $errors) = Validator\User\validate_creation($values);
if ($valid) {
if (Model\User\create_user($values['username'], $values['password'], (bool) $values['is_admin'])) {
SessionStorage::getInstance()->setFlashMessage(t('New user created successfully.'));
} else {
SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to create this user.'));
}
Response\redirect('?action=users');
}
Response\html(Template\layout('new_user', array(
'values' => $values + array('csrf' => Helper\generate_csrf()),
'errors' => $errors,
'menu' => 'config',
'title' => t('New User'),
)));
});
Router\get_action('edit-user', function () {
if (! SessionStorage::getInstance()->isAdmin()) {
Response\text('Access Forbidden', 403);
}
$user = Model\User\get_user_by_id_without_password(Request\int_param('user_id'));
if (empty($user)) {
Response\redirect('?action=users');
}
Response\html(Template\layout('edit_user', array(
'values' => $user + array('csrf' => Helper\generate_csrf()),
'errors' => array(),
'menu' => 'config',
'title' => t('Edit User'),
)));
});
Router\post_action('edit-user', function () {
if (! SessionStorage::getInstance()->isAdmin()) {
Response\text('Access Forbidden', 403);
}
$values = Request\values() + array('is_admin' => 0);
Helper\check_csrf_values($values);
list($valid, $errors) = Validator\User\validate_modification($values);
if ($valid) {
$new_password = empty($values['password']) ? null : $values['password'];
$is_admin = $values['is_admin'] == 1 ? 1 : 0;
if (Model\User\update_user($values['id'], $values['username'], $new_password, $is_admin)) {
SessionStorage::getInstance()->setFlashMessage(t('User modified successfully.'));
} else {
SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to edit this user.'));
}
Response\redirect('?action=users');
}
Response\html(Template\layout('edit_user', array(
'values' => $values + array('csrf' => Helper\generate_csrf()),
'errors' => $errors,
'menu' => 'config',
'title' => t('Edit User'),
)));
});
Router\get_action('confirm-remove-user', function () {
if (! SessionStorage::getInstance()->isAdmin()) {
Response\text('Access Forbidden', 403);
}
Response\html(Template\layout('confirm_remove_user', array(
'user' => Model\User\get_user_by_id_without_password(Request\int_param('user_id')),
'csrf_token' => Helper\generate_csrf(),
'menu' => 'config',
'title' => t('Remove User'),
)));
});
Router\get_action('remove-user', function () {
if (! SessionStorage::getInstance()->isAdmin() || ! Helper\check_csrf(Request\param('csrf'))) {
Response\text('Access Forbidden', 403);
}
Model\User\remove_user(Request\int_param('user_id'));
Response\redirect('?action=users');
});

View File

@ -2,20 +2,24 @@
namespace Miniflux\Session; namespace Miniflux\Session;
const SESSION_LIFETIME = 2678400; use Miniflux\Helper;
function open($base_path = '/', $save_path = '', $session_lifetime = SESSION_LIFETIME) class SessionManager
{ {
const SESSION_LIFETIME = 2678400;
public static function open($base_path = '/', $save_path = '', $duration = self::SESSION_LIFETIME)
{
if ($save_path !== '') { if ($save_path !== '') {
session_save_path($save_path); session_save_path($save_path);
} }
// HttpOnly and secure flags for session cookie // HttpOnly and secure flags for session cookie
session_set_cookie_params( session_set_cookie_params(
$session_lifetime, $duration,
$base_path ?: '/', $base_path ?: '/',
null, null,
isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', Helper\is_secure_connection(),
true true
); );
@ -28,7 +32,7 @@ function open($base_path = '/', $save_path = '', $session_lifetime = SESSION_LIF
ini_set('session.hash_bits_per_character', 6); ini_set('session.hash_bits_per_character', 6);
// Custom session name // Custom session name
session_name('__$'); session_name('MX_SID');
session_start(); session_start();
@ -37,19 +41,116 @@ function open($base_path = '/', $save_path = '', $session_lifetime = SESSION_LIF
session_regenerate_id(true); session_regenerate_id(true);
$_SESSION['__validated'] = 1; $_SESSION['__validated'] = 1;
} }
} }
function close() public static function close()
{ {
session_destroy(); session_destroy();
}
} }
function flash($message)
class SessionStorage
{ {
private static $instance = null;
public function __construct(array $session = null)
{
if (! isset($_SESSION)) {
$_SESSION = array();
}
$_SESSION = $session ?: $_SESSION;
}
public static function getInstance(array $session = null)
{
if (self::$instance === null) {
self::$instance = new static($session);
}
return self::$instance;
}
public function flush()
{
$_SESSION = array();
return $this;
}
public function flushConfig()
{
unset($_SESSION['config']);
return $this;
}
public function setConfig(array $config)
{
$_SESSION['config'] = $config;
return $this;
}
public function getConfig()
{
return $this->getValue('config');
}
public function setUser(array $user)
{
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['is_admin'] = (bool) $user['is_admin'];
return $this;
}
public function getUserId()
{
return $this->getValue('user_id');
}
public function getUsername()
{
return $this->getValue('username');
}
public function isAdmin()
{
return $this->getValue('is_admin');
}
public function isLogged()
{
return $this->getValue('user_id') !== null;
}
public function setFlashMessage($message)
{
$_SESSION['flash_message'] = $message; $_SESSION['flash_message'] = $message;
} return $this;
}
function flash_error($message) public function setFlashErrorMessage($message)
{ {
$_SESSION['flash_error_message'] = $message; $_SESSION['flash_error_message'] = $message;
return $this;
}
public function getFlashMessage()
{
$message = $this->getValue('flash_message');
unset($_SESSION['flash_message']);
return $message;
}
public function getFlashErrorMessage()
{
$message = $this->getValue('flash_error_message');
unset($_SESSION['flash_error_message']);
return $message;
}
protected function getValue($key)
{
return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
}
} }

View File

@ -2,6 +2,8 @@
namespace Miniflux\Template; namespace Miniflux\Template;
use Miniflux\Model;
const PATH = 'app/templates/'; const PATH = 'app/templates/';
// Template\load('template_name', ['bla' => 'value']); // Template\load('template_name', ['bla' => 'value']);
@ -30,5 +32,8 @@ function load()
function layout($template_name, array $template_args = array(), $layout_name = 'layout') function layout($template_name, array $template_args = array(), $layout_name = 'layout')
{ {
return load($layout_name, $template_args + array('content_for_layout' => load($template_name, $template_args))); return load(
$layout_name,
$template_args + array('content_for_layout' => load($template_name, $template_args))
);
} }

View File

@ -24,3 +24,14 @@ function dt()
{ {
return call_user_func_array('\Miniflux\Translator\datetime', func_get_args()); return call_user_func_array('\Miniflux\Translator\datetime', func_get_args());
} }
function get_cli_option($option, array $options)
{
$value = null;
if (! empty($options[$option]) && ctype_digit($options[$option])) {
$value = (int) $options[$option];
}
return $value;
}

161
app/handlers/feed.php Normal file
View File

@ -0,0 +1,161 @@
<?php
namespace Miniflux\Handler\Feed;
use Miniflux\Helper;
use Miniflux\Model;
use PicoFeed;
use PicoFeed\Config\Config as ReaderConfig;
use PicoFeed\Logging\Logger;
use PicoFeed\Reader\Reader;
function fetch_feed($url, $download_content = false, $etag = '', $last_modified = '')
{
$error_message = '';
$feed = null;
$resource = null;
try {
$reader = new Reader(get_reader_config());
$resource = $reader->discover($url, $last_modified, $etag);
if ($resource->isModified()) {
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
if ($download_content) {
$parser->enableContentGrabber();
}
$feed = $parser->execute();
}
} catch (PicoFeed\Client\InvalidCertificateException $e) {
$error_message = t('Invalid SSL certificate.');
} catch (PicoFeed\Client\InvalidUrlException $e) {
$error_message = $e->getMessage();
} catch (PicoFeed\Client\MaxRedirectException $e) {
$error_message = t('Maximum number of HTTP redirection exceeded.');
} catch (PicoFeed\Client\MaxSizeException $e) {
$error_message = t('The content size exceeds to maximum allowed size.');
} catch (PicoFeed\Client\TimeoutException $e) {
$error_message = t('Connection timeout.');
} catch (PicoFeed\Parser\MalformedXmlException $e) {
$error_message = t('Feed is malformed.');
} catch (PicoFeed\Reader\SubscriptionNotFoundException $e) {
$error_message = t('Unable to find a subscription.');
} catch (PicoFeed\Reader\UnsupportedFeedFormatException $e) {
$error_message = t('Unable to detect the feed format.');
}
return array($feed, $resource, $error_message);
}
function create_feed($user_id, $url, $download_content = false, $rtl = false, $cloak_referrer = false, array $feed_group_ids = array(), $group_name = null)
{
$feed_id = null;
list($feed, $resource, $error_message) = fetch_feed($url, $download_content);
if ($feed !== null) {
$feed_id = Model\Feed\create(
$user_id,
$feed,
$resource->getEtag(),
$resource->getLastModified(),
$rtl,
$download_content,
$cloak_referrer
);
if ($feed_id === -1) {
$error_message = t('This subscription already exists.');
} else if ($feed_id === false) {
$error_message = t('Unable to save this subscription in the database.');
} else {
Model\Favicon\create_feed_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
if (! empty($feed_group_ids)) {
Model\Group\update_feed_groups($user_id, $feed_id, $feed_group_ids, $group_name);
}
}
}
return array($feed_id, $error_message);
}
function update_feed($user_id, $feed_id)
{
$subscription = Model\Feed\get_feed($user_id, $feed_id);
list($feed, $resource, $error_message) = fetch_feed(
$subscription['feed_url'],
(bool) $subscription['download_content'],
$subscription['etag'],
$subscription['last_modified']
);
if (! empty($error_message)) {
Model\Feed\update_feed($user_id, $feed_id, array(
'last_checked' => time(),
'parsing_error' => 1,
));
return false;
} else {
Model\Feed\update_feed($user_id, $feed_id, array(
'etag' => $resource->getEtag(),
'last_modified' => $resource->getLastModified(),
'last_checked' => time(),
'parsing_error' => 0,
));
}
if ($feed !== null) {
Model\Item\update_feed_items($user_id, $feed_id, $feed->getItems(), $subscription['rtl']);
Model\Favicon\create_feed_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
}
return true;
}
function update_feeds($user_id, $limit = null)
{
foreach (Model\Feed\get_feed_ids($user_id, $limit) as $feed_id) {
update_feed($user_id, $feed_id);
}
}
function get_reader_config()
{
$config = new ReaderConfig;
$config->setTimezone(Helper\config('timezone'));
// Client
$config->setClientTimeout(HTTP_TIMEOUT);
$config->setClientUserAgent(HTTP_USER_AGENT);
$config->setMaxBodySize(HTTP_MAX_RESPONSE_SIZE);
// Grabber
$config->setGrabberRulesFolder(RULES_DIRECTORY);
// Proxy
$config->setProxyHostname(PROXY_HOSTNAME);
$config->setProxyPort(PROXY_PORT);
$config->setProxyUsername(PROXY_USERNAME);
$config->setProxyPassword(PROXY_PASSWORD);
// Filter
$config->setFilterIframeWhitelist(Model\Config\get_iframe_whitelist());
// Parser
$config->setParserHashAlgo('crc32b');
if (DEBUG_MODE) {
Logger::enable();
}
return $config;
}

33
app/handlers/item.php Normal file
View File

@ -0,0 +1,33 @@
<?php
namespace Miniflux\Handler\Item;
use Miniflux\Handler;
use Miniflux\Helper;
use Miniflux\Model;
use PicoDb\Database;
function download_item_content($user_id, $item_id)
{
$item = Model\Item\get_item($user_id, $item_id);
$content = Handler\Scraper\download_content($item['url']);
if (! empty($content)) {
if (! Helper\config('nocontent')) {
Database::getInstance('db')
->table('items')
->eq('id', $item['id'])
->save(array('content' => $content));
}
return array(
'result' => true,
'content' => $content
);
}
return array(
'result' => false,
'content' => ''
);
}

View File

@ -2,20 +2,20 @@
namespace Miniflux\Handler\Opml; namespace Miniflux\Handler\Opml;
use Miniflux\Model\Feed; use Miniflux\Model;
use Miniflux\Model\Group; use PicoDb\Database;
use PicoFeed\Serialization\Subscription; use PicoFeed\Serialization\Subscription;
use PicoFeed\Serialization\SubscriptionList; use PicoFeed\Serialization\SubscriptionList;
use PicoFeed\Serialization\SubscriptionListBuilder; use PicoFeed\Serialization\SubscriptionListBuilder;
use PicoFeed\Serialization\SubscriptionListParser;
function export_all_feeds($user_id)
function export_all_feeds()
{ {
$feeds = Feed\get_all(); $feeds = Model\Feed\get_feeds($user_id);
$subscriptionList = SubscriptionList::create()->setTitle(t('Subscriptions')); $subscriptionList = SubscriptionList::create()->setTitle(t('Subscriptions'));
foreach ($feeds as $feed) { foreach ($feeds as $feed) {
$groups = Group\get_feed_groups($feed['id']); $groups = Model\Group\get_feed_groups($feed['id']);
$category = ''; $category = '';
if (!empty($groups)) { if (!empty($groups)) {
@ -32,3 +32,36 @@ function export_all_feeds()
return SubscriptionListBuilder::create($subscriptionList)->build(); return SubscriptionListBuilder::create($subscriptionList)->build();
} }
function import_opml($user_id, $content)
{
$subscriptionList = SubscriptionListParser::create($content)->parse();
$db = Database::getInstance('db');
$db->startTransaction();
foreach ($subscriptionList->subscriptions as $subscription) {
if (! $db->table('feeds')->eq('user_id', $user_id)->eq('feed_url', $subscription->getFeedUrl())->exists()) {
$db->table('feeds')->insert(array(
'user_id' => $user_id,
'title' => $subscription->getTitle(),
'site_url' => $subscription->getSiteUrl(),
'feed_url' => $subscription->getFeedUrl(),
));
if ($subscription->getCategory() !== '') {
$feed_id = $db->getLastId();
$group_id = Model\Group\get_group_id_from_title($user_id, $subscription->getCategory());
if (empty($group_id)) {
$group_id = Model\Group\create_group($user_id, $subscription->getCategory());
}
Model\Group\associate_feed_groups($feed_id, array($group_id));
}
}
}
$db->closeTransaction();
return true;
}

View File

@ -3,7 +3,6 @@
namespace Miniflux\Handler\Proxy; namespace Miniflux\Handler\Proxy;
use Miniflux\Helper; use Miniflux\Helper;
use Miniflux\Model\Config;
use PicoFeed\Client\ClientException; use PicoFeed\Client\ClientException;
use PicoFeed\Config\Config as PicoFeedConfig; use PicoFeed\Config\Config as PicoFeedConfig;
use PicoFeed\Filter\Filter; use PicoFeed\Filter\Filter;
@ -51,15 +50,14 @@ function rewrite_html($html, $website, $proxy_images, $cloak_referrer)
function download($url) function download($url)
{ {
try { try {
if ((bool) Config\get('debug_mode')) { if (DEBUG_MODE) {
Logger::enable(); Logger::enable();
} }
$client = Client::getInstance(); $client = Client::getInstance();
$client->setUserAgent(Config\HTTP_USER_AGENT); $client->setUserAgent(HTTP_USER_AGENT);
$client->enablePassthroughMode(); $client->enablePassthroughMode();
$client->execute($url); $client->execute($url);
} catch (ClientException $e) {} } catch (ClientException $e) {
}
Config\write_debug();
} }

View File

@ -3,13 +3,13 @@
namespace Miniflux\Handler\Scraper; namespace Miniflux\Handler\Scraper;
use PicoFeed\Scraper\Scraper; use PicoFeed\Scraper\Scraper;
use Miniflux\Model\Config; use Miniflux\Handler;
function download_contents($url) function download_content($url)
{ {
$contents = ''; $contents = '';
$scraper = new Scraper(Config\get_reader_config()); $scraper = new Scraper(Handler\Feed\get_reader_config());
$scraper->setUrl($url); $scraper->setUrl($url);
$scraper->execute(); $scraper->execute();

View File

@ -2,24 +2,24 @@
namespace Miniflux\Handler\Service; namespace Miniflux\Handler\Service;
use Miniflux\Model;
use Miniflux\Helper;
use PicoFeed\Client\Client; use PicoFeed\Client\Client;
use PicoFeed\Client\ClientException; use PicoFeed\Client\ClientException;
use Miniflux\Model\Config;
use Miniflux\Model\Item;
function sync($item_id) function sync($user_id, $item_id)
{ {
$item = Item\get($item_id); $item = Model\Item\get_item($user_id, $item_id);
if ((bool) Config\get('pinboard_enabled')) { if (Helper\bool_config('pinboard_enabled')) {
pinboard_sync($item); pinboard_sync($item);
} }
if ((bool) Config\get('instapaper_enabled')) { if (Helper\bool_config('instapaper_enabled')) {
instapaper_sync($item); instapaper_sync($item);
} }
if ((bool) Config\get('wallabag_enabled')) { if (Helper\bool_config('wallabag_enabled')) {
wallabag_sync($item); wallabag_sync($item);
} }
} }
@ -27,8 +27,8 @@ function sync($item_id)
function instapaper_sync(array $item) function instapaper_sync(array $item)
{ {
$params = array( $params = array(
'username' => Config\get('instapaper_username'), 'username' => Helper\config('instapaper_username'),
'password' => Config\get('instapaper_password'), 'password' => Helper\config('instapaper_password'),
'url' => $item['url'], 'url' => $item['url'],
'title' => $item['title'], 'title' => $item['title'],
); );
@ -47,11 +47,11 @@ function instapaper_sync(array $item)
function pinboard_sync(array $item) function pinboard_sync(array $item)
{ {
$params = array( $params = array(
'auth_token' => Config\get('pinboard_token'), 'auth_token' => Helper\config('pinboard_token'),
'format' => 'json', 'format' => 'json',
'url' => $item['url'], 'url' => $item['url'],
'description' => $item['title'], 'description' => $item['title'],
'tags' => Config\get('pinboard_tags'), 'tags' => Helper\config('pinboard_tags'),
); );
$url = 'https://api.pinboard.in/v1/posts/add?'.http_build_query($params); $url = 'https://api.pinboard.in/v1/posts/add?'.http_build_query($params);
@ -79,7 +79,7 @@ function wallabag_has_url($url)
if ($token === false) { if ($token === false) {
return false; return false;
} }
$apiUrl = rtrim(Config\get('wallabag_url'), '\/') . '/api/entries/exists.json?url=' . urlencode($url); $apiUrl = rtrim(Helper\config('wallabag_url'), '\/') . '/api/entries/exists.json?url=' . urlencode($url);
$headers = array('Authorization: Bearer ' . $token); $headers = array('Authorization: Bearer ' . $token);
$response = api_get_call($apiUrl, $headers); $response = api_get_call($apiUrl, $headers);
if ($response !== false) { if ($response !== false) {
@ -94,7 +94,7 @@ function wallabag_add_item($url, $title)
if ($token === false) { if ($token === false) {
return false; return false;
} }
$apiUrl = rtrim(Config\get('wallabag_url'), '\/') . '/api/entries.json'; $apiUrl = rtrim(Helper\config('wallabag_url'), '\/') . '/api/entries.json';
$headers = array('Authorization: Bearer ' . $token); $headers = array('Authorization: Bearer ' . $token);
$data = array( $data = array(
'url' => $url, 'url' => $url,
@ -112,13 +112,13 @@ function wallabag_get_access_token()
if (!empty($_SESSION['wallabag_access_token'])) { if (!empty($_SESSION['wallabag_access_token'])) {
return $_SESSION['wallabag_access_token']; return $_SESSION['wallabag_access_token'];
} }
$url = rtrim(Config\get('wallabag_url'), '\/') . '/oauth/v2/token'; $url = rtrim(Helper\config('wallabag_url'), '\/') . '/oauth/v2/token';
$data = array( $data = array(
'grant_type' => 'password', 'grant_type' => 'password',
'client_id' => Config\get('wallabag_client_id'), 'client_id' => Helper\config('wallabag_client_id'),
'client_secret' => Config\get('wallabag_client_secret'), 'client_secret' => Helper\config('wallabag_client_secret'),
'username' => Config\get('wallabag_username'), 'username' => Helper\config('wallabag_username'),
'password' => Config\get('wallabag_password') 'password' => Helper\config('wallabag_password')
); );
$response = api_post_call($url, $data); $response = api_post_call($url, $data);
if ($response !== false) { if ($response !== false) {
@ -135,7 +135,7 @@ function api_get_call($url, array $headers = array())
{ {
try { try {
$client = Client::getInstance(); $client = Client::getInstance();
$client->setUserAgent(Config\HTTP_USER_AGENT); $client->setUserAgent(HTTP_USER_AGENT);
if ($headers) { if ($headers) {
$client->setHeaders($headers); $client->setHeaders($headers);
} }

View File

@ -2,6 +2,8 @@
namespace Miniflux\Helper; namespace Miniflux\Helper;
use PicoFeed\Logging\Logger;
function escape($value) function escape($value)
{ {
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
@ -43,7 +45,11 @@ function get_current_base_url()
{ {
$url = is_secure_connection() ? 'https://' : 'http://'; $url = is_secure_connection() ? 'https://' : 'http://';
$url .= $_SERVER['HTTP_HOST']; $url .= $_SERVER['HTTP_HOST'];
if (strpos($_SERVER['HTTP_HOST'], ':') === false) {
$url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
}
$url .= str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])) !== '/' ? str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])).'/' : '/'; $url .= str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])) !== '/' ? str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])).'/' : '/';
return $url; return $url;
@ -53,3 +59,9 @@ function is_secure_connection()
{ {
return ! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; return ! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
} }
function write_debug_file() {
if (DEBUG_MODE) {
file_put_contents(DEBUG_FILENAME, implode(PHP_EOL, Logger::getMessages()), FILE_APPEND|LOCK_EX);
}
}

38
app/helpers/config.php Normal file
View File

@ -0,0 +1,38 @@
<?php
namespace Miniflux\Helper;
use Miniflux\Model;
use Miniflux\Session\SessionStorage;
function config($parameter, $default = null)
{
$session = SessionStorage::getInstance();
$cache = $session->getConfig();
$value = null;
if (empty($cache)) {
$cache = Model\Config\get_all($session->getUserId());
$session->setConfig($cache);
}
if (array_key_exists($parameter, $cache)) {
$value = $cache[$parameter];
}
if ($value === null) {
$value = $default;
}
return $value;
}
function bool_config($parameter, $default = false)
{
return (bool) config($parameter, $default);
}
function int_config($parameter, $default = false)
{
return (int) config($parameter, $default);
}

View File

@ -22,7 +22,7 @@ function favicon_extension($type)
function favicon(array $favicons, $feed_id) function favicon(array $favicons, $feed_id)
{ {
if (! empty($favicons[$feed_id])) { if (! empty($favicons[$feed_id])) {
return '<img src="'.FAVICON_URL_PATH.'/'.$favicons[$feed_id]['hash'].favicon_extension($favicons[$feed_id]['type']).'" class="favicon"/>'; return '<img src="'.FAVICON_URL_PATH.'/'.$favicons[$feed_id]['hash'].favicon_extension($favicons[$feed_id]['type']).'" class="favicon">';
} }
return ''; return '';

View File

@ -2,7 +2,17 @@
namespace Miniflux\Helper; namespace Miniflux\Helper;
use Miniflux\Model\Config; use Miniflux\Session\SessionStorage;
function get_user_id()
{
return SessionStorage::getInstance()->getUserId();
}
function is_admin()
{
return SessionStorage::getInstance()->isAdmin();
}
function flash($type, $html) function flash($type, $html)
{ {
@ -16,14 +26,18 @@ function flash($type, $html)
return $data; return $data;
} }
function is_rtl(array $item) function rtl(array $item)
{ {
return ! empty($item['rtl']) || \PicoFeed\Parser\Parser::isLanguageRTL($item['language']); if ($item['rtl'] == 1) {
return 'dir="rtl"';
}
return 'dir="ltr"';
} }
function css() function css()
{ {
$theme = Config\get('theme'); $theme = config('theme');
if ($theme !== 'original') { if ($theme !== 'original') {
$css_file = THEME_DIRECTORY.'/'.$theme.'/css/app.css'; $css_file = THEME_DIRECTORY.'/'.$theme.'/css/app.css';

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => ':تحديث تلقائي للميني فلكس من الرابط', 'Auto-Update URL' => ':تحديث تلقائي للميني فلكس من الرابط',
'Update Miniflux' => 'تحديث برنامج Miniflux', 'Update Miniflux' => 'تحديث برنامج Miniflux',
'Miniflux is updated!' => 'بنجاح! Miniflux تمت عملية تحديث برنامج', 'Miniflux is updated!' => 'بنجاح! Miniflux تمت عملية تحديث برنامج',
'Unable to update Miniflux, check the console for errors.' => 'غير قادر على تحديث برنامج Miniflux لمزيد من المعلومات يرجى الذهاب إلى نافذة رسائل الإشعارات',
'Don\'t forget to backup your database' => 'لاتنسى إنشاء نسخة إحتياطية من قاعدة البيانات', 'Don\'t forget to backup your database' => 'لاتنسى إنشاء نسخة إحتياطية من قاعدة البيانات',
'The name must have only alpha-numeric characters' => 'يجب إدخال أحرف أو أرقام فقط', 'The name must have only alpha-numeric characters' => 'يجب إدخال أحرف أو أرقام فقط',
'New database' => 'إنشاء قاعدة بيانات جديده', 'New database' => 'إنشاء قاعدة بيانات جديده',
@ -200,7 +199,6 @@ return array(
'about' => 'حول البرنامج', 'about' => 'حول البرنامج',
'This action will update Miniflux with the last development version, are you sure?' => 'سيتم إستبدال هذه النسخة من برنامج ميني فلكس بأحدث نسخه ... هل أنت متأكد من انك تريد ذلك؟ ?', 'This action will update Miniflux with the last development version, are you sure?' => 'سيتم إستبدال هذه النسخة من برنامج ميني فلكس بأحدث نسخه ... هل أنت متأكد من انك تريد ذلك؟ ?',
'database' => 'قاعدة البيانات', 'database' => 'قاعدة البيانات',
'Console' => 'Console',
'Miniflux API' => 'Miniflux API', 'Miniflux API' => 'Miniflux API',
'menu' => 'قائمة', 'menu' => 'قائمة',
'Default' => 'إفتراضي', 'Default' => 'إفتراضي',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'URL automatické aktualizace', 'Auto-Update URL' => 'URL automatické aktualizace',
'Update Miniflux' => 'Aktualizovat Miniflux', 'Update Miniflux' => 'Aktualizovat Miniflux',
'Miniflux is updated!' => 'Miniflux je aktualizovaný!', 'Miniflux is updated!' => 'Miniflux je aktualizovaný!',
'Unable to update Miniflux, check the console for errors.' => 'Nelze aktualizovat Miniflux, zkontrolujte konzoli na chyby.',
'Don\'t forget to backup your database' => 'Nezapomeňte zálohovat vaši databázi', 'Don\'t forget to backup your database' => 'Nezapomeňte zálohovat vaši databázi',
'The name must have only alpha-numeric characters' => 'Jméno smí obsahovat pouze písmena a číslice', 'The name must have only alpha-numeric characters' => 'Jméno smí obsahovat pouze písmena a číslice',
'New database' => 'Nová databáze', 'New database' => 'Nová databáze',
@ -200,7 +199,6 @@ return array(
'about' => 'o', 'about' => 'o',
'This action will update Miniflux with the last development version, are you sure?' => 'Tato akce aktualizuje Miniflux na poslední vývojovou verzi. Jste si jistí?', 'This action will update Miniflux with the last development version, are you sure?' => 'Tato akce aktualizuje Miniflux na poslední vývojovou verzi. Jste si jistí?',
'database' => 'databáze', 'database' => 'databáze',
'Console' => 'konzole',
'Miniflux API' => 'Miniflux API', 'Miniflux API' => 'Miniflux API',
'menu' => 'nabídka', 'menu' => 'nabídka',
'Default' => 'Výchozí', 'Default' => 'Výchozí',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'Auto-Update URL', 'Auto-Update URL' => 'Auto-Update URL',
'Update Miniflux' => 'Miniflux aktualisieren', 'Update Miniflux' => 'Miniflux aktualisieren',
'Miniflux is updated!' => 'Miniflux wurde erfolgreich aktualisiert!', 'Miniflux is updated!' => 'Miniflux wurde erfolgreich aktualisiert!',
'Unable to update Miniflux, check the console for errors.' => 'Aktualisierung von Miniflux fehlgeschlagen, überprüfe die Konsole nach Fehlermeldungen.',
'Don\'t forget to backup your database' => 'Vergiss nicht, die Datenbank zu sichern', 'Don\'t forget to backup your database' => 'Vergiss nicht, die Datenbank zu sichern',
'The name must have only alpha-numeric characters' => 'Der Name darf nur alphanumerische Zeichen enthalten', 'The name must have only alpha-numeric characters' => 'Der Name darf nur alphanumerische Zeichen enthalten',
'New database' => 'Neue Datenbank', 'New database' => 'Neue Datenbank',
@ -200,7 +199,6 @@ return array(
'about' => 'über', 'about' => 'über',
'This action will update Miniflux with the last development version, are you sure?' => 'Miniflux wird auf die aktuelle Entwicklungsversion aktualisiert. Bist du sicher?', 'This action will update Miniflux with the last development version, are you sure?' => 'Miniflux wird auf die aktuelle Entwicklungsversion aktualisiert. Bist du sicher?',
'database' => 'Datenbank', 'database' => 'Datenbank',
'Console' => 'Konsole',
'Miniflux API' => 'Miniflux API', 'Miniflux API' => 'Miniflux API',
'menu' => 'Menü', 'menu' => 'Menü',
'Default' => 'Standard', 'Default' => 'Standard',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'Actualizar automáticamente la URL', 'Auto-Update URL' => 'Actualizar automáticamente la URL',
'Update Miniflux' => 'Actualizar Miniflux', 'Update Miniflux' => 'Actualizar Miniflux',
'Miniflux is updated!' => 'Miniflux esta actualizado', 'Miniflux is updated!' => 'Miniflux esta actualizado',
'Unable to update Miniflux, check the console for errors.' => 'No ha sido posible actualizar Miniflux, lea los errores en la consola',
'Don\'t forget to backup your database' => 'No olvides de hacer una copia de seguridad de la base de datos', 'Don\'t forget to backup your database' => 'No olvides de hacer una copia de seguridad de la base de datos',
'The name must have only alpha-numeric characters' => 'El nombre sólo puede contener caractéres alfanuméricos', 'The name must have only alpha-numeric characters' => 'El nombre sólo puede contener caractéres alfanuméricos',
'New database' => 'Nueva base de datos', 'New database' => 'Nueva base de datos',
@ -200,7 +199,6 @@ return array(
'about' => 'acerca de', 'about' => 'acerca de',
'This action will update Miniflux with the last development version, are you sure?' => 'Esta acción actualizará Miniflux a la última versión de desarrollo, ¿está seguro?', 'This action will update Miniflux with the last development version, are you sure?' => 'Esta acción actualizará Miniflux a la última versión de desarrollo, ¿está seguro?',
'database' => 'base de datos', 'database' => 'base de datos',
'Console' => 'Consola',
'Miniflux API' => 'API Miniflux', 'Miniflux API' => 'API Miniflux',
'menu' => 'menú', 'menu' => 'menú',
'Default' => 'Por defecto', 'Default' => 'Por defecto',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'URL de mise à jour automatique', 'Auto-Update URL' => 'URL de mise à jour automatique',
'Update Miniflux' => 'Mettre à jour Miniflux', 'Update Miniflux' => 'Mettre à jour Miniflux',
'Miniflux is updated!' => 'Miniflux a été mis à jour avec succès !', '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', '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', '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', 'New database' => 'Nouvelle base de données',
@ -200,7 +199,6 @@ return array(
'about' => 'a propos', 'about' => 'a propos',
'This action will update Miniflux with the last development version, are you sure?' => 'Cette action va mettre à jour Miniflux avec la dernière version en cours de développement, êtes-vous certain ?', 'This action will update Miniflux with the last development version, are you sure?' => 'Cette action va mettre à jour Miniflux avec la dernière version en cours de développement, êtes-vous certain ?',
'database' => 'base de données', 'database' => 'base de données',
'Console' => 'Console',
'Miniflux API' => 'Miniflux API', 'Miniflux API' => 'Miniflux API',
'menu' => 'menu', 'menu' => 'menu',
'Default' => 'Défaut', 'Default' => 'Défaut',

View File

@ -157,7 +157,6 @@ return array(
// 'Auto-Update URL' => '', // 'Auto-Update URL' => '',
// 'Update Miniflux' => '', // 'Update Miniflux' => '',
// 'Miniflux is updated!' => '', // 'Miniflux is updated!' => '',
// 'Unable to update Miniflux, check the console for errors.' => '',
// 'Don\'t forget to backup your database' => '', // 'Don\'t forget to backup your database' => '',
// 'The name must have only alpha-numeric characters' => '', // 'The name must have only alpha-numeric characters' => '',
// 'New database' => '', // 'New database' => '',
@ -200,7 +199,6 @@ return array(
// 'about' => '', // 'about' => '',
// 'This action will update Miniflux with the last development version, are you sure?' => '', // 'This action will update Miniflux with the last development version, are you sure?' => '',
// 'database' => '', // 'database' => '',
// 'Console' => '',
// 'Miniflux API' => '', // 'Miniflux API' => '',
// 'menu' => '', // 'menu' => '',
// 'Default' => '', // 'Default' => '',

View File

@ -159,7 +159,6 @@ return array(
'Auto-Update URL' => '自動更新のURL', 'Auto-Update URL' => '自動更新のURL',
'Update Miniflux' => 'Minifluxを更新', 'Update Miniflux' => 'Minifluxを更新',
'Miniflux is updated!' => 'Minifluxは更新されました', 'Miniflux is updated!' => 'Minifluxは更新されました',
'Unable to update Miniflux, check the console for errors.' => 'Minifluxを更新できません。エラーコンソールを確認してください。',
'Don\'t forget to backup your database' => 'データベースのバックアップを忘れないで下さい', 'Don\'t forget to backup your database' => 'データベースのバックアップを忘れないで下さい',
'The name must have only alpha-numeric characters' => '名前には英数字のみを使用することが出来ます', 'The name must have only alpha-numeric characters' => '名前には英数字のみを使用することが出来ます',
'New database' => '新しいデータベース', 'New database' => '新しいデータベース',
@ -202,7 +201,6 @@ return array(
'about' => 'Minifluxについて', 'about' => 'Minifluxについて',
'This action will update Miniflux with the last development version, are you sure?' => '最新の開発バージョンでMinifluxを更新します。よろしいですか', 'This action will update Miniflux with the last development version, are you sure?' => '最新の開発バージョンでMinifluxを更新します。よろしいですか',
'database' => 'データベース', 'database' => 'データベース',
'Console' => 'コンソール',
'Miniflux API' => 'Miniflux API', 'Miniflux API' => 'Miniflux API',
'menu' => 'メニュー', 'menu' => 'メニュー',
'Default' => 'デフォルト', 'Default' => 'デフォルト',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'URL de atualização automática', 'Auto-Update URL' => 'URL de atualização automática',
'Update Miniflux' => 'Atualizar Miniflux', 'Update Miniflux' => 'Atualizar Miniflux',
'Miniflux is updated!' => 'Miniflux foi atualizado!', 'Miniflux is updated!' => 'Miniflux foi atualizado!',
'Unable to update Miniflux, check the console for errors.' => 'Incapaz de atualizar Miniflux, verifique o console para erros',
'Don\'t forget to backup your database' => 'Não esqueça de fazer backup de seu banco de dados', 'Don\'t forget to backup your database' => 'Não esqueça de fazer backup de seu banco de dados',
'The name must have only alpha-numeric characters' => 'O nome deve conter apenas caracteres alfa-numéricos', 'The name must have only alpha-numeric characters' => 'O nome deve conter apenas caracteres alfa-numéricos',
'New database' => 'Novo banco de dados', 'New database' => 'Novo banco de dados',
@ -200,7 +199,6 @@ return array(
'about' => 'sobre', 'about' => 'sobre',
'This action will update Miniflux with the last development version, are you sure?' => 'Esta ação irá atualizar o Miniflux com a última versão de desenvolvimento, você tem certeza?', 'This action will update Miniflux with the last development version, are you sure?' => 'Esta ação irá atualizar o Miniflux com a última versão de desenvolvimento, você tem certeza?',
'database' => 'banco de dados', 'database' => 'banco de dados',
'Console' => 'Console',
'Miniflux API' => 'API do Miniflux', 'Miniflux API' => 'API do Miniflux',
'menu' => 'menu', 'menu' => 'menu',
'Default' => 'Padrão', 'Default' => 'Padrão',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'URL автоматического обновления', 'Auto-Update URL' => 'URL автоматического обновления',
'Update Miniflux' => 'Обновить Miniflux', 'Update Miniflux' => 'Обновить Miniflux',
'Miniflux is updated!' => 'Miniflux обновлен!', 'Miniflux is updated!' => 'Miniflux обновлен!',
'Unable to update Miniflux, check the console for errors.' => 'Невозможно обновить Miniflux, смотрите ошибки в консоле.',
'Don\'t forget to backup your database' => 'Не забудьте предварительно сделать резервную копию базы данных', 'Don\'t forget to backup your database' => 'Не забудьте предварительно сделать резервную копию базы данных',
'The name must have only alpha-numeric characters' => 'Название должно состоять только из алфавитно-цифровых символов', 'The name must have only alpha-numeric characters' => 'Название должно состоять только из алфавитно-цифровых символов',
'New database' => 'Новая база данных', 'New database' => 'Новая база данных',
@ -200,7 +199,6 @@ return array(
'about' => 'о программе', 'about' => 'о программе',
'This action will update Miniflux with the last development version, are you sure?' => 'Это действие обновит Miniflux до последней разрабатываемой версии, вы уверены?', 'This action will update Miniflux with the last development version, are you sure?' => 'Это действие обновит Miniflux до последней разрабатываемой версии, вы уверены?',
'database' => 'база данных', 'database' => 'база данных',
'Console' => 'Консоль',
'Miniflux API' => 'Miniflux API', 'Miniflux API' => 'Miniflux API',
'menu' => 'меню', 'menu' => 'меню',
'Default' => 'По-умолчанию', 'Default' => 'По-умолчанию',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'УРЛ за аутоматско ажурирање', 'Auto-Update URL' => 'УРЛ за аутоматско ажурирање',
'Update Miniflux' => 'Ажурирај Минифлукс', 'Update Miniflux' => 'Ажурирај Минифлукс',
'Miniflux is updated!' => 'Минифлукс је успешно ажуриран !', 'Miniflux is updated!' => 'Минифлукс је успешно ажуриран !',
'Unable to update Miniflux, check the console for errors.' => 'Неуспешно ажурирање Минифлукса, проверите конзолу за списак грешака.',
'Don\'t forget to backup your database' => 'Не заборавите да бекапујете базу података', 'Don\'t forget to backup your database' => 'Не заборавите да бекапујете базу података',
'The name must have only alpha-numeric characters' => 'Име може садржати само бројеве или слова', 'The name must have only alpha-numeric characters' => 'Име може садржати само бројеве или слова',
'New database' => 'Нова база података', 'New database' => 'Нова база података',
@ -200,7 +199,6 @@ return array(
'about' => 'о програму', 'about' => 'о програму',
'This action will update Miniflux with the last development version, are you sure?' => 'Ова акција ће ажурирати Минифлукс на најновију развојну верију, да ли сте сигурни?', 'This action will update Miniflux with the last development version, are you sure?' => 'Ова акција ће ажурирати Минифлукс на најновију развојну верију, да ли сте сигурни?',
'database' => 'база података', 'database' => 'база података',
'Console' => 'Конзола',
'Miniflux API' => 'АПИ Минифлукса', 'Miniflux API' => 'АПИ Минифлукса',
'menu' => 'мени', 'menu' => 'мени',
'Default' => 'Основна', 'Default' => 'Основна',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'URL za automatsko ažuriranje', 'Auto-Update URL' => 'URL za automatsko ažuriranje',
'Update Miniflux' => 'Ažuriraj Miniflux', 'Update Miniflux' => 'Ažuriraj Miniflux',
'Miniflux is updated!' => 'Miniflux je uspešno ažuriran !', 'Miniflux is updated!' => 'Miniflux je uspešno ažuriran !',
'Unable to update Miniflux, check the console for errors.' => 'Neuspešno ažuriranje Minifluxa, proverite konzolu za spisak grešaka.',
'Don\'t forget to backup your database' => 'Ne zaboravite da bekapujete bazu podataka', 'Don\'t forget to backup your database' => 'Ne zaboravite da bekapujete bazu podataka',
'The name must have only alpha-numeric characters' => 'Ime može sadržati samo brojeve ili slova', 'The name must have only alpha-numeric characters' => 'Ime može sadržati samo brojeve ili slova',
'New database' => 'Nova baza podataka', 'New database' => 'Nova baza podataka',
@ -200,7 +199,6 @@ return array(
'about' => 'o programu', 'about' => 'o programu',
'This action will update Miniflux with the last development version, are you sure?' => 'Ova akcija će ažurirati Miniflux na najnoviju razvojnu veriju, da li ste sigurni?', 'This action will update Miniflux with the last development version, are you sure?' => 'Ova akcija će ažurirati Miniflux na najnoviju razvojnu veriju, da li ste sigurni?',
'database' => 'baza podataka', 'database' => 'baza podataka',
'Console' => 'Konzola',
'Miniflux API' => 'API Minifluxa', 'Miniflux API' => 'API Minifluxa',
'menu' => 'meni', 'menu' => 'meni',
'Default' => 'Osnovna', 'Default' => 'Osnovna',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'Otomatik güncelleme bağlantısı', 'Auto-Update URL' => 'Otomatik güncelleme bağlantısı',
'Update Miniflux' => 'Miniflux\'ı Güncelle', 'Update Miniflux' => 'Miniflux\'ı Güncelle',
'Miniflux is updated!' => 'Miniflux güncellendi!', 'Miniflux is updated!' => 'Miniflux güncellendi!',
'Unable to update Miniflux, check the console for errors.' => 'Miniflux güncellenemiyor, hataları konsol üzerinden kontrol edin.',
'Don\'t forget to backup your database' => 'Veritabanınızı yedeklemeyi unutmayın', 'Don\'t forget to backup your database' => 'Veritabanınızı yedeklemeyi unutmayın',
'The name must have only alpha-numeric characters' => 'İsim yalnızca alfanümerik karakterler içermeli', 'The name must have only alpha-numeric characters' => 'İsim yalnızca alfanümerik karakterler içermeli',
'New database' => 'Yeni veritabanı', 'New database' => 'Yeni veritabanı',
@ -200,7 +199,6 @@ return array(
'about' => 'hakkında', 'about' => 'hakkında',
'This action will update Miniflux with the last development version, are you sure?' => 'Bu işlem Miniflux\'u en son yayınlanan geliştirme sürümüne güncelleyecektir, emin misiniz?', 'This action will update Miniflux with the last development version, are you sure?' => 'Bu işlem Miniflux\'u en son yayınlanan geliştirme sürümüne güncelleyecektir, emin misiniz?',
'database' => 'veritabanı', 'database' => 'veritabanı',
'Console' => 'Konsol',
'Miniflux API' => 'Miniflux API', 'Miniflux API' => 'Miniflux API',
'menu' => 'menü', 'menu' => 'menü',
'Default' => 'Varsayılan', 'Default' => 'Varsayılan',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => '自动更新URL', 'Auto-Update URL' => '自动更新URL',
'Update Miniflux' => '更新Miniflux', 'Update Miniflux' => '更新Miniflux',
'Miniflux is updated!' => 'Miniflux已被更新', 'Miniflux is updated!' => 'Miniflux已被更新',
'Unable to update Miniflux, check the console for errors.' => '无法更新Miniflux检查控制台上的错误',
'Don\'t forget to backup your database' => '不要忘记备份你的数据库', 'Don\'t forget to backup your database' => '不要忘记备份你的数据库',
'The name must have only alpha-numeric characters' => '名字只能包含字母和数字', 'The name must have only alpha-numeric characters' => '名字只能包含字母和数字',
'New database' => '新数据库', 'New database' => '新数据库',
@ -200,7 +199,6 @@ return array(
'about' => '关于', 'about' => '关于',
'This action will update Miniflux with the last development version, are you sure?' => '这个操作将更新Miniflux到最新的开发版你确认吗', 'This action will update Miniflux with the last development version, are you sure?' => '这个操作将更新Miniflux到最新的开发版你确认吗',
'database' => '数据库', 'database' => '数据库',
'Console' => '控制台',
'Miniflux API' => 'Miniflux API', 'Miniflux API' => 'Miniflux API',
'menu' => '菜单', 'menu' => '菜单',
'Default' => '默认', 'Default' => '默认',

View File

@ -48,8 +48,6 @@ function is_excluded_path($path, array $exclude_list)
// Synchronize 2 directories (copy/remove files) // Synchronize 2 directories (copy/remove files)
function synchronize($source_directory, $destination_directory) function synchronize($source_directory, $destination_directory)
{ {
Config\debug('[SYNCHRONIZE] '.$source_directory.' to '.$destination_directory);
$src_files = get_files_list($source_directory); $src_files = get_files_list($source_directory);
$dst_files = get_files_list($destination_directory); $dst_files = get_files_list($destination_directory);
@ -59,7 +57,6 @@ function synchronize($source_directory, $destination_directory)
foreach ($remove_files as $file) { foreach ($remove_files as $file) {
if ($file !== '.htaccess') { if ($file !== '.htaccess') {
$destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file; $destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file;
Config\debug('[REMOVE] '.$destination_file);
if (! @unlink($destination_file)) { if (! @unlink($destination_file)) {
return false; return false;
@ -72,8 +69,6 @@ function synchronize($source_directory, $destination_directory)
$directory = $destination_directory.DIRECTORY_SEPARATOR.dirname($file); $directory = $destination_directory.DIRECTORY_SEPARATOR.dirname($file);
if (! is_dir($directory)) { if (! is_dir($directory)) {
Config\debug('[MKDIR] '.$directory);
if (! @mkdir($directory, 0755, true)) { if (! @mkdir($directory, 0755, true)) {
return false; return false;
} }
@ -82,8 +77,6 @@ function synchronize($source_directory, $destination_directory)
$source_file = $source_directory.DIRECTORY_SEPARATOR.$file; $source_file = $source_directory.DIRECTORY_SEPARATOR.$file;
$destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file; $destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file;
Config\debug('[COPY] '.$source_file.' to '.$destination_file);
if (! @copy($source_file, $destination_file)) { if (! @copy($source_file, $destination_file)) {
return false; return false;
} }
@ -96,9 +89,6 @@ function synchronize($source_directory, $destination_directory)
function uncompress_archive($url, $download_directory = AUTO_UPDATE_DOWNLOAD_DIRECTORY, $archive_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY) function uncompress_archive($url, $download_directory = AUTO_UPDATE_DOWNLOAD_DIRECTORY, $archive_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY)
{ {
$archive_file = $download_directory.DIRECTORY_SEPARATOR.'update.zip'; $archive_file = $download_directory.DIRECTORY_SEPARATOR.'update.zip';
Config\debug('[DOWNLOAD] '.$url);
if (($data = @file_get_contents($url)) === false) { if (($data = @file_get_contents($url)) === false) {
return false; return false;
} }
@ -107,8 +97,6 @@ function uncompress_archive($url, $download_directory = AUTO_UPDATE_DOWNLOAD_DIR
return false; return false;
} }
Config\debug('[UNZIP] '.$archive_file);
$zip = new ZipArchive; $zip = new ZipArchive;
if (! $zip->open($archive_file)) { if (! $zip->open($archive_file)) {
@ -124,8 +112,6 @@ function uncompress_archive($url, $download_directory = AUTO_UPDATE_DOWNLOAD_DIR
// Remove all files for a given directory // Remove all files for a given directory
function cleanup_directory($directory) function cleanup_directory($directory)
{ {
Config\debug('[CLEANUP] '.$directory);
$dir = new DirectoryIterator($directory); $dir = new DirectoryIterator($directory);
foreach ($dir as $fileinfo) { foreach ($dir as $fileinfo) {
@ -133,7 +119,6 @@ function cleanup_directory($directory)
$filename = $fileinfo->getRealPath(); $filename = $fileinfo->getRealPath();
if ($fileinfo->isFile()) { if ($fileinfo->isFile()) {
Config\debug('[REMOVE] '.$filename);
@unlink($filename); @unlink($filename);
} else { } else {
cleanup_directory($filename); cleanup_directory($filename);
@ -165,14 +150,10 @@ function find_archive_root($base_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY)
} }
if (empty($directory)) { if (empty($directory)) {
Config\debug('[FIND ARCHIVE] No directory found');
return false; return false;
} }
$path = $base_directory.DIRECTORY_SEPARATOR.$directory; return $base_directory.DIRECTORY_SEPARATOR.$directory;
Config\debug('[FIND ARCHIVE] '.$path);
return $path;
} }
// Check if everything is setup correctly // Check if everything is setup correctly

View File

@ -2,60 +2,68 @@
namespace Miniflux\Model\Bookmark; namespace Miniflux\Model\Bookmark;
use Miniflux\Helper;
use Miniflux\Model;
use PicoDb\Database; use PicoDb\Database;
use Miniflux\Handler\Service;
use Miniflux\Model\Config;
function count_items($feed_ids = array()) function count_bookmarked_items($user_id, array $feed_ids = array())
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('items') ->table(Model\Item\TABLE)
->eq('bookmark', 1) ->eq('bookmark', 1)
->eq('user_id', $user_id)
->in('feed_id', $feed_ids) ->in('feed_id', $feed_ids)
->in('status', array('read', 'unread')) ->in('status', array(Model\Item\STATUS_READ, Model\Item\STATUS_UNREAD))
->count(); ->count();
} }
function get_all_items($offset = null, $limit = null, $feed_ids = array()) function get_bookmarked_items($user_id, $offset = null, $limit = null, array $feed_ids = array())
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('items') ->table(Model\Item\TABLE)
->columns( ->columns(
'items.id', 'items.id',
'items.checksum',
'items.title', 'items.title',
'items.updated', 'items.updated',
'items.url', 'items.url',
'items.enclosure', 'items.enclosure_url',
'items.enclosure_type', 'items.enclosure_type',
'items.bookmark', 'items.bookmark',
'items.status', 'items.status',
'items.content', 'items.content',
'items.feed_id', 'items.feed_id',
'items.language', 'items.language',
'items.rtl',
'items.author', 'items.author',
'feeds.site_url', 'feeds.site_url',
'feeds.title AS feed_title', 'feeds.title AS feed_title'
'feeds.rtl'
) )
->join('feeds', 'id', 'feed_id') ->join(Model\Feed\TABLE, 'id', 'feed_id')
->in('feed_id', $feed_ids) ->eq('items.user_id', $user_id)
->in('status', array('read', 'unread')) ->in('items.feed_id', $feed_ids)
->eq('bookmark', 1) ->neq('items.status', Model\Item\STATUS_REMOVED)
->orderBy('updated', Config\get('items_sorting_direction')) ->eq('items.bookmark', 1)
->orderBy('items.updated', Helper\config('items_sorting_direction'))
->offset($offset) ->offset($offset)
->limit($limit) ->limit($limit)
->findAll(); ->findAll();
} }
function set_flag($id, $value) function get_bookmarked_item_ids($user_id)
{ {
if ($value == 1) {
Service\sync($id);
}
return Database::getInstance('db') return Database::getInstance('db')
->table('items') ->table(Model\Item\TABLE)
->eq('id', $id) ->eq('user_id', $user_id)
->in('status', array('read', 'unread')) ->eq('bookmark', 1)
->save(array('bookmark' => $value)); ->findAllByColumn('id');
}
function set_flag($user_id, $item_id, $value)
{
return Database::getInstance('db')
->table(Model\Item\TABLE)
->eq('user_id', $user_id)
->eq('id', $item_id)
->update(array('bookmark' => (int) $value));
} }

View File

@ -3,46 +3,12 @@
namespace Miniflux\Model\Config; namespace Miniflux\Model\Config;
use Miniflux\Helper; use Miniflux\Helper;
use Miniflux\Translator; use Miniflux\Model;
use DirectoryIterator; use DirectoryIterator;
use Miniflux\Session\SessionStorage;
use PicoDb\Database; use PicoDb\Database;
use PicoFeed\Config\Config as ReaderConfig;
use PicoFeed\Logging\Logger;
const HTTP_USER_AGENT = 'Miniflux (https://miniflux.net)'; const TABLE = 'user_settings';
// Get PicoFeed config
function get_reader_config()
{
$config = new ReaderConfig;
$config->setTimezone(get('timezone'));
// Client
$config->setClientTimeout(HTTP_TIMEOUT);
$config->setClientUserAgent(HTTP_USER_AGENT);
$config->setMaxBodySize(HTTP_MAX_RESPONSE_SIZE);
// Grabber
$config->setGrabberRulesFolder(RULES_DIRECTORY);
// Proxy
$config->setProxyHostname(PROXY_HOSTNAME);
$config->setProxyPort(PROXY_PORT);
$config->setProxyUsername(PROXY_USERNAME);
$config->setProxyPassword(PROXY_PASSWORD);
// Filter
$config->setFilterIframeWhitelist(get_iframe_whitelist());
if ((bool) get('debug_mode')) {
Logger::enable();
}
// Parser
$config->setParserHashAlgo('crc32b');
return $config;
}
function get_iframe_whitelist() function get_iframe_whitelist()
{ {
@ -56,39 +22,21 @@ function get_iframe_whitelist()
); );
} }
// Send a debug message to the console
function debug($line)
{
Logger::setMessage($line);
write_debug();
}
// Write PicoFeed debug output to a file
function write_debug()
{
if ((bool) get('debug_mode')) {
file_put_contents(DEBUG_FILENAME, implode(PHP_EOL, Logger::getMessages()));
}
}
// Get available timezone
function get_timezones() function get_timezones()
{ {
$timezones = timezone_identifiers_list(); $timezones = timezone_identifiers_list();
return array_combine(array_values($timezones), $timezones); return array_combine(array_values($timezones), $timezones);
} }
// Returns true if the language is RTL
function is_language_rtl() function is_language_rtl()
{ {
$languages = array( $languages = array(
'ar_AR' 'ar_AR'
); );
return in_array(get('language'), $languages); return in_array(Helper\config('language'), $languages);
} }
// Get all supported languages
function get_languages() function get_languages()
{ {
return array( return array(
@ -109,7 +57,6 @@ function get_languages()
); );
} }
// Get all skins
function get_themes() function get_themes()
{ {
$themes = array( $themes = array(
@ -129,7 +76,6 @@ function get_themes()
return $themes; return $themes;
} }
// Sorting direction choices for items
function get_sorting_directions() function get_sorting_directions()
{ {
return array( return array(
@ -138,26 +84,23 @@ function get_sorting_directions()
); );
} }
// Display summaries or full contents on lists
function get_display_mode() function get_display_mode()
{ {
return array( return array(
'titles' => t('Titles'), 'titles' => t('Titles'),
'summaries' => t('Summaries'), 'summaries' => t('Summaries'),
'full' => t('Full contents') 'full' => t('Full contents'),
); );
} }
// Item title links to original or full contents
function get_item_title_link() function get_item_title_link()
{ {
return array( return array(
'original' => t('Original'), 'original' => t('Original'),
'full' => t('Full contents') 'full' => t('Full contents'),
); );
} }
// Autoflush choices for read items
function get_autoflush_read_options() function get_autoflush_read_options()
{ {
return array( return array(
@ -166,11 +109,10 @@ function get_autoflush_read_options()
'1' => t('After %d day', 1), '1' => t('After %d day', 1),
'5' => t('After %d day', 5), '5' => t('After %d day', 5),
'15' => t('After %d day', 15), '15' => t('After %d day', 15),
'30' => t('After %d day', 30) '30' => t('After %d day', 30),
); );
} }
// Autoflush choices for unread items
function get_autoflush_unread_options() function get_autoflush_unread_options()
{ {
return array( return array(
@ -182,7 +124,6 @@ function get_autoflush_unread_options()
); );
} }
// Number of items per pages
function get_paging_options() function get_paging_options()
{ {
return array( return array(
@ -197,7 +138,6 @@ function get_paging_options()
); );
} }
// Get redirect options when there is nothing to read
function get_nothing_to_read_redirections() function get_nothing_to_read_redirections()
{ {
return array( return array(
@ -207,74 +147,87 @@ function get_nothing_to_read_redirections()
); );
} }
function get_default_values()
// Regenerate tokens for the API and bookmark feed
function new_tokens()
{ {
$values = array( return array(
'api_token' => Helper\generate_token(), 'language' => 'en_US',
'feed_token' => Helper\generate_token(), 'timezone' => 'UTC',
'bookmarklet_token' => Helper\generate_token(), 'theme' => 'original',
'fever_token' => substr(Helper\generate_token(), 0, 8), 'autoflush' => 15,
'autoflush_unread' => 45,
'frontend_updatecheck_interval' => 10,
'favicons' => 1,
'nocontent' => 0,
'image_proxy' => 0,
'original_marks_read' => 1,
'instapaper_enabled' => 0,
'pinboard_enabled' => 0,
'pinboard_tags' => 'miniflux',
'items_per_page' => 100,
'items_display_mode' => 'summaries',
'items_sorting_direction' => 'desc',
'redirect_nothing_to_read' => 'feeds',
'item_title_link' => 'full',
); );
return Database::getInstance('db')->hashtable('settings')->put($values);
} }
// Get a config value from the DB or from the session function get_all($user_id)
function get($name)
{ {
if (! isset($_SESSION)) { $settings = Database::getInstance('db')
return current(Database::getInstance('db')->hashtable('settings')->get($name)); ->hashtable(TABLE)
} else { ->eq('user_id', $user_id)
if (! isset($_SESSION['config'][$name])) { ->getAll('key', 'value');
$_SESSION['config'] = get_all();
if (empty($settings)) {
save_defaults($user_id);
$settings = Database::getInstance('db')
->hashtable(TABLE)
->eq('user_id', $user_id)
->getAll('key', 'value');
} }
if (isset($_SESSION['config'][$name])) { return $settings;
return $_SESSION['config'][$name];
}
}
return null;
} }
// Get all config parameters function save_defaults($user_id)
function get_all()
{ {
$config = Database::getInstance('db')->hashtable('settings')->get(); return save($user_id, get_default_values());
unset($config['password']);
return $config;
} }
// Save config into the database and update the session function save($user_id, array $values)
function save(array $values)
{ {
// Update the password if needed $db = Database::getInstance('db');
if (! empty($values['password'])) { $results = array();
$values['password'] = password_hash($values['password'], PASSWORD_BCRYPT); $db->startTransaction();
} 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']) { if (isset($values['nocontent']) && (bool) $values['nocontent']) {
Database::getInstance('db')->table('items')->update(array('content' => '')); $db
->table(Model\Item\TABLE)
->eq('user_id', $user_id)
->update(array('content' => ''));
} }
if (Database::getInstance('db')->hashtable('settings')->put($values)) { foreach ($values as $key => $value) {
reload(); if ($db->table(TABLE)->eq('user_id', $user_id)->eq('key', $key)->exists()) {
return true; $results[] = $db->table(TABLE)
->eq('user_id', $user_id)
->eq('key', $key)
->update(array('value' => $value));
} else {
$results[] = $db->table(TABLE)->insert(array(
'key' => $key,
'value' => $value,
'user_id' => $user_id,
));
}
} }
if (in_array(false, $results, true)) {
$db->cancelTransaction();
return false; return false;
} }
// Reload the cache in session $db->closeTransaction();
function reload() SessionStorage::getInstance()->flushConfig();
{ return true;
$_SESSION['config'] = get_all();
Translator\load(get('language'));
} }

View File

@ -1,102 +0,0 @@
<?php
namespace Miniflux\Model\Database;
use DirectoryIterator;
use Miniflux\Schema;
use Miniflux\Model\Config;
// Create a new database for a new user
function create($filename, $username, $password)
{
$filename = DATA_DIRECTORY.DIRECTORY_SEPARATOR.$filename;
if (ENABLE_MULTIPLE_DB && ! file_exists($filename)) {
$db = new \PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => $filename,
));
if ($db->schema('\Miniflux\Schema')->check(Schema\VERSION)) {
$credentials = array(
'username' => $username,
'password' => password_hash($password, PASSWORD_BCRYPT)
);
$db->hashtable('settings')->put($credentials);
return true;
}
}
return false;
}
// Get or set the current database
function select($filename = '')
{
static $current_filename = DB_FILENAME;
// function gets called with a filename at least once the database
// connection is established
if (! empty($filename)) {
if (ENABLE_MULTIPLE_DB && in_array($filename, get_all())) {
$current_filename = $filename;
// unset the authenticated flag if the database is changed
if (empty($_SESSION['database']) || $_SESSION['database'] !== $filename) {
if (isset($_SESSION)) {
unset($_SESSION['loggedin']);
}
$_SESSION['database'] = $filename;
$_SESSION['config'] = Config\get_all();
}
} else {
return false;
}
}
return $current_filename;
}
// Get database path
function get_path()
{
return DATA_DIRECTORY.DIRECTORY_SEPARATOR.select();
}
// Get the list of available databases
function get_all()
{
$listing = array();
$dir = new DirectoryIterator(DATA_DIRECTORY);
foreach ($dir as $fileinfo) {
$filename = $fileinfo->getFilename();
if (preg_match('/sqlite$/', $filename)) {
$listing[] = $filename;
}
}
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;
}

View File

@ -2,44 +2,38 @@
namespace Miniflux\Model\Favicon; namespace Miniflux\Model\Favicon;
use Miniflux\Model\Config;
use Miniflux\Model\Group;
use Miniflux\Helper; use Miniflux\Helper;
use Miniflux\Model;
use PicoDb\Database; use PicoDb\Database;
use PicoFeed\Reader\Favicon; use PicoFeed\Reader\Favicon;
// Create a favicons const TABLE = 'favicons';
const JOIN_TABLE = 'favicons_feeds';
function create_feed_favicon($feed_id, $site_url, $icon_link) function create_feed_favicon($feed_id, $site_url, $icon_link)
{ {
if (has_favicon($feed_id)) { $favicon = fetch_favicon($feed_id, $site_url, $icon_link);
return true;
}
$favicon = fetch($feed_id, $site_url, $icon_link);
if ($favicon === false) { if ($favicon === false) {
return false; return false;
} }
$favicon_id = store($favicon->getType(), $favicon->getContent()); $favicon_id = store_favicon($favicon->getType(), $favicon->getContent());
if ($favicon_id === false) { if ($favicon_id === false) {
return false; return false;
} }
return Database::getInstance('db') return Database::getInstance('db')
->table('favicons_feeds') ->table(JOIN_TABLE)
->save(array( ->save(array(
'feed_id' => $feed_id, 'feed_id' => $feed_id,
'favicon_id' => $favicon_id 'favicon_id' => $favicon_id
)); ));
} }
// Download a favicon function fetch_favicon($feed_id, $site_url, $icon_link)
function fetch($feed_id, $site_url, $icon_link)
{ {
if (Config\get('favicons') == 1 && ! has_favicon($feed_id)) { if (Helper\bool_config('favicons') && ! has_favicon($feed_id)) {
$favicon = new Favicon; $favicon = new Favicon();
$favicon->find($site_url, $icon_link); $favicon->find($site_url, $icon_link);
return $favicon; return $favicon;
} }
@ -47,145 +41,125 @@ function fetch($feed_id, $site_url, $icon_link)
return false; return false;
} }
// Store the favicon (only if it does not exist yet) function store_favicon($mime_type, $blob)
function store($type, $icon)
{ {
if ($icon === '') { if (empty($blob)) {
return false; return false;
} }
$hash = sha1($icon); $hash = sha1($blob);
$favicon_id = get_favicon_id($hash); $favicon_id = get_favicon_id($hash);
if ($favicon_id) { if ($favicon_id) {
return $favicon_id; return $favicon_id;
} }
$file = $hash.Helper\favicon_extension($type); $file = $hash.Helper\favicon_extension($mime_type);
if (file_put_contents(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$file, $blob) === false) {
if (file_put_contents(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$file, $icon) === false) {
return false; return false;
} }
$saved = Database::getInstance('db') return Database::getInstance('db')
->table('favicons') ->table(TABLE)
->save(array( ->persist(array(
'hash' => $hash, 'hash' => $hash,
'type' => $type 'type' => $mime_type
)); ));
}
if ($saved === false) { function get_favicon_data_url($filename, $mime_type)
return false; {
} $blob = base64_encode(file_get_contents(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$filename));
return sprintf('data:%s;base64,%s', $mime_type, $blob);
return get_favicon_id($hash);
} }
function get_favicon_id($hash) function get_favicon_id($hash)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('favicons') ->table(TABLE)
->eq('hash', $hash) ->eq('hash', $hash)
->findOneColumn('id'); ->findOneColumn('id');
} }
// Delete the favicon function delete_favicon(array $favicon)
function delete_favicon($favicon)
{ {
unlink(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$favicon['hash'].Helper\favicon_extension($favicon['type'])); unlink(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$favicon['hash'].Helper\favicon_extension($favicon['type']));
Database::getInstance('db') Database::getInstance('db')
->table('favicons') ->table(TABLE)
->eq('hash', $favicon['hash']) ->eq('hash', $favicon['hash'])
->remove(); ->remove();
} }
// Purge orphaned favicons from database function has_favicon($feed_id)
function purge_favicons()
{ {
return Database::getInstance('db')
->table(JOIN_TABLE)
->eq('feed_id', $feed_id)
->exists();
}
function get_favicons_by_feed_ids(array $feed_ids)
{
$result = array();
if (! Helper\bool_config('favicons')) {
return $result;
}
$favicons = Database::getInstance('db') $favicons = Database::getInstance('db')
->table('favicons') ->table(TABLE)
->columns( ->columns(
'favicons.type', 'favicons.type',
'favicons.hash', 'favicons.hash',
'favicons_feeds.feed_id' 'favicons_feeds.feed_id'
) )
->join('favicons_feeds', 'favicon_id', 'id') ->join('favicons_feeds', 'favicon_id', 'id')
->isNull('favicons_feeds.feed_id') ->in('favicons_feeds.feed_id', $feed_ids)
->findAll(); ->findAll();
foreach ($favicons as $favicon) { foreach ($favicons as $favicon) {
delete_favicon($favicon); $result[$favicon['feed_id']] = $favicon;
}
}
// Return true if the feed has a favicon
function has_favicon($feed_id)
{
return Database::getInstance('db')->table('favicons_feeds')->eq('feed_id', $feed_id)->count() === 1;
}
// Get favicons for those feeds
function get_favicons(array $feed_ids)
{
if (Config\get('favicons') == 0) {
return array();
}
$result = array();
foreach ($feed_ids as $feed_id) {
$result[$feed_id] = Database::getInstance('db')
->table('favicons')
->columns(
'favicons.type',
'favicons.hash'
)
->join('favicons_feeds', 'favicon_id', 'id')
->eq('favicons_feeds.feed_id', $feed_id)
->findOne();
} }
return $result; return $result;
} }
// Get all favicons for a list of items function get_items_favicons(array $items)
function get_item_favicons(array $items)
{ {
$feed_ids = array(); $feed_ids = array();
foreach ($items as $item) { foreach ($items as $item) {
$feed_ids[$item['feed_id']] = $item['feed_id']; $feed_ids[] = $item['feed_id'];
} }
return get_favicons($feed_ids); return get_favicons_by_feed_ids(array_unique($feed_ids));
} }
// Get all favicons function get_feeds_favicons(array $feeds)
function get_all_favicons()
{ {
if (Config\get('favicons') == 0) { $feed_ids = array();
return array();
foreach ($feeds as $feed) {
$feed_ids[] = $feed['id'];
} }
$result = Database::getInstance('db') return get_favicons_by_feed_ids($feed_ids);
->table('favicons') }
->columns(
'favicons_feeds.feed_id', function get_favicons_with_data_url($user_id)
'favicons.type', {
'favicons.hash' $favicons = Database::getInstance('db')
) ->table(TABLE)
->join('favicons_feeds', 'favicon_id', 'id') ->columns(JOIN_TABLE.'.feed_id', TABLE.'.file', TABLE.'.type')
->join(JOIN_TABLE, 'favicon_id', 'id', TABLE)
->join(Model\Feed\TABLE, 'id', 'feed_id')
->eq(Model\Feed\TABLE.'.user_id', $user_id)
->findAll(); ->findAll();
$map = array(); foreach ($favicons as &$favicon) {
$favicon['url'] = get_favicon_data_url($favicon['file'], $favicon['mime_type']);
foreach ($result as $row) {
$map[$row['feed_id']] = array(
"type" => $row['type'],
"hash" => $row['hash']
);
} }
return $map; return $favicons;
} }

View File

@ -2,251 +2,65 @@
namespace Miniflux\Model\Feed; namespace Miniflux\Model\Feed;
use UnexpectedValueException;
use Miniflux\Model\Config;
use Miniflux\Model\Item; use Miniflux\Model\Item;
use Miniflux\Model\Group; use Miniflux\Model\Group;
use Miniflux\Model\Favicon;
use Miniflux\Helper;
use PicoDb\Database; use PicoDb\Database;
use PicoFeed\Reader\Reader; use PicoFeed\Parser\Feed;
use PicoFeed\PicoFeedException;
use PicoFeed\Serialization\SubscriptionListParser;
const LIMIT_ALL = -1; const STATUS_ACTIVE = 1;
const STATUS_INACTIVE = 0;
const TABLE = 'feeds';
// Update feed information function create($user_id, Feed $feed, $etag, $last_modified, $rtl = false, $scraper = false, $cloak_referrer = false)
function update(array $values)
{ {
Database::getInstance('db')->startTransaction();
$result = Database::getInstance('db')
->table('feeds')
->eq('id', $values['id'])
->save(array(
'title' => $values['title'],
'site_url' => $values['site_url'],
'feed_url' => $values['feed_url'],
'enabled' => $values['enabled'],
'rtl' => $values['rtl'],
'download_content' => $values['download_content'],
'cloak_referrer' => $values['cloak_referrer'],
'parsing_error' => 0,
));
if ($result) {
if (! Group\update_feed_groups($values['id'], $values['feed_group_ids'], $values['create_group'])) {
Database::getInstance('db')->cancelTransaction();
$result = false;
}
}
Database::getInstance('db')->closeTransaction();
return $result;
}
// Import OPML file
function import_opml($content)
{
$subscriptionList = SubscriptionListParser::create($content)->parse();
$db = Database::getInstance('db');
$db->startTransaction();
foreach ($subscriptionList->subscriptions as $subscription) {
if (! $db->table('feeds')->eq('feed_url', $subscription->getFeedUrl())->exists()) {
$db->table('feeds')->insert(array(
'title' => $subscription->getTitle(),
'site_url' => $subscription->getSiteUrl(),
'feed_url' => $subscription->getFeedUrl(),
));
if ($subscription->getCategory() !== '') {
$feed_id = $db->getLastId();
$group_id = Group\get_group_id($subscription->getCategory());
if (empty($group_id)) {
$group_id = Group\create($subscription->getCategory());
}
Group\add($feed_id, array($group_id));
}
}
}
$db->closeTransaction();
Config\write_debug();
return true;
}
// Add a new feed from an URL
function create($url, $enable_grabber = false, $force_rtl = false, $cloak_referrer = false, $group_ids = array(), $create_group = '')
{
$feed_id = false;
$db = Database::getInstance('db'); $db = Database::getInstance('db');
// Discover the feed if ($db->table('feeds')->eq('user_id', $user_id)->eq('feed_url', $feed->getFeedUrl())->exists()) {
$reader = new Reader(Config\get_reader_config()); return -1;
$resource = $reader->discover($url);
// Feed already there
if ($db->table('feeds')->eq('feed_url', $resource->getUrl())->count()) {
throw new UnexpectedValueException;
} }
// Parse the feed $feed_id = $db
$parser = $reader->getParser( ->table(TABLE)
$resource->getUrl(), ->persist(array(
$resource->getContent(), 'user_id' => $user_id,
$resource->getEncoding()
);
if ($enable_grabber) {
$parser->enableContentGrabber();
}
$feed = $parser->execute();
// Save the feed
$result = $db->table('feeds')->save(array(
'title' => $feed->getTitle(), 'title' => $feed->getTitle(),
'site_url' => $feed->getSiteUrl(), 'site_url' => $feed->getSiteUrl(),
'feed_url' => $feed->getFeedUrl(), 'feed_url' => $feed->getFeedUrl(),
'download_content' => $enable_grabber ? 1 : 0, 'download_content' => $scraper ? 1 : 0,
'rtl' => $force_rtl ? 1 : 0, 'rtl' => $rtl ? 1 : 0,
'last_modified' => $resource->getLastModified(), 'etag' => $etag,
'last_modified' => $last_modified,
'last_checked' => time(), 'last_checked' => time(),
'etag' => $resource->getEtag(),
'cloak_referrer' => $cloak_referrer ? 1 : 0, 'cloak_referrer' => $cloak_referrer ? 1 : 0,
)); ));
if ($result) { if ($feed_id !== false) {
$feed_id = $db->getLastId(); Item\update_feed_items($user_id, $feed_id, $feed->getItems(), $rtl);
Group\update_feed_groups($feed_id, $group_ids, $create_group);
Item\update_all($feed_id, $feed->getItems());
Favicon\create_feed_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
} }
return $feed_id; return $feed_id;
} }
// Refresh all feeds function get_feeds($user_id)
function refresh_all($limit = LIMIT_ALL)
{
foreach (get_ids($limit) as $feed_id) {
refresh($feed_id);
}
// Auto-vacuum for people using the cronjob
Database::getInstance('db')->getConnection()->exec('VACUUM');
return true;
}
// Refresh one feed
function refresh($feed_id)
{
try {
$feed = get($feed_id);
if (empty($feed)) {
return false;
}
$reader = new Reader(Config\get_reader_config());
$resource = $reader->download(
$feed['feed_url'],
$feed['last_modified'],
$feed['etag']
);
// Update the `last_checked` column each time, HTTP cache or not
update_last_checked($feed_id);
// Feed modified
if ($resource->isModified()) {
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
if ($feed['download_content']) {
$parser->enableContentGrabber();
// Don't fetch previous items, only new one
$parser->setGrabberIgnoreUrls(
Database::getInstance('db')->table('items')->eq('feed_id', $feed_id)->findAllByColumn('url')
);
}
$feed = $parser->execute();
update_cache($feed_id, $resource->getLastModified(), $resource->getEtag());
Item\update_all($feed_id, $feed->getItems());
Favicon\create_feed_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
}
update_parsing_error($feed_id, 0);
Config\write_debug();
return true;
} catch (PicoFeedException $e) {
}
update_parsing_error($feed_id, 1);
Config\write_debug();
return false;
}
// Get the list of feeds ID to refresh
function get_ids($limit = LIMIT_ALL)
{
$query = Database::getInstance('db')->table('feeds')->eq('enabled', 1)->asc('last_checked');
if ($limit !== LIMIT_ALL) {
$query->limit((int) $limit);
}
return $query->findAllByColumn('id');
}
// get number of feeds with errors
function count_failed_feeds()
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('feeds') ->table(TABLE)
->eq('parsing_error', '1') ->eq('user_id', $user_id)
->count();
}
// Get all feeds
function get_all()
{
return Database::getInstance('db')
->table('feeds')
->asc('title') ->asc('title')
->findAll(); ->findAll();
} }
// Get all feeds with the number unread/total items in the order failed, working, disabled function get_feeds_with_items_count($user_id)
function get_all_item_counts()
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('feeds') ->table(TABLE)
->columns( ->columns(
'feeds.*', 'feeds.*',
'SUM(CASE WHEN items.status IN ("unread") THEN 1 ELSE 0 END) as "items_unread"', 'SUM(CASE WHEN items.status IN ("unread") THEN 1 ELSE 0 END) as "items_unread"',
'SUM(CASE WHEN items.status IN ("read", "unread") THEN 1 ELSE 0 END) as "items_total"' 'SUM(CASE WHEN items.status IN ("read", "unread") THEN 1 ELSE 0 END) as "items_total"'
) )
->join('items', 'feed_id', 'id') ->join('items', 'feed_id', 'id')
->eq('feeds.user_id', $user_id)
->groupBy('feeds.id') ->groupBy('feeds.id')
->desc('feeds.parsing_error') ->desc('feeds.parsing_error')
->desc('feeds.enabled') ->desc('feeds.enabled')
@ -254,98 +68,94 @@ function get_all_item_counts()
->findAll(); ->findAll();
} }
// Get unread/total count for one feed function get_feed_ids($user_id, $limit = null)
function count_items($feed_id)
{ {
$counts = Database::getInstance('db') $query = Database::getInstance('db')
->table('items') ->table(TABLE)
->columns('status', 'count(*) as item_count') ->eq('user_id', $user_id)
->in('status', array('read', 'unread')) ->eq('enabled', STATUS_ACTIVE)
->eq('feed_id', $feed_id) ->asc('last_checked')
->groupBy('status') ->asc('id');
->findAll();
$result = array( if ($limit !== null) {
'items_unread' => 0, $query->limit($limit);
'items_total' => 0,
);
foreach ($counts as &$count) {
if ($count['status'] === 'unread') {
$result['items_unread'] = (int) $count['item_count'];
} }
$result['items_total'] += $count['item_count']; return $query->findAllByColumn('id');
}
return $result;
} }
// Get one feed function get_feed($user_id, $feed_id)
function get($feed_id)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('feeds') ->table(TABLE)
->eq('user_id', $user_id)
->eq('id', $feed_id) ->eq('id', $feed_id)
->findOne(); ->findOne();
} }
// Update parsing error column function update_feed($user_id, $feed_id, array $values)
function update_parsing_error($feed_id, $value)
{ {
Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save(array('parsing_error' => $value)); $db = Database::getInstance('db');
} $db->startTransaction();
// Update last check date $feed = $values;
function update_last_checked($feed_id) unset($feed['id']);
{ unset($feed['group_name']);
Database::getInstance('db') unset($feed['feed_group_ids']);
$result = Database::getInstance('db')
->table('feeds') ->table('feeds')
->eq('user_id', $user_id)
->eq('id', $feed_id) ->eq('id', $feed_id)
->save(array( ->save($feed);
'last_checked' => time()
)); if ($result) {
if (isset($values['feed_group_ids']) && isset($values['group_name']) &&
! Group\update_feed_groups($user_id, $values['id'], $values['feed_group_ids'], $values['group_name'])) {
$db->cancelTransaction();
return false;
}
$db->closeTransaction();
return true;
}
$db->cancelTransaction();
return false;
} }
// Update Etag and last Modified columns function change_feed_status($user_id, $feed_id, $status = STATUS_ACTIVE)
function update_cache($feed_id, $last_modified, $etag)
{ {
Database::getInstance('db') return Database::getInstance('db')
->table('feeds') ->table(TABLE)
->eq('user_id', $user_id)
->eq('id', $feed_id) ->eq('id', $feed_id)
->save(array( ->save((array('enabled' => $status)));
'last_modified' => $last_modified,
'etag' => $etag
));
} }
// Remove one feed function remove_feed($user_id, $feed_id)
function remove($feed_id)
{ {
Group\remove_all($feed_id); return Database::getInstance('db')
->table(TABLE)
// Items are removed by a sql constraint ->eq('user_id', $user_id)
$result = Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->remove(); ->eq('id', $feed_id)
Favicon\purge_favicons(); ->remove();
return $result;
} }
// Remove all feeds function count_failed_feeds($user_id)
function remove_all()
{ {
$result = Database::getInstance('db')->table('feeds')->remove(); return Database::getInstance('db')
Favicon\purge_favicons(); ->table(TABLE)
return $result; ->eq('user_id', $user_id)
->eq('parsing_error', 1)
->count();
} }
// Enable a feed (activate refresh) function count_feeds($user_id)
function enable($feed_id)
{ {
return Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 1))); return Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->count();
} }
// Disable feed
function disable($feed_id)
{
return Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 0)));
}

View File

@ -4,77 +4,47 @@ namespace Miniflux\Model\Group;
use PicoDb\Database; use PicoDb\Database;
/** const TABLE = 'groups';
* Get all groups const JOIN_TABLE = 'feeds_groups';
*
* @return array function get_all($user_id)
*/
function get_all()
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('groups') ->table(TABLE)
->eq('user_id', $user_id)
->orderBy('title') ->orderBy('title')
->findAll(); ->findAll();
} }
/** function get_groups_feed_ids($user_id)
* Get assoc array of group ids with assigned feeds ids
*
* @return array
*/
function get_map()
{ {
$result = Database::getInstance('db') $result = array();
->table('feeds_groups') $rows = Database::getInstance('db')
->table(JOIN_TABLE)
->columns('feed_id', 'group_id')
->join(TABLE, 'id', 'group_id')
->eq('user_id', $user_id)
->findAll(); ->findAll();
// TODO: add PDO::FETCH_COLUMN|PDO::FETCH_GROUP to picodb and use it instead foreach ($rows as $row) {
// of the following lines
$map = array();
foreach ($result as $row) {
$group_id = $row['group_id']; $group_id = $row['group_id'];
$feed_id = $row['feed_id']; $feed_id = $row['feed_id'];
if (isset($map[$group_id])) { if (isset($result[$group_id])) {
$map[$group_id][] = $feed_id; $result[$group_id][] = $feed_id;
} else { } else {
$map[$group_id] = array($feed_id); $result[$group_id] = array($feed_id);
} }
} }
return $map; return $result;
} }
/**
* Get assoc array of feeds ids with assigned groups ids
*
* @return array
*/
function get_feeds_map()
{
$result = Database::getInstance('db')
->table('feeds_groups')
->findAll();
$map = array();
foreach ($result as $row) {
$map[$row['feed_id']][] = $row['group_id'];
}
return $map;
}
/**
* Get all groups assigned to feed
*
* @param integer $feed_id id of the feed
* @return array
*/
function get_feed_group_ids($feed_id) function get_feed_group_ids($feed_id)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('groups') ->table(TABLE)
->join('feeds_groups', 'group_id', 'id') ->join(JOIN_TABLE, 'group_id', 'id')
->eq('feed_id', $feed_id) ->eq('feed_id', $feed_id)
->findAllByColumn('id'); ->findAllByColumn('id');
} }
@ -82,84 +52,77 @@ function get_feed_group_ids($feed_id)
function get_feed_groups($feed_id) function get_feed_groups($feed_id)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('groups') ->table(TABLE)
->columns('groups.id', 'groups.title') ->columns('groups.id', 'groups.title')
->join('feeds_groups', 'group_id', 'id') ->join(JOIN_TABLE, 'group_id', 'id')
->eq('feed_id', $feed_id) ->eq('feed_id', $feed_id)
->findAll(); ->findAll();
} }
/** function get_group_id_from_title($user_id, $title)
* Get the id of a group
*
* @param string $title group name
* @return mixed group id or false if not found
*/
function get_group_id($title)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('groups') ->table('groups')
->eq('user_id', $user_id)
->eq('title', $title) ->eq('title', $title)
->findOneColumn('id'); ->findOneColumn('id');
} }
/** function get_feed_ids_by_group($group_id)
* Get all feed ids assigned to a group
*
* @param integer $group_id
* @return array
*/
function get_feeds_by_group($group_id)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('feeds_groups') ->table(JOIN_TABLE)
->eq('group_id', $group_id) ->eq('group_id', $group_id)
->findAllByColumn('feed_id'); ->findAllByColumn('feed_id');
} }
/** function create_group($user_id, $title)
* Add a group to the Database
*
* Returns either the id of the new group or the id of an existing group with
* the same name
*
* @param string $title group name
* @return mixed id of the created group or false on error
*/
function create($title)
{ {
$data = array('title' => $title); $group_id = get_group_id_from_title($user_id, $title);
// check if the group already exists
$group_id = get_group_id($title);
// create group if missing
if ($group_id === false) { if ($group_id === false) {
Database::getInstance('db') $group_id = Database::getInstance('db')
->table('groups') ->table(TABLE)
->insert($data); ->persist(array('title' => $title, 'user_id' => $user_id));
$group_id = get_group_id($title);
} }
return $group_id; return $group_id;
} }
/** function update_feed_groups($user_id, $feed_id, array $group_ids, $group_name = '')
* Add groups to feed {
* if ($group_name !== '') {
* @param integer $feed_id feed id $group_id = create_group($user_id, $group_name);
* @param array $group_ids array of group ids if ($group_id === false) {
* @return boolean true on success, false on error return false;
*/ }
function add($feed_id, array $group_ids)
if (! in_array($group_id, $group_ids)) {
$group_ids[] = $group_id;
}
}
$assigned = get_feed_group_ids($feed_id);
$superfluous = array_diff($assigned, $group_ids);
$missing = array_diff($group_ids, $assigned);
if (! empty($superfluous) && ! dissociate_feed_groups($feed_id, $superfluous)) {
return false;
}
if (! empty($missing) && ! associate_feed_groups($feed_id, $missing)) {
return false;
}
return true;
}
function associate_feed_groups($feed_id, array $group_ids)
{ {
foreach ($group_ids as $group_id) { foreach ($group_ids as $group_id) {
$data = array('feed_id' => $feed_id, 'group_id' => $group_id);
$result = Database::getInstance('db') $result = Database::getInstance('db')
->table('feeds_groups') ->table(JOIN_TABLE)
->insert($data); ->insert(array('feed_id' => $feed_id, 'group_id' => $group_id));
if ($result === false) { if ($result === false) {
return false; return false;
@ -169,104 +132,15 @@ function add($feed_id, array $group_ids)
return true; return true;
} }
/** function dissociate_feed_groups($feed_id, array $group_ids)
* Remove groups from feed
*
* @param integer $feed_id id of the feed
* @param array $group_ids array of group ids
* @return boolean true on success, false on error
*/
function remove($feed_id, array $group_ids)
{ {
$result = Database::getInstance('db') if (empty($group_ids)) {
->table('feeds_groups') return false;
}
return Database::getInstance('db')
->table(JOIN_TABLE)
->eq('feed_id', $feed_id) ->eq('feed_id', $feed_id)
->in('group_id', $group_ids) ->in('group_id', $group_ids)
->remove(); ->remove();
// remove empty groups
if ($result) {
purge_groups();
}
return $result;
}
/**
* Remove all groups from feed
*
* @param integer $feed_id id of the feed
* @return boolean true on success, false on error
*/
function remove_all($feed_id)
{
$result = Database::getInstance('db')
->table('feeds_groups')
->eq('feed_id', $feed_id)
->remove();
// remove empty groups
if ($result) {
purge_groups();
}
return $result;
}
/**
* Purge orphaned groups from database
*/
function purge_groups()
{
$groups = Database::getInstance('db')
->table('groups')
->join('feeds_groups', 'group_id', 'id')
->isNull('feed_id')
->findAllByColumn('id');
if (! empty($groups)) {
Database::getInstance('db')
->table('groups')
->in('id', $groups)
->remove();
}
}
/**
* Update feed group associations
*
* @param integer $feed_id id of the feed to update
* @param array $group_ids valid groups ids for feed
* @param string $create_group group to create and assign to feed
* @return boolean
*/
function update_feed_groups($feed_id, array $group_ids, $create_group = '')
{
if ($create_group !== '') {
$id = create($create_group);
if ($id === false) {
return false;
}
if (! in_array($id, $group_ids)) {
$group_ids[] = $id;
}
}
$assigned = get_feed_group_ids($feed_id);
$superfluous = array_diff($assigned, $group_ids);
$missing = array_diff($group_ids, $assigned);
// remove no longer assigned groups from feed
if (! empty($superfluous) && ! remove($feed_id, $superfluous)) {
return false;
}
// add requested groups to feed
if (! empty($missing) && ! add($feed_id, $missing)) {
return false;
}
return true;
} }

View File

@ -3,157 +3,177 @@
namespace Miniflux\Model\Item; namespace Miniflux\Model\Item;
use PicoDb\Database; use PicoDb\Database;
use PicoFeed\Logging\Logger; use Miniflux\Model\Feed;
use Miniflux\Handler\Service;
use Miniflux\Model\Config;
use Miniflux\Model\Group; use Miniflux\Model\Group;
use Miniflux\Handler; use Miniflux\Handler;
use Miniflux\Helper;
use PicoFeed\Parser\Parser;
// Get all items without filtering const TABLE = 'items';
function get_all() const STATUS_UNREAD = 'unread';
const STATUS_READ = 'read';
const STATUS_REMOVED = 'removed';
function change_item_status($user_id, $item_id, $status)
{
if (! in_array($status, array(STATUS_READ, STATUS_UNREAD, STATUS_REMOVED))) {
return false;
}
return Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->eq('id', $item_id)
->save(array('status' => $status));
}
function change_items_status($user_id, $current_status, $new_status)
{
if (! in_array($new_status, array(STATUS_READ, STATUS_UNREAD, STATUS_REMOVED))) {
return false;
}
return Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->eq('status', $current_status)
->save(array('status' => $new_status));
}
function change_item_ids_status($user_id, array $item_ids, $status)
{
if (! in_array($status, array(STATUS_READ, STATUS_UNREAD, STATUS_REMOVED))) {
return false;
}
if (empty($item_ids)) {
return false;
}
return Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->in('id', $item_ids)
->save(array('status' => $status));
}
function update_feed_items($user_id, $feed_id, array $items, $rtl = false)
{
$items_in_feed = array();
$db = Database::getInstance('db');
$db->startTransaction();
foreach ($items as $item) {
if ($item->getId() && $item->getUrl()) {
$item_id = get_item_id_from_checksum($feed_id, $item->getId());
$values = array(
'title' => $item->getTitle(),
'url' => $item->getUrl(),
'updated' => $item->getDate()->getTimestamp(),
'author' => $item->getAuthor(),
'content' => Helper\bool_config('nocontent') ? '' : $item->getContent(),
'enclosure_url' => $item->getEnclosureUrl(),
'enclosure_type' => $item->getEnclosureType(),
'language' => $item->getLanguage(),
'rtl' => $rtl || Parser::isLanguageRTL($item->getLanguage()) ? 1 : 0,
);
if ($item_id > 0) {
$db
->table(TABLE)
->eq('user_id', $user_id)
->eq('feed_id', $feed_id)
->eq('id', $item_id)
->update($values);
} else {
$values['checksum'] = $item->getId();
$values['user_id'] = $user_id;
$values['feed_id'] = $feed_id;
$values['status'] = STATUS_UNREAD;
$item_id = $db->table(TABLE)->persist($values);
}
$items_in_feed[] = $item_id;
}
}
cleanup_feed_items($feed_id, $items_in_feed);
$db->closeTransaction();
}
function cleanup_feed_items($feed_id, array $items_in_feed)
{
if (! empty($items_in_feed)) {
$db = Database::getInstance('db');
$removed_items = $db
->table(TABLE)
->columns('id')
->notIn('id', $items_in_feed)
->eq('status', STATUS_REMOVED)
->eq('feed_id', $feed_id)
->desc('updated')
->findAllByColumn('id');
// Keep a buffer of 2 items
// It's workaround for buggy feeds (cache issue with some Wordpress plugins)
if (is_array($removed_items)) {
$items_to_remove = array_slice($removed_items, 2);
if (! empty($items_to_remove)) {
// Handle the case when there is a huge number of items to remove
// Sqlite have a limit of 1000 sql variables by default
// Avoid the error message "too many SQL variables"
// We remove old items by batch of 500 items
$chunks = array_chunk($items_to_remove, 500);
foreach ($chunks as $chunk) {
$db->table(TABLE)
->in('id', $chunk)
->eq('status', STATUS_REMOVED)
->eq('feed_id', $feed_id)
->remove();
}
}
}
}
}
function get_item_id_from_checksum($feed_id, $checksum)
{
return (int) Database::getInstance('db')
->table(TABLE)
->eq('feed_id', $feed_id)
->eq('checksum', $checksum)
->findOneColumn('id');
}
function get_item($user_id, $item_id)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('items') ->table('items')
->columns( ->eq('user_id', $user_id)
'items.id', ->eq('id', $item_id)
'items.title',
'items.updated',
'items.url',
'items.enclosure',
'items.enclosure_type',
'items.bookmark',
'items.feed_id',
'items.status',
'items.content',
'items.language',
'feeds.site_url',
'feeds.title AS feed_title',
'feeds.rtl'
)
->join('feeds', 'id', 'feed_id')
->in('status', array('read', 'unread'))
->orderBy('updated', 'desc')
->findAll();
}
// Get everthing since date (timestamp)
function get_all_since($timestamp)
{
return Database::getInstance('db')
->table('items')
->columns(
'items.id',
'items.title',
'items.updated',
'items.url',
'items.enclosure',
'items.enclosure_type',
'items.bookmark',
'items.feed_id',
'items.status',
'items.content',
'items.language',
'feeds.site_url',
'feeds.title AS feed_title',
'feeds.rtl'
)
->join('feeds', 'id', 'feed_id')
->in('status', array('read', 'unread'))
->gte('updated', $timestamp)
->orderBy('updated', 'desc')
->findAll();
}
function get_latest_feeds_items()
{
return Database::getInstance('db')
->table('feeds')
->columns(
'feeds.id',
'MAX(items.updated) as updated',
'items.status'
)
->join('items', 'feed_id', 'id')
->groupBy('feeds.id')
->orderBy('feeds.id')
->findAll();
}
// Get a list of [item_id => status,...]
function get_all_status()
{
return Database::getInstance('db')
->hashtable('items')
->in('status', array('read', 'unread'))
->orderBy('updated', 'desc')
->getAll('id', 'status');
}
// Get all items by status
function get_all_by_status($status, $feed_ids = array(), $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc')
{
return Database::getInstance('db')
->table('items')
->columns(
'items.id',
'items.title',
'items.updated',
'items.url',
'items.enclosure',
'items.enclosure_type',
'items.bookmark',
'items.feed_id',
'items.status',
'items.content',
'items.language',
'items.author',
'feeds.site_url',
'feeds.title AS feed_title',
'feeds.rtl'
)
->join('feeds', 'id', 'feed_id')
->eq('status', $status)
->in('feed_id', $feed_ids)
->orderBy($order_column, $order_direction)
->offset($offset)
->limit($limit)
->findAll();
}
// Get the number of items per status
function count_by_status($status, $feed_ids = array())
{
return Database::getInstance('db')
->table('items')
->eq('status', $status)
->in('feed_id', $feed_ids)
->count();
}
// Get one item by id
function get($id)
{
return Database::getInstance('db')
->table('items')
->eq('id', $id)
->findOne(); ->findOne();
} }
// Get item naviguation (next/prev items) function get_item_nav($user_id, array $item, $status = array(STATUS_UNREAD), $bookmark = array(1, 0), $feed_id = null, $group_id = null)
function get_nav($item, $status = array('unread'), $bookmark = array(1, 0), $feed_id = null, $group_id = null)
{ {
$query = Database::getInstance('db') $query = Database::getInstance('db')
->table('items') ->table(TABLE)
->columns('id', 'status', 'title', 'bookmark') ->columns('id', 'status', 'title', 'bookmark')
->neq('status', 'removed') ->neq('status', STATUS_REMOVED)
->orderBy('updated', Config\get('items_sorting_direction')); ->eq('user_id', $user_id)
->orderBy('updated', Helper\config('items_sorting_direction'))
->desc('id')
;
if ($feed_id) { if ($feed_id) {
$query->eq('feed_id', $feed_id); $query->eq('feed_id', $feed_id);
} }
if ($group_id) { if ($group_id) {
$query->in('feed_id', Group\get_feeds_by_group($group_id)); $query->in('feed_id', Group\get_feed_ids_by_group($group_id));
} }
$items = $query->findAll(); $items = $query->findAll();
@ -199,240 +219,149 @@ function get_nav($item, $status = array('unread'), $bookmark = array(1, 0), $fee
); );
} }
// Change item status to removed and clear content function get_items_by_status($user_id, $status, $feed_ids = array(), $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc')
function set_removed($id)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('items') ->table('items')
->eq('id', $id) ->columns(
->save(array('status' => 'removed', 'content' => '')); 'items.id',
'items.checksum',
'items.title',
'items.updated',
'items.url',
'items.enclosure_url',
'items.enclosure_type',
'items.bookmark',
'items.feed_id',
'items.status',
'items.content',
'items.language',
'items.rtl',
'items.author',
'feeds.site_url',
'feeds.title AS feed_title'
)
->join('feeds', 'id', 'feed_id')
->eq('items.user_id', $user_id)
->eq('items.status', $status)
->in('items.feed_id', $feed_ids)
->orderBy($order_column, $order_direction)
->offset($offset)
->limit($limit)
->findAll();
} }
// Change item status to read function get_items($user_id, $since_id = null, array $item_ids = array(), $limit = 50)
function set_read($id) {
$query = Database::getInstance('db')
->table('items')
->columns(
'items.id',
'items.checksum',
'items.title',
'items.updated',
'items.url',
'items.enclosure_url',
'items.enclosure_type',
'items.bookmark',
'items.feed_id',
'items.status',
'items.content',
'items.language',
'items.rtl',
'items.author',
'feeds.site_url',
'feeds.title AS feed_title'
)
->join('feeds', 'id', 'feed_id')
->eq('items.user_id', $user_id)
->neq('items.status', STATUS_REMOVED)
->limit($limit)
->asc('items.id');
if ($since_id !== null) {
$query->gt('items.id', $since_id);
} elseif (! empty($item_ids)) {
$query->in('items.id', $item_ids);
}
return $query->findAll();
}
function get_item_ids_by_status($user_id, $status)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('items') ->table('items')
->eq('id', $id) ->eq('user_id', $user_id)
->save(array('status' => 'read')); ->eq('status', $status)
}
// Change item status to unread
function set_unread($id)
{
return Database::getInstance('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'unread'));
}
// Change item status to "read", "unread" or "removed"
function set_status($status, array $items)
{
if (! in_array($status, array('read', 'unread', 'removed'))) {
return false;
}
return Database::getInstance('db')
->table('items')
->in('id', $items)
->save(array('status' => $status));
}
// Mark all unread items as read
function mark_all_as_read()
{
return Database::getInstance('db')
->table('items')
->eq('status', 'unread')
->save(array('status' => 'read'));
}
// Mark all read items to removed
function mark_all_as_removed()
{
return Database::getInstance('db')
->table('items')
->eq('status', 'read')
->eq('bookmark', 0)
->save(array('status' => 'removed', 'content' => ''));
}
// Mark all read items to removed after X days
function autoflush_read()
{
$autoflush = (int) Config\get('autoflush');
if ($autoflush > 0) {
// Mark read items removed after X days
Database::getInstance('db')
->table('items')
->eq('bookmark', 0)
->eq('status', 'read')
->lt('updated', strtotime('-'.$autoflush.'day'))
->save(array('status' => 'removed', 'content' => ''));
} elseif ($autoflush === -1) {
// Mark read items removed immediately
Database::getInstance('db')
->table('items')
->eq('bookmark', 0)
->eq('status', 'read')
->save(array('status' => 'removed', 'content' => ''));
}
}
// Mark all unread items to removed after X days
function autoflush_unread()
{
$autoflush = (int) Config\get('autoflush_unread');
if ($autoflush > 0) {
// Mark read items removed after X days
Database::getInstance('db')
->table('items')
->eq('bookmark', 0)
->eq('status', 'unread')
->lt('updated', strtotime('-'.$autoflush.'day'))
->save(array('status' => 'removed', 'content' => ''));
}
}
// Update all items
function update_all($feed_id, array $items)
{
$nocontent = (bool) Config\get('nocontent');
$items_in_feed = array();
$db = Database::getInstance('db');
$db->startTransaction();
foreach ($items as $item) {
Logger::setMessage('Item => '.$item->getId().' '.$item->getUrl());
// Item parsed correctly?
if ($item->getId() && $item->getUrl()) {
Logger::setMessage('Item parsed correctly');
// Get item record in database, if any
$itemrec = $db
->table('items')
->columns('enclosure')
->eq('id', $item->getId())
->findOne();
// Insert a new item
if ($itemrec === null) {
Logger::setMessage('Item added to the database');
$db->table('items')->save(array(
'id' => $item->getId(),
'title' => $item->getTitle(),
'url' => $item->getUrl(),
'updated' => $item->getDate()->getTimestamp(),
'author' => $item->getAuthor(),
'content' => $nocontent ? '' : $item->getContent(),
'status' => 'unread',
'feed_id' => $feed_id,
'enclosure' => $item->getEnclosureUrl(),
'enclosure_type' => $item->getEnclosureType(),
'language' => $item->getLanguage(),
));
} elseif (! $itemrec['enclosure'] && $item->getEnclosureUrl()) {
Logger::setMessage('Update item enclosure');
$db->table('items')->eq('id', $item->getId())->save(array(
'status' => 'unread',
'enclosure' => $item->getEnclosureUrl(),
'enclosure_type' => $item->getEnclosureType(),
));
} else {
Logger::setMessage('Item already in the database');
}
// Items inside this feed
$items_in_feed[] = $item->id;
}
}
// Cleanup old items
cleanup($feed_id, $items_in_feed);
$db->closeTransaction();
}
// Remove from the database items marked as "removed"
// and not present inside the feed
function cleanup($feed_id, array $items_in_feed)
{
if (! empty($items_in_feed)) {
$db = Database::getInstance('db');
$removed_items = $db
->table('items')
->columns('id')
->notIn('id', $items_in_feed)
->eq('status', 'removed')
->eq('feed_id', $feed_id)
->desc('updated')
->findAllByColumn('id'); ->findAllByColumn('id');
// Keep a buffer of 2 items
// It's workaround for buggy feeds (cache issue with some Wordpress plugins)
if (is_array($removed_items)) {
$items_to_remove = array_slice($removed_items, 2);
if (! empty($items_to_remove)) {
$nb_items = count($items_to_remove);
Logger::setMessage('There is '.$nb_items.' items to remove');
// Handle the case when there is a huge number of items to remove
// Sqlite have a limit of 1000 sql variables by default
// Avoid the error message "too many SQL variables"
// We remove old items by batch of 500 items
$chunks = array_chunk($items_to_remove, 500);
foreach ($chunks as $chunk) {
$db->table('items')
->in('id', $chunk)
->eq('status', 'removed')
->eq('feed_id', $feed_id)
->remove();
}
}
}
}
} }
// Download item content function get_latest_feeds_items($user_id)
function download_contents($item_id)
{ {
$item = get($item_id); return Database::getInstance('db')
$content = Handler\Scraper\download_contents($item['url']); ->table(Feed\TABLE)
->columns(
if (! empty($content)) { 'feeds.id',
if (! Config\get('nocontent')) { 'MAX(items.updated) as updated',
Database::getInstance('db') 'items.status'
->table('items') )
->eq('id', $item['id']) ->join(TABLE, 'feed_id', 'id')
->save(array('content' => $content)); ->eq('feeds.user_id', $user_id)
} ->groupBy('feeds.id')
->orderBy('feeds.id')
Config\write_debug(); ->findAll();
}
return array(
'result' => true, function count_by_status($user_id, $status, $feed_ids = array())
'content' => $content {
); $query = Database::getInstance('db')
} ->table('items')
->eq('user_id', $user_id)
Config\write_debug(); ->in('feed_id', $feed_ids);
return array( if (is_array($status)) {
'result' => false, $query->in('status', $status);
'content' => '' } else {
); $query->eq('status', $status);
}
return $query->count();
}
function autoflush_read($user_id)
{
$autoflush = Helper\int_config('autoflush');
if ($autoflush > 0) {
Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->eq('bookmark', 0)
->eq('status', STATUS_READ)
->lt('updated', strtotime('-'.$autoflush.'day'))
->save(array('status' => STATUS_REMOVED, 'content' => ''));
} elseif ($autoflush === -1) {
Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->eq('bookmark', 0)
->eq('status', STATUS_READ)
->save(array('status' => STATUS_REMOVED, 'content' => ''));
}
}
function autoflush_unread($user_id)
{
$autoflush = Helper\int_config('autoflush_unread');
if ($autoflush > 0) {
Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->eq('bookmark', 0)
->eq('status', STATUS_UNREAD)
->lt('updated', strtotime('-'.$autoflush.'day'))
->save(array('status' => STATUS_REMOVED, 'content' => ''));
}
} }

View File

@ -2,18 +2,48 @@
namespace Miniflux\Model\ItemFeed; namespace Miniflux\Model\ItemFeed;
use Miniflux\Model\Feed;
use Miniflux\Model\Item;
use PicoDb\Database; use PicoDb\Database;
function count_items($feed_id) function count_items_by_status($user_id, $feed_id)
{
$counts = Database::getInstance('db')
->table(Item\TABLE)
->columns('status', 'count(*) as item_count')
->in('status', array(Item\STATUS_READ, Item\STATUS_UNREAD))
->eq('user_id', $user_id)
->eq('feed_id', $feed_id)
->groupBy('status')
->findAll();
$result = array(
'items_unread' => 0,
'items_total' => 0,
);
foreach ($counts as &$count) {
if ($count['status'] === Item\STATUS_UNREAD) {
$result['items_unread'] = (int) $count['item_count'];
}
$result['items_total'] += $count['item_count'];
}
return $result;
}
function count_items($user_id, $feed_id)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('items') ->table(Item\TABLE)
->eq('feed_id', $feed_id) ->eq('feed_id', $feed_id)
->in('status', array('unread', 'read')) ->eq('user_id', $user_id)
->in('status', array(Item\STATUS_READ, Item\STATUS_UNREAD))
->count(); ->count();
} }
function get_all_items($feed_id, $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc') function get_all_items($user_id, $feed_id, $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc')
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('items') ->table('items')
@ -22,31 +52,39 @@ function get_all_items($feed_id, $offset = null, $limit = null, $order_column =
'items.title', 'items.title',
'items.updated', 'items.updated',
'items.url', 'items.url',
'items.enclosure', 'items.enclosure_url',
'items.enclosure_type', 'items.enclosure_type',
'items.feed_id', 'items.feed_id',
'items.status', 'items.status',
'items.content', 'items.content',
'items.bookmark', 'items.bookmark',
'items.language', 'items.language',
'items.rtl',
'items.author', 'items.author',
'feeds.site_url', 'feeds.site_url',
'feeds.rtl' 'feeds.title AS feed_title'
) )
->join('feeds', 'id', 'feed_id') ->join(Feed\TABLE, 'id', 'feed_id')
->in('status', array('unread', 'read')) ->in('status', array(Item\STATUS_UNREAD, Item\STATUS_READ))
->eq('feed_id', $feed_id) ->eq('items.feed_id', $feed_id)
->eq('items.user_id', $user_id)
->orderBy($order_column, $order_direction) ->orderBy($order_column, $order_direction)
->offset($offset) ->offset($offset)
->limit($limit) ->limit($limit)
->findAll(); ->findAll();
} }
function mark_all_as_read($feed_id) function change_items_status($user_id, $feed_id, $current_status, $new_status, $before = null)
{ {
return Database::getInstance('db') $query = Database::getInstance('db')
->table('items') ->table(Item\TABLE)
->eq('status', 'unread') ->eq('status', $current_status)
->eq('feed_id', $feed_id) ->eq('feed_id', $feed_id)
->update(array('status' => 'read')); ->eq('user_id', $user_id);
if ($before !== null) {
$query->lte('updated', $before);
}
return $query->update(array('status' => $new_status));
} }

View File

@ -2,28 +2,27 @@
namespace Miniflux\Model\ItemGroup; namespace Miniflux\Model\ItemGroup;
use PicoDb\Database; use Miniflux\Model\Item;
use Miniflux\Model\Group; use Miniflux\Model\Group;
use PicoDb\Database;
function mark_all_as_read($group_id) function change_items_status($user_id, $group_id, $current_status, $new_status, $before = null)
{ {
$feed_ids = Group\get_feeds_by_group($group_id); $feed_ids = Group\get_feed_ids_by_group($group_id);
return Database::getInstance('db') if (empty($feed_ids)) {
->table('items') return false;
->eq('status', 'unread') }
->in('feed_id', $feed_ids)
->update(array('status' => 'read')); $query = Database::getInstance('db')
} ->table(Item\TABLE)
->eq('user_id', $user_id)
function mark_all_as_removed($group_id) ->eq('status', $current_status)
{ ->in('feed_id', $feed_ids);
$feed_ids = Group\get_feeds_by_group($group_id);
if ($before !== null) {
return Database::getInstance('db') $query->lte('updated', $before);
->table('items') }
->eq('status', 'read')
->eq('bookmark', 0) return $query->update(array('status' => $new_status));
->in('feed_id', $feed_ids)
->save(array('status' => 'removed', 'content' => ''));
} }

View File

@ -2,24 +2,16 @@
namespace Miniflux\Model\RememberMe; namespace Miniflux\Model\RememberMe;
use PicoDb\Database; use Miniflux\Session\SessionStorage;
use Miniflux\Helper; use Miniflux\Helper;
use Miniflux\Model\Config; use Miniflux\Model\User;
use Miniflux\Model\Database as DatabaseModel; use PicoDb\Database;
const TABLE = 'remember_me'; const TABLE = 'remember_me';
const COOKIE_NAME = '_R_'; const COOKIE_NAME = '_R_';
const EXPIRATION = 5184000; const EXPIRATION = 5184000;
/** function get_record($token, $sequence)
* Get a remember me record
*
* @access public
* @param string $token
* @param string $sequence
* @return mixed
*/
function find($token, $sequence)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table(TABLE) ->table(TABLE)
@ -29,46 +21,16 @@ function find($token, $sequence)
->findOne(); ->findOne();
} }
/**
* Get all sessions
*
* @access public
* @return array
*/
function get_all()
{
return Database::getInstance('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() function authenticate()
{ {
$credentials = read_cookie(); $credentials = read_cookie();
if ($credentials !== false) { if ($credentials !== false) {
$record = find($credentials['token'], $credentials['sequence']); $record = get_record($credentials['token'], $credentials['sequence']);
if ($record) { if ($record) {
$user = User\get_user_by_id($record['user_id']);
// Update the sequence SessionStorage::getInstance()->setUser($user);
write_cookie(
$record['token'],
update($record['token']),
$record['expiration']
);
// mark user as sucessfull logged in
$_SESSION['loggedin'] = true;
return true; return true;
} }
} }
@ -76,35 +38,6 @@ function authenticate()
return false; 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['expiration']
);
}
}
}
/**
* Remove the current RememberMe session and the cookie
*
* @access public
*/
function destroy() function destroy()
{ {
$credentials = read_cookie(); $credentials = read_cookie();
@ -119,19 +52,9 @@ function destroy()
delete_cookie(); delete_cookie();
} }
/** function create($user_id, $ip, $user_agent)
* 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.Helper\generate_token()); $token = hash('sha256', $user_id.$user_agent.$ip.Helper\generate_token());
$sequence = Helper\generate_token(); $sequence = Helper\generate_token();
$expiration = time() + EXPIRATION; $expiration = time() + EXPIRATION;
@ -140,7 +63,7 @@ function create($dbname, $username, $ip, $user_agent)
Database::getInstance('db') Database::getInstance('db')
->table(TABLE) ->table(TABLE)
->insert(array( ->insert(array(
'username' => $username, 'user_id' => $user_id,
'ip' => $ip, 'ip' => $ip,
'user_agent' => $user_agent, 'user_agent' => $user_agent,
'token' => $token, 'token' => $token,
@ -156,12 +79,6 @@ function create($dbname, $username, $ip, $user_agent)
); );
} }
/**
* Remove old sessions
*
* @access public
* @return bool
*/
function cleanup() function cleanup()
{ {
return Database::getInstance('db') return Database::getInstance('db')
@ -170,13 +87,6 @@ function cleanup()
->remove(); ->remove();
} }
/**
* Return a new sequence token and update the database
*
* @access public
* @param string $token Session token
* @return string
*/
function update($token) function update($token)
{ {
$new_sequence = Helper\generate_token(); $new_sequence = Helper\generate_token();
@ -189,33 +99,14 @@ function update($token)
return $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) function encode_cookie($token, $sequence)
{ {
return implode('|', array(base64_encode(DatabaseModel\select()), $token, $sequence)); return implode('|', array($token, $sequence));
} }
/**
* Decode the value of a cookie
*
* @access public
* @param string $value Raw cookie data
* @return array
*/
function decode_cookie($value) function decode_cookie($value)
{ {
@list($database, $token, $sequence) = explode('|', $value); @list($token, $sequence) = explode('|', $value);
if (ENABLE_MULTIPLE_DB && ! DatabaseModel\select(base64_decode($database))) {
return false;
}
return array( return array(
'token' => $token, 'token' => $token,
@ -223,25 +114,11 @@ function decode_cookie($value)
); );
} }
/**
* Return true if the current user has a RememberMe cookie
*
* @access public
* @return bool
*/
function has_cookie() function has_cookie()
{ {
return ! empty($_COOKIE[COOKIE_NAME]); 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) function write_cookie($token, $sequence, $expiration)
{ {
setcookie( setcookie(
@ -255,12 +132,6 @@ function write_cookie($token, $sequence, $expiration)
); );
} }
/**
* Read and decode the cookie
*
* @access public
* @return mixed
*/
function read_cookie() function read_cookie()
{ {
if (empty($_COOKIE[COOKIE_NAME])) { if (empty($_COOKIE[COOKIE_NAME])) {
@ -270,11 +141,6 @@ function read_cookie()
return decode_cookie($_COOKIE[COOKIE_NAME]); return decode_cookie($_COOKIE[COOKIE_NAME]);
} }
/**
* Remove the cookie
*
* @access public
*/
function delete_cookie() function delete_cookie()
{ {
setcookie( setcookie(

View File

@ -1,43 +1,47 @@
<?php <?php
namespace Miniflux\Model\Search; namespace Miniflux\Model\ItemSearch;
use PicoDb\Database; use PicoDb\Database;
use Miniflux\Model\Feed;
use Miniflux\Model\Item;
function count_items($text) function count_items($user_id, $text)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('items') ->table(Item\TABLE)
->neq('status', 'removed') ->eq('user_id', $user_id)
->neq('status', Item\STATUS_REMOVED)
->ilike('title', '%' . $text . '%') ->ilike('title', '%' . $text . '%')
->count(); ->count();
} }
function get_all_items($text, $offset = null, $limit = null) function get_all_items($user_id, $text, $offset = null, $limit = null)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table('items') ->table(Item\TABLE)
->columns( ->columns(
'items.id', 'items.id',
'items.title', 'items.title',
'items.updated', 'items.updated',
'items.url', 'items.url',
'items.enclosure', 'items.enclosure_url',
'items.enclosure_type', 'items.enclosure_type',
'items.bookmark', 'items.bookmark',
'items.feed_id', 'items.feed_id',
'items.status', 'items.status',
'items.content', 'items.content',
'items.language', 'items.language',
'items.rtl',
'items.author', 'items.author',
'feeds.site_url', 'feeds.site_url',
'feeds.title AS feed_title', 'feeds.title AS feed_title'
'feeds.rtl'
) )
->join('feeds', 'id', 'feed_id') ->join(Feed\TABLE, 'id', 'feed_id')
->neq('status', 'removed') ->eq('items.user_id', $user_id)
->neq('items.status', Item\STATUS_REMOVED)
->ilike('items.title', '%' . $text . '%') ->ilike('items.title', '%' . $text . '%')
->orderBy('updated', 'desc') ->orderBy('items.updated', 'desc')
->offset($offset) ->offset($offset)
->limit($limit) ->limit($limit)
->findAll(); ->findAll();

View File

@ -3,37 +3,142 @@
namespace Miniflux\Model\User; namespace Miniflux\Model\User;
use PicoDb\Database; use PicoDb\Database;
use Miniflux\Session; use Miniflux\Helper;
use Miniflux\Request;
use Miniflux\Model\Config;
use Miniflux\Model\RememberMe;
use Miniflux\Model\Database as DatabaseModel;
// Check if the user is logged in const TABLE = 'users';
function is_loggedin()
function create_user($username, $password, $is_admin = false)
{ {
return ! empty($_SESSION['loggedin']); list($fever_token, $fever_api_key) = generate_fever_api_key($username);
return Database::getInstance('db')
->table(TABLE)
->persist(array(
'username' => $username,
'password' => password_hash($password, PASSWORD_BCRYPT),
'is_admin' => (int) $is_admin,
'api_token' => Helper\generate_token(),
'bookmarklet_token' => Helper\generate_token(),
'cronjob_token' => Helper\generate_token(),
'feed_token' => Helper\generate_token(),
'fever_token' => $fever_token,
'fever_api_key' => $fever_api_key,
));
} }
// Destroy the session and the rememberMe cookie function update_user($user_id, $username, $password = null, $is_admin = null)
function logout()
{ {
RememberMe\destroy(); $user = get_user_by_id($user_id);
Session\close(); $values = array();
if ($user['username'] !== $username) {
list($fever_token, $fever_api_key) = generate_fever_api_key($user['username']);
$values['username'] = $username;
$values['fever_token'] = $fever_token;
$values['fever_api_key'] = $fever_api_key;
}
if ($password !== null) {
$values['password'] = password_hash($password, PASSWORD_BCRYPT);
}
if ($is_admin !== null) {
$values['is_admin'] = (int) $is_admin;
}
if (! empty($values)) {
return Database::getInstance('db')
->table(TABLE)
->eq('id', $user_id)
->update($values);
}
return true;
} }
// Get the credentials from the current selected database function regenerate_tokens($user_id)
function get_credentials() {
$user = get_user_by_id($user_id);
list($fever_token, $fever_api_key) = generate_fever_api_key($user['username']);
return Database::getInstance('db')
->table(TABLE)
->eq('id', $user_id)
->update(array(
'api_token' => Helper\generate_token(),
'bookmarklet_token' => Helper\generate_token(),
'cronjob_token' => Helper\generate_token(),
'feed_token' => Helper\generate_token(),
'fever_token' => $fever_token,
'fever_api_key' => $fever_api_key,
));
}
function remove_user($user_id)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->hashtable('settings') ->table(TABLE)
->get('username', 'password'); ->eq('id', $user_id)
->remove();
} }
// Set last login date function generate_fever_api_key($username)
function set_last_login() {
$token = Helper\generate_token();
$api_key = md5($username . ':' . $token);
return array($token, $api_key);
}
function get_all_users()
{ {
return Database::getInstance('db') return Database::getInstance('db')
->hashtable('settings') ->table(TABLE)
->put(array('last_login' => time())); ->columns('id', 'username', 'is_admin', 'last_login')
->asc('username')
->asc('id')
->findAll();
}
function get_user_by_id($user_id)
{
return Database::getInstance('db')
->table(TABLE)
->eq('id', $user_id)
->findOne();
}
function get_user_by_id_without_password($user_id)
{
$user = Database::getInstance('db')
->table(TABLE)
->eq('id', $user_id)
->findOne();
unset($user['password']);
return $user;
}
function get_user_by_username($username)
{
return Database::getInstance('db')
->table(TABLE)
->eq('username', $username)
->findOne();
}
function get_user_by_token($key, $token)
{
return Database::getInstance('db')
->table(TABLE)
->eq($key, $token)
->findOne();
}
function set_last_login_date($user_id)
{
return Database::getInstance('db')
->table(TABLE)
->eq('id', $user_id)
->update(array('last_login' => time()));
} }

View File

@ -5,396 +5,129 @@ namespace Miniflux\Schema;
use PDO; use PDO;
use Miniflux\Helper; use Miniflux\Helper;
const VERSION = 44; const VERSION = 1;
function version_1(PDO $pdo)
function version_44(PDO $pdo)
{ {
$pdo->exec('INSERT INTO settings ("key", "value") VALUES ("item_title_link", "full")'); $pdo->exec('CREATE TABLE users (
}
function version_43(PDO $pdo)
{
$pdo->exec('DROP TABLE favicons');
$pdo->exec(
'CREATE TABLE favicons (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
hash TEXT UNIQUE, username TEXT UNIQUE,
type TEXT password TEXT NOT NULL,
)' is_admin INTEGER DEFAULT 0,
); last_login INTEGER,
api_token TEXT NOT NULL UNIQUE,
bookmarklet_token TEXT NOT NULL UNIQUE,
cronjob_token TEXT NOT NULL UNIQUE,
feed_token TEXT NOT NULL UNIQUE,
fever_token TEXT NOT NULL UNIQUE,
fever_api_key TEXT NOT NULL UNIQUE
)');
$pdo->exec(' $pdo->exec('CREATE TABLE user_settings (
CREATE TABLE "favicons_feeds" ( "user_id" INTEGER NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
PRIMARY KEY("user_id", "key"),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)');
$pdo->exec('CREATE TABLE feeds (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
feed_url TEXT NOT NULL,
site_url TEXT,
title TEXT COLLATE NOCASE,
last_checked INTEGER DEFAULT 0,
last_modified TEXT,
etag TEXT,
enabled INTEGER DEFAULT 1,
download_content INTEGER DEFAULT 0,
parsing_error INTEGER DEFAULT 0,
rtl INTEGER DEFAULT 0,
cloak_referrer INTEGER DEFAULT 0,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, feed_url)
)');
$pdo->exec('CREATE TABLE items (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
feed_id INTEGER NOT NULL, feed_id INTEGER NOT NULL,
favicon_id INTEGER NOT NULL, checksum TEXT NOT NULL,
PRIMARY KEY(feed_id, favicon_id), status TEXT,
FOREIGN KEY(favicon_id) REFERENCES favicons(id) ON DELETE CASCADE, bookmark INTEGER DEFAULT 0,
FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE url TEXT,
) title TEXT COLLATE NOCASE,
'); author TEXT,
} content TEXT,
updated INTEGER,
enclosure_url TEXT,
enclosure_type TEXT,
language TEXT,
rtl INTEGER DEFAULT 0,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE,
UNIQUE(feed_id, checksum)
)');
function version_42(PDO $pdo) $pdo->exec('CREATE TABLE "groups" (
{
$pdo->exec('DROP TABLE favicons');
$pdo->exec(
'CREATE TABLE favicons (
feed_id INTEGER UNIQUE,
link TEXT,
file TEXT,
type TEXT,
FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
)'
);
}
function version_41(PDO $pdo)
{
$pdo->exec('
CREATE TABLE "groups" (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
title TEXT user_id INTEGER NOT NULL,
) title TEXT COLLATE NOCASE,
'); FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, title)
)');
$pdo->exec(' $pdo->exec('CREATE TABLE "feeds_groups" (
CREATE TABLE "feeds_groups" (
feed_id INTEGER NOT NULL, feed_id INTEGER NOT NULL,
group_id INTEGER NOT NULL, group_id INTEGER NOT NULL,
PRIMARY KEY(feed_id, group_id), PRIMARY KEY(feed_id, group_id),
FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE, FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
) )');
');
}
function version_40(PDO $pdo) $pdo->exec('CREATE TABLE favicons (
{
$pdo->exec('UPDATE settings SET "value"="https://github.com/miniflux/miniflux/archive/master.zip" WHERE "key"="auto_update_url"');
}
function version_39(PDO $pdo)
{
$pdo->exec('ALTER TABLE feeds ADD COLUMN cloak_referrer INTEGER DEFAULT 0');
}
function version_38(PDO $pdo)
{
$pdo->exec('INSERT INTO settings ("key", "value") VALUES ("original_marks_read", 1)');
}
function version_37(PDO $pdo)
{
$pdo->exec('INSERT INTO settings ("key", "value") VALUES ("debug_mode", 0)');
}
function version_36(PDO $pdo)
{
$pdo->exec('INSERT INTO settings ("key", "value") VALUES ("frontend_updatecheck_interval", 10)');
}
function version_35(PDO $pdo)
{
$pdo->exec('DELETE FROM favicons WHERE icon = ""');
$pdo->exec('
CREATE TABLE settings (
"key" TEXT NOT NULL UNIQUE,
"value" TEXT Default NULL,
PRIMARY KEY(key)
)
');
$pdo->exec("
INSERT INTO settings (key,value)
SELECT 'username', username FROM config UNION
SELECT 'password', password FROM config UNION
SELECT 'language', language FROM config UNION
SELECT 'autoflush', autoflush FROM config UNION
SELECT 'nocontent', nocontent FROM config UNION
SELECT 'items_per_page', items_per_page FROM config UNION
SELECT 'theme', theme FROM config UNION
SELECT 'api_token', api_token FROM config UNION
SELECT 'feed_token', feed_token FROM config UNION
SELECT 'items_sorting_direction', items_sorting_direction FROM config UNION
SELECT 'redirect_nothing_to_read', redirect_nothing_to_read FROM config UNION
SELECT 'timezone', timezone FROM config UNION
SELECT 'auto_update_url', auto_update_url FROM config UNION
SELECT 'bookmarklet_token', bookmarklet_token FROM config UNION
SELECT 'items_display_mode', items_display_mode FROM config UNION
SELECT 'fever_token', fever_token FROM config UNION
SELECT 'autoflush_unread', autoflush_unread FROM config UNION
SELECT 'pinboard_enabled', pinboard_enabled FROM config UNION
SELECT 'pinboard_token', pinboard_token FROM config UNION
SELECT 'pinboard_tags', pinboard_tags FROM config UNION
SELECT 'instapaper_enabled', instapaper_enabled FROM config UNION
SELECT 'instapaper_username', instapaper_username FROM config UNION
SELECT 'instapaper_password', instapaper_password FROM config UNION
SELECT 'image_proxy', image_proxy FROM config UNION
SELECT 'favicons', favicons FROM config
");
$pdo->exec('DROP TABLE config');
}
function version_34(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN favicons INTEGER DEFAULT 0');
$pdo->exec(
'CREATE TABLE favicons (
feed_id INTEGER UNIQUE,
link TEXT,
icon TEXT,
FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
)'
);
}
function version_33(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN image_proxy INTEGER DEFAULT 0');
}
function version_32(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN instapaper_enabled INTEGER DEFAULT 0');
$pdo->exec('ALTER TABLE config ADD COLUMN instapaper_username TEXT');
$pdo->exec('ALTER TABLE config ADD COLUMN instapaper_password TEXT');
}
function version_31(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN pinboard_enabled INTEGER DEFAULT 0');
$pdo->exec('ALTER TABLE config ADD COLUMN pinboard_token TEXT');
$pdo->exec('ALTER TABLE config ADD COLUMN pinboard_tags TEXT DEFAULT "miniflux"');
}
function version_30(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN autoflush_unread INTEGER DEFAULT 45');
}
function version_29(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN fever_token INTEGER DEFAULT "'.substr(Helper\generate_token(), 0, 8).'"');
}
function version_28(PDO $pdo)
{
$pdo->exec('ALTER TABLE feeds ADD COLUMN rtl INTEGER DEFAULT 0');
}
function version_27(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN items_display_mode TEXT DEFAULT "summaries"');
}
function version_26(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN bookmarklet_token TEXT DEFAULT "'.Helper\generate_token().'"');
}
function version_25(PDO $pdo)
{
$pdo->exec(
'CREATE TABLE remember_me (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
username TEXT, hash TEXT UNIQUE,
type TEXT
)');
$pdo->exec('CREATE TABLE "favicons_feeds" (
feed_id INTEGER NOT NULL,
favicon_id INTEGER NOT NULL,
PRIMARY KEY(feed_id, favicon_id),
FOREIGN KEY(favicon_id) REFERENCES favicons(id) ON DELETE CASCADE,
FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
)');
$pdo->exec('CREATE TABLE remember_me (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
ip TEXT, ip TEXT,
user_agent TEXT, user_agent TEXT,
token TEXT, token TEXT,
sequence TEXT, sequence TEXT,
expiration INTEGER, expiration INTEGER,
date_creation INTEGER date_creation INTEGER,
)' FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
); )');
}
function version_24(PDO $pdo) $fever_token = Helper\generate_token();
{
$pdo->exec("ALTER TABLE config ADD COLUMN auto_update_url TEXT DEFAULT 'https://github.com/fguillot/miniflux/archive/master.zip'");
}
function version_23(PDO $pdo)
{
$pdo->exec('ALTER TABLE items ADD COLUMN language TEXT');
}
function version_22(PDO $pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN timezone TEXT DEFAULT 'UTC'");
}
function version_21(PDO $pdo)
{
$pdo->exec('ALTER TABLE items ADD COLUMN enclosure TEXT');
$pdo->exec('ALTER TABLE items ADD COLUMN enclosure_type TEXT');
}
function version_20(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN redirect_nothing_to_read TEXT DEFAULT "feeds"');
}
function version_19(PDO $pdo)
{
$rq = $pdo->prepare('SELECT autoflush FROM config');
$rq->execute();
$value = (int) $rq->fetchColumn();
// Change default value of autoflush to 15 days to avoid very large database
if ($value <= 0) {
$rq = $pdo->prepare('UPDATE config SET autoflush=?');
$rq->execute(array(15));
}
}
function version_18(PDO $pdo)
{
$pdo->exec('ALTER TABLE feeds ADD COLUMN parsing_error INTEGER DEFAULT 0');
}
function version_17(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN items_sorting_direction TEXT DEFAULT "desc"');
}
function version_16(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN auth_google_token TEXT DEFAULT ""');
$pdo->exec('ALTER TABLE config ADD COLUMN auth_mozilla_token TEXT DEFAULT ""');
}
function version_15(PDO $pdo)
{
$pdo->exec('ALTER TABLE feeds ADD COLUMN download_content INTEGER DEFAULT 0');
}
function version_14(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN feed_token TEXT DEFAULT "'.Helper\generate_token().'"');
}
function version_13(PDO $pdo)
{
$pdo->exec('ALTER TABLE feeds ADD COLUMN enabled INTEGER DEFAULT 1');
}
function version_12(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT "'.Helper\generate_token().'"');
}
function version_11(PDO $pdo)
{
$rq = $pdo->prepare(' $rq = $pdo->prepare('
SELECT INSERT INTO users
items.id, items.url AS item_url, feeds.site_url (username, password, is_admin, api_token, bookmarklet_token, cronjob_token, feed_token, fever_token, fever_api_key)
FROM items VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
LEFT JOIN feeds ON feeds.id=items.feed_id
'); ');
$rq->execute(); $rq->execute(array(
'admin',
$items = $rq->fetchAll(PDO::FETCH_ASSOC); password_hash('admin', PASSWORD_BCRYPT),
'1',
foreach ($items as $item) { Helper\generate_token(),
if ($item['id'] !== $item['item_url']) { Helper\generate_token(),
$id = hash('crc32b', $item['id'].$item['site_url']); Helper\generate_token(),
} else { Helper\generate_token(),
$id = hash('crc32b', $item['item_url'].$item['site_url']); $fever_token,
} md5('admin:'.$fever_token),
));
$rq = $pdo->prepare('UPDATE items SET id=? WHERE id=?');
$rq->execute(array($id, $item['id']));
}
}
function version_10(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN theme TEXT DEFAULT "original"');
}
function version_9(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN items_per_page INTEGER DEFAULT 100');
}
function version_8(PDO $pdo)
{
$pdo->exec('ALTER TABLE items ADD COLUMN bookmark INTEGER DEFAULT 0');
}
function version_7(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN nocontent INTEGER DEFAULT 0');
}
function version_6(PDO $pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN autoflush INTEGER DEFAULT 0');
}
function version_5(PDO $pdo)
{
$pdo->exec('ALTER TABLE feeds ADD COLUMN last_checked INTEGER');
}
function version_4(PDO $pdo)
{
$pdo->exec('CREATE INDEX idx_status ON items(status)');
}
function version_3(PDO $pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN language TEXT DEFAULT 'en_US'");
}
function version_2(PDO $pdo)
{
$pdo->exec('ALTER TABLE feeds ADD COLUMN last_modified TEXT');
$pdo->exec('ALTER TABLE feeds ADD COLUMN etag TEXT');
}
function version_1(PDO $pdo)
{
$pdo->exec("
CREATE TABLE config (
username TEXT DEFAULT 'admin',
password TEXT
)
");
$pdo->exec("
INSERT INTO config
(password)
VALUES ('".\password_hash('admin', PASSWORD_BCRYPT)."')
");
$pdo->exec('
CREATE TABLE feeds (
id INTEGER PRIMARY KEY,
site_url TEXT,
feed_url TEXT UNIQUE,
title TEXT COLLATE NOCASE
)
');
$pdo->exec('
CREATE TABLE items (
id TEXT PRIMARY KEY,
url TEXT,
title TEXT,
author TEXT,
content TEXT,
updated INTEGER,
status TEXT,
feed_id INTEGER,
FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
)
');
} }

View File

@ -3,9 +3,12 @@
<nav> <nav>
<ul> <ul>
<li><a href="?action=config"><?php echo t('general') ?></a></li> <li><a href="?action=config"><?php echo t('general') ?></a></li>
<li><a href="?action=profile"><?php echo t('profile') ?></a></li>
<?php if (Miniflux\Helper\is_admin()): ?>
<li><a href="?action=users"><?php echo t('users') ?></a></li>
<?php endif ?>
<li><a href="?action=services"><?php echo t('external services') ?></a></li> <li><a href="?action=services"><?php echo t('external services') ?></a></li>
<li><a href="?action=api"><?php echo t('api') ?></a></li> <li><a href="?action=api"><?php echo t('api') ?></a></li>
<li><a href="?action=database"><?php echo t('database') ?></a></li>
<li><a href="?action=help"><?php echo t('help') ?></a></li> <li><a href="?action=help"><?php echo t('help') ?></a></li>
<li class="active"><a href="?action=about"><?php echo t('about') ?></a></li> <li class="active"><a href="?action=about"><?php echo t('about') ?></a></li>
</ul> </ul>
@ -16,24 +19,26 @@
<h3><?php echo t('Bookmarks') ?></h3> <h3><?php echo t('Bookmarks') ?></h3>
<ul> <ul>
<li> <li>
<a href="<?php echo Miniflux\Helper\get_current_base_url(), '?action=bookmark-feed&amp;database=', urlencode($db_name), '&amp;token=', urlencode($config['feed_token']) ?>" target="_blank"><?php echo t('Bookmark RSS Feed') ?></a> <a href="<?php echo Miniflux\Helper\get_current_base_url(), '?action=bookmark-feed&amp;token=', urlencode($user['feed_token']) ?>" target="_blank"><?php echo t('Bookmark RSS Feed') ?></a>
</li> </li>
</ul> </ul>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<h3><?php echo t('Bookmarklet') ?></h3> <h3><?php echo t('Bookmarklet') ?></h3>
<a class="bookmarklet" href="javascript:location.href='<?php echo Miniflux\Helper\get_current_base_url() ?>?action=subscribe&amp;token=<?php echo urlencode($config['bookmarklet_token']) ?>&amp;url='+encodeURIComponent(location.href)"><?php echo t('Subscribe with Miniflux') ?></a> (<?php echo t('Drag and drop this link to your bookmarks') ?>) <a class="bookmarklet" href="javascript:location.href='<?php echo Miniflux\Helper\get_current_base_url() ?>?action=subscribe&amp;token=<?php echo urlencode($user['bookmarklet_token']) ?>&amp;url='+encodeURIComponent(location.href)"><?php echo t('Subscribe with Miniflux') ?></a> (<?php echo t('Drag and drop this link to your bookmarks') ?>)
<input type="text" class="auto-select" readonly="readonly" value="javascript:location.href='<?php echo Miniflux\Helper\get_current_base_url() ?>?action=subscribe&amp;token=<?php echo urlencode($config['bookmarklet_token']) ?>&amp;url='+encodeURIComponent(location.href)"/> <input type="text" class="auto-select" readonly="readonly" value="javascript:location.href='<?php echo Miniflux\Helper\get_current_base_url() ?>?action=subscribe&amp;token=<?php echo urlencode($user['bookmarklet_token']) ?>&amp;url='+encodeURIComponent(location.href)"/>
</div> </div>
<?php if (ENABLE_CRONJOB_HTTP_ACCESS): ?>
<div class="panel panel-default">
<h3><?php echo t('Cronjob URL') ?></h3>
<input type="text" class="auto-select" readonly="readonly" value="<?php echo Miniflux\Helper\get_current_base_url(), 'cronjob.php?token=', urlencode($user['cronjob_token']) ?>">
</div>
<?php endif ?>
<div class="panel panel-default"> <div class="panel panel-default">
<h3><?php echo t('About') ?></h3> <h3><?php echo t('About') ?></h3>
<ul> <ul>
<?php if (! empty($config['last_login'])): ?>
<li><?php echo t('Last login:') ?> <strong><?php echo date('Y-m-d H:i', $config['last_login']) ?></strong></li>
<?php endif ?>
<li><?php echo t('Miniflux version:') ?> <strong><?php echo APP_VERSION ?></strong></li> <li><?php echo t('Miniflux version:') ?> <strong><?php echo APP_VERSION ?></strong></li>
<li><?php echo t('Official website:') ?> <a href="https://miniflux.net" rel="noreferrer" target="_blank">https://miniflux.net</a></li> <li><?php echo t('Official website:') ?> <a href="https://miniflux.net" rel="noreferrer" target="_blank">https://miniflux.net</a></li>
<li><a href="?action=console"><?php echo t('Console') ?></a></li>
</ul> </ul>
</div> </div>
</section> </section>

View File

@ -11,7 +11,6 @@
</div> </div>
<form method="post" action="?action=subscribe" autocomplete="off"> <form method="post" action="?action=subscribe" autocomplete="off">
<?php echo Miniflux\Helper\form_hidden('csrf', $values) ?> <?php echo Miniflux\Helper\form_hidden('csrf', $values) ?>
<?php echo Miniflux\Helper\form_label(t('Website or Feed URL'), 'url') ?> <?php echo Miniflux\Helper\form_label(t('Website or Feed URL'), 'url') ?>
@ -23,13 +22,13 @@
<p class="form-help"><?php echo t('Downloading full content is slower because Miniflux grab the content from the original website. You should use that for subscriptions that display only a summary. This feature doesn\'t work with all websites.') ?></p> <p class="form-help"><?php echo t('Downloading full content is slower because Miniflux grab the content from the original website. You should use that for subscriptions that display only a summary. This feature doesn\'t work with all websites.') ?></p>
<?php echo Miniflux\Helper\form_label(t('Groups'), 'groups'); ?> <?php echo Miniflux\Helper\form_label(t('Groups'), 'group_name'); ?>
<div id="grouplist"> <div id="grouplist">
<?php foreach ($groups as $group): ?> <?php foreach ($groups as $group): ?>
<?php echo Miniflux\Helper\form_checkbox('feed_group_ids[]', $group['title'], $group['id'], in_array($group['id'], $values['feed_group_ids']), 'hide') ?> <?php echo Miniflux\Helper\form_checkbox('feed_group_ids[]', $group['title'], $group['id'], in_array($group['id'], $values['feed_group_ids']), 'hide') ?>
<?php endforeach ?> <?php endforeach ?>
<?php echo Miniflux\Helper\form_text('create_group', $values, array(), array('placeholder="'.t('add a new group').'"')) ?> <?php echo Miniflux\Helper\form_text('group_name', $values, array(), array('placeholder="'.t('add a new group').'"')) ?>
</div> </div>
<div class="form-actions"> <div class="form-actions">

View File

@ -3,9 +3,12 @@
<nav> <nav>
<ul> <ul>
<li><a href="?action=config"><?php echo t('general') ?></a></li> <li><a href="?action=config"><?php echo t('general') ?></a></li>
<li><a href="?action=profile"><?php echo t('profile') ?></a></li>
<?php if (Miniflux\Helper\is_admin()): ?>
<li><a href="?action=users"><?php echo t('users') ?></a></li>
<?php endif ?>
<li><a href="?action=services"><?php echo t('external services') ?></a></li> <li><a href="?action=services"><?php echo t('external services') ?></a></li>
<li class="active"><a href="?action=api"><?php echo t('api') ?></a></li> <li class="active"><a href="?action=api"><?php echo t('api') ?></a></li>
<li><a href="?action=database"><?php echo t('database') ?></a></li>
<li><a href="?action=help"><?php echo t('help') ?></a></li> <li><a href="?action=help"><?php echo t('help') ?></a></li>
<li><a href="?action=about"><?php echo t('about') ?></a></li> <li><a href="?action=about"><?php echo t('about') ?></a></li>
</ul> </ul>
@ -16,16 +19,16 @@
<h3 id="fever"><?php echo t('Fever API') ?></h3> <h3 id="fever"><?php echo t('Fever API') ?></h3>
<ul> <ul>
<li><?php echo t('API endpoint:') ?> <strong><?php echo Miniflux\Helper\get_current_base_url(), 'fever/' ?></strong></li> <li><?php echo t('API endpoint:') ?> <strong><?php echo Miniflux\Helper\get_current_base_url(), 'fever/' ?></strong></li>
<li><?php echo t('API username:') ?> <strong><?php echo Miniflux\Helper\escape($config['username']) ?></strong></li> <li><?php echo t('API username:') ?> <strong><?php echo Miniflux\Helper\escape($user['username']) ?></strong></li>
<li><?php echo t('API token:') ?> <strong><?php echo Miniflux\Helper\escape($config['fever_token']) ?></strong></li> <li><?php echo t('API token:') ?> <strong><?php echo Miniflux\Helper\escape($user['fever_token']) ?></strong></li>
</ul> </ul>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<h3 id="api"><?php echo t('Miniflux API') ?></h3> <h3 id="api"><?php echo t('Miniflux API') ?></h3>
<ul> <ul>
<li><?php echo t('API endpoint:') ?> <strong><?php echo Miniflux\Helper\get_current_base_url(), 'jsonrpc.php' ?></strong></li> <li><?php echo t('API endpoint:') ?> <strong><?php echo Miniflux\Helper\get_current_base_url(), 'jsonrpc.php' ?></strong></li>
<li><?php echo t('API username:') ?> <strong><?php echo Miniflux\Helper\escape($config['username']) ?></strong></li> <li><?php echo t('API username:') ?> <strong><?php echo Miniflux\Helper\escape($user['username']) ?></strong></li>
<li><?php echo t('API token:') ?> <strong><?php echo Miniflux\Helper\escape($config['api_token']) ?></strong></li> <li><?php echo t('API token:') ?> <strong><?php echo Miniflux\Helper\escape($user['api_token']) ?></strong></li>
</ul> </ul>
</div> </div>
</section> </section>

View File

@ -3,9 +3,12 @@
<nav> <nav>
<ul> <ul>
<li class="active"><a href="?action=config"><?php echo t('general') ?></a></li> <li class="active"><a href="?action=config"><?php echo t('general') ?></a></li>
<li><a href="?action=profile"><?php echo t('profile') ?></a></li>
<?php if (Miniflux\Helper\is_admin()): ?>
<li><a href="?action=users"><?php echo t('users') ?></a></li>
<?php endif ?>
<li><a href="?action=services"><?php echo t('external services') ?></a></li> <li><a href="?action=services"><?php echo t('external services') ?></a></li>
<li><a href="?action=api"><?php echo t('api') ?></a></li> <li><a href="?action=api"><?php echo t('api') ?></a></li>
<li><a href="?action=database"><?php echo t('database') ?></a></li>
<li><a href="?action=help"><?php echo t('help') ?></a></li> <li><a href="?action=help"><?php echo t('help') ?></a></li>
<li><a href="?action=about"><?php echo t('about') ?></a></li> <li><a href="?action=about"><?php echo t('about') ?></a></li>
</ul> </ul>
@ -13,19 +16,7 @@
</div> </div>
<section> <section>
<form method="post" action="?action=config" autocomplete="off" id="config-form"> <form method="post" action="?action=config" autocomplete="off" id="config-form">
<h3><?php echo t('Authentication') ?></h3>
<div class="options">
<?php echo Miniflux\Helper\form_hidden('csrf', $values) ?> <?php echo Miniflux\Helper\form_hidden('csrf', $values) ?>
<?php echo Miniflux\Helper\form_label(t('Username'), 'username') ?>
<?php echo Miniflux\Helper\form_text('username', $values, $errors, array('required')) ?><br/>
<?php echo Miniflux\Helper\form_label(t('Password'), 'password') ?>
<?php echo Miniflux\Helper\form_password('password', $values, $errors) ?><br/>
<?php echo Miniflux\Helper\form_label(t('Confirmation'), 'confirmation') ?>
<?php echo Miniflux\Helper\form_password('confirmation', $values, $errors) ?><br/>
</div>
<h3><?php echo t('Application') ?></h3> <h3><?php echo t('Application') ?></h3>
<div class="options"> <div class="options">
@ -40,11 +31,9 @@
<?php if (ENABLE_AUTO_UPDATE): ?> <?php if (ENABLE_AUTO_UPDATE): ?>
<?php echo Miniflux\Helper\form_label(t('Auto-Update URL'), 'auto_update_url') ?> <?php echo Miniflux\Helper\form_label(t('Auto-Update URL'), 'auto_update_url') ?>
<?php echo Miniflux\Helper\form_text('auto_update_url', $values, $errors, array('required')) ?><br/> <?php echo Miniflux\Helper\form_text('auto_update_url', $values, $errors) ?><br/>
<?php endif ?> <?php endif ?>
<?php echo Miniflux\Helper\form_checkbox('debug_mode', t('Enable debug mode'), 1, isset($values['debug_mode']) && $values['debug_mode'] == 1) ?><br/>
<?php echo Miniflux\Helper\form_checkbox('image_proxy', t('Enable image proxy'), 1, isset($values['image_proxy']) && $values['image_proxy'] == 1) ?> <?php echo Miniflux\Helper\form_checkbox('image_proxy', t('Enable image proxy'), 1, isset($values['image_proxy']) && $values['image_proxy'] == 1) ?>
<div class="form-help"><?php echo t('Avoid mixed content warnings with HTTPS') ?></div> <div class="form-help"><?php echo t('Avoid mixed content warnings with HTTPS') ?></div>
</div> </div>

View File

@ -0,0 +1,10 @@
<div class="page-header">
<h2><?php echo t('Confirmation') ?></h2>
</div>
<p class="alert alert-info"><?php echo t('Do you really want to remove this user: "%s"?', Miniflux\Helper\escape($user['username'])) ?></p>
<div class="form-actions">
<a href="?action=remove-user&amp;user_id=<?php echo $user['id'] ?>&amp;csrf=<?php echo $csrf_token ?>" class="btn btn-red"><?php echo t('Remove') ?></a>
<?php echo t('or') ?> <a href="?action=users"><?php echo t('cancel') ?></a>
</div>

View File

@ -1,13 +0,0 @@
<div class="page-header">
<h2><?php echo t('Console') ?></h2>
<ul>
<li><a href="?action=console"><?php echo t('refresh') ?></a></li>
<li><a href="?action=flush-console"><?php echo t('flush messages') ?></a></li>
</ul>
</div>
<?php if (empty($content)): ?>
<p class="alert alert-info"><?php echo t('Nothing to show. Enable the debug mode to see log messages.') ?></p>
<?php else: ?>
<pre id="console"><code><?php echo Miniflux\Helper\escape($content) ?></code></pre>
<?php endif ?>

View File

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

View File

@ -29,13 +29,13 @@
<?php echo Miniflux\Helper\form_checkbox('enabled', t('Activated'), 1, $values['enabled']) ?><br /> <?php echo Miniflux\Helper\form_checkbox('enabled', t('Activated'), 1, $values['enabled']) ?><br />
<?php echo Miniflux\Helper\form_label(t('Groups'), 'groups'); ?> <?php echo Miniflux\Helper\form_label(t('Groups'), 'group_name'); ?>
<div id="grouplist"> <div id="grouplist">
<?php foreach ($groups as $group): ?> <?php foreach ($groups as $group): ?>
<?php echo Miniflux\Helper\form_checkbox('feed_group_ids[]', $group['title'], $group['id'], in_array($group['id'], $values['feed_group_ids']), 'hide') ?> <?php echo Miniflux\Helper\form_checkbox('feed_group_ids[]', $group['title'], $group['id'], in_array($group['id'], $values['feed_group_ids']), 'hide') ?>
<?php endforeach ?> <?php endforeach ?>
<?php echo Miniflux\Helper\form_text('create_group', $values, array(), array('placeholder="'.t('add a new group').'"')) ?> <?php echo Miniflux\Helper\form_text('group_name', $values, array(), array('placeholder="'.t('add a new group').'"')) ?>
</div> </div>
<div class="form-actions"> <div class="form-actions">

View File

@ -0,0 +1,39 @@
<div class="page-header">
<h2><?php echo $title ?></h2>
<nav>
<ul>
<li><a href="?action=config"><?php echo t('general') ?></a></li>
<li><a href="?action=profile"><?php echo t('profile') ?></a></li>
<?php if (Miniflux\Helper\is_admin()): ?>
<li class="active"><a href="?action=users"><?php echo t('users') ?></a></li>
<?php endif ?>
<li><a href="?action=services"><?php echo t('external services') ?></a></li>
<li><a href="?action=api"><?php echo t('api') ?></a></li>
<li><a href="?action=help"><?php echo t('help') ?></a></li>
<li><a href="?action=about"><?php echo t('about') ?></a></li>
</ul>
</nav>
</div>
<section>
<form method="post" action="?action=edit-user" autocomplete="off" id="config-form">
<div class="options">
<?php echo Miniflux\Helper\form_hidden('csrf', $values) ?>
<?php echo Miniflux\Helper\form_hidden('id', $values) ?>
<?php echo Miniflux\Helper\form_label(t('Username'), 'username') ?>
<?php echo Miniflux\Helper\form_text('username', $values, $errors, array('required')) ?>
<?php echo Miniflux\Helper\form_label(t('Password'), 'password') ?>
<?php echo Miniflux\Helper\form_password('password', $values, $errors) ?>
<?php echo Miniflux\Helper\form_label(t('Confirmation'), 'confirmation') ?>
<?php echo Miniflux\Helper\form_password('confirmation', $values, $errors) ?>
<?php echo Miniflux\Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1) ?>
</div>
<div class="form-actions">
<input type="submit" value="<?php echo t('Save') ?>" class="btn btn-blue">
</div>
</form>
</section>

View File

@ -24,7 +24,7 @@
<?php if ($feed['parsing_error']): ?> <?php if ($feed['parsing_error']): ?>
<p class="alert alert-error"> <p class="alert alert-error">
<?php echo tne('An error occurred during the last check. Refresh the feed manually and check the %sconsole%s for errors afterwards!','<a href="?action=console">','</a>') ?> <?php echo t('An error occurred during the last check. You could enable the debug mode to have more information.') ?>
</p> </p>
<?php endif; ?> <?php endif; ?>

View File

@ -18,7 +18,7 @@
<?php else: ?> <?php else: ?>
<?php if ($nb_failed_feeds > 0): ?> <?php if ($nb_failed_feeds > 0): ?>
<p class="alert alert-error"><?php echo tne('An error occurred during the last check. Refresh the feed manually and check the %sconsole%s for errors afterwards!', '<a href="?action=console">', '</a>') ?></p> <p class="alert alert-error"><?php echo t('An error occurred during the last check. You could enable the debug mode to have more information.') ?></p>
<?php elseif ($nothing_to_read): ?> <?php elseif ($nothing_to_read): ?>
<p class="alert alert-info"><?php echo tne('Nothing to read, do you want to %supdate your subscriptions%s?','<a href="?action=refresh-all" data-action="refresh-all">','</a>') ?></p> <p class="alert alert-info"><?php echo tne('Nothing to read, do you want to %supdate your subscriptions%s?','<a href="?action=refresh-all" data-action="refresh-all">','</a>') ?></p>
<?php endif ?> <?php endif ?>

View File

@ -3,9 +3,12 @@
<nav> <nav>
<ul> <ul>
<li><a href="?action=config"><?php echo t('general') ?></a></li> <li><a href="?action=config"><?php echo t('general') ?></a></li>
<li><a href="?action=profile"><?php echo t('profile') ?></a></li>
<?php if (Miniflux\Helper\is_admin()): ?>
<li><a href="?action=users"><?php echo t('users') ?></a></li>
<?php endif ?>
<li><a href="?action=services"><?php echo t('external services') ?></a></li> <li><a href="?action=services"><?php echo t('external services') ?></a></li>
<li><a href="?action=api"><?php echo t('api') ?></a></li> <li><a href="?action=api"><?php echo t('api') ?></a></li>
<li><a href="?action=database"><?php echo t('database') ?></a></li>
<li class="active"><a href="?action=help"><?php echo t('help') ?></a></li> <li class="active"><a href="?action=help"><?php echo t('help') ?></a></li>
<li><a href="?action=about"><?php echo t('about') ?></a></li> <li><a href="?action=about"><?php echo t('about') ?></a></li>
</ul> </ul>

View File

@ -6,7 +6,7 @@
data-item-bookmark="<?php echo $item['bookmark'] ?>" data-item-bookmark="<?php echo $item['bookmark'] ?>"
<?php echo $hide ? 'data-hide="true"' : '' ?> <?php echo $hide ? 'data-hide="true"' : '' ?>
> >
<h2 <?php echo Miniflux\Helper\is_rtl($item) ? 'dir="rtl"' : 'dir="ltr"' ?>> <h2 <?php echo Miniflux\Helper\rtl($item) ?>>
<span class="item-icons"> <span class="item-icons">
<a <a
class="bookmark-icon" class="bookmark-icon"
@ -70,16 +70,16 @@
<a href="<?php echo $item['url'] ?>" class="original" rel="noreferrer" target="_blank" <?php echo ($original_marks_read) ? 'data-action="mark-read"' : '' ?>><?php echo t('original link') ?></a> <a href="<?php echo $item['url'] ?>" class="original" rel="noreferrer" target="_blank" <?php echo ($original_marks_read) ? 'data-action="mark-read"' : '' ?>><?php echo t('original link') ?></a>
</li> </li>
<?php endif ?> <?php endif ?>
<?php if ($item['enclosure']): ?> <?php if ($item['enclosure_url']): ?>
<li> <li>
<?php if (strpos($item['enclosure_type'], 'video/') === 0): ?> <?php if (strpos($item['enclosure_type'], 'video/') === 0): ?>
<a href="<?php echo $item['enclosure'] ?>" class="video-enclosure" rel="noreferrer" target="_blank"><?php echo t('attachment') ?></a> <a href="<?php echo $item['enclosure_url'] ?>" class="video-enclosure" rel="noreferrer" target="_blank"><?php echo t('attachment') ?></a>
<?php elseif(strpos($item['enclosure_type'], 'audio/') === 0): ?> <?php elseif(strpos($item['enclosure_type'], 'audio/') === 0): ?>
<a href="<?php echo $item['enclosure'] ?>" class="audio-enclosure" rel="noreferrer" target="_blank"><?php echo t('attachment') ?></a> <a href="<?php echo $item['enclosure_url'] ?>" class="audio-enclosure" rel="noreferrer" target="_blank"><?php echo t('attachment') ?></a>
<?php elseif(strpos($item['enclosure_type'], 'image/') === 0): ?> <?php elseif(strpos($item['enclosure_type'], 'image/') === 0): ?>
<a href="<?php echo $item['enclosure'] ?>" class="image-enclosure" rel="noreferrer" target="_blank"><?php echo t('attachment') ?></a> <a href="<?php echo $item['enclosure_url'] ?>" class="image-enclosure" rel="noreferrer" target="_blank"><?php echo t('attachment') ?></a>
<?php else: ?> <?php else: ?>
<a href="<?php echo $item['enclosure'] ?>" class="enclosure" rel="noreferrer" target="_blank"><?php echo t('attachment') ?></a> <a href="<?php echo $item['enclosure_url'] ?>" class="enclosure" rel="noreferrer" target="_blank"><?php echo t('attachment') ?></a>
<?php endif ?> <?php endif ?>
</li> </li>
<?php endif ?> <?php endif ?>
@ -87,9 +87,9 @@
<?php echo Miniflux\Template\load('status_links', array('item' => $item, 'menu' => $menu, 'offset' => $offset)) ?> <?php echo Miniflux\Template\load('status_links', array('item' => $item, 'menu' => $menu, 'offset' => $offset)) ?>
</ul> </ul>
<?php if ($display_mode === 'full'): ?> <?php if ($display_mode === 'full'): ?>
<div class="preview-full-content" <?php echo Miniflux\Helper\is_rtl($item) ? 'dir="rtl"' : 'dir="ltr"' ?>><?php echo $item['content'] ?></div> <div class="preview-full-content" <?php echo Miniflux\Helper\rtl($item) ?>><?php echo $item['content'] ?></div>
<?php elseif ($display_mode === 'summaries'): ?> <?php elseif ($display_mode === 'summaries'): ?>
<p class="preview" <?php echo Miniflux\Helper\is_rtl($item) ? 'dir="rtl"' : 'dir="ltr"' ?>><?php echo Miniflux\Helper\escape(Miniflux\Helper\summary(strip_tags($item['content']), 50, 300)) ?></p> <p class="preview" <?php echo Miniflux\Helper\rtl($item) ?>><?php echo Miniflux\Helper\escape(Miniflux\Helper\summary(strip_tags($item['content']), 50, 300)) ?></p>
<?php else: ?> <?php else: ?>
<p class="no-preview"></p> <p class="no-preview"></p>
<?php endif ?> <?php endif ?>

View File

@ -39,16 +39,6 @@
<?php echo Miniflux\Helper\form_checkbox('remember_me', t('Remember Me'), 1) ?><br/> <?php echo Miniflux\Helper\form_checkbox('remember_me', t('Remember Me'), 1) ?><br/>
<?php if (ENABLE_MULTIPLE_DB && count($databases) > 1): ?>
<div id="database-selector">
<h4><?php echo t('Select another database') ?></h4>
<?php foreach ($databases as $filename => $dbname): ?>
<?php echo Miniflux\Helper\form_radio('database', $dbname, $filename, ($current_database === $filename)) ?>
<?php endforeach ?>
</div>
<?php endif ?>
<div class="form-actions"> <div class="form-actions">
<input type="submit" value="<?php echo t('Sign in') ?>" class="btn btn-blue"/> <input type="submit" value="<?php echo t('Sign in') ?>" class="btn btn-blue"/>
</div> </div>

View File

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

View File

@ -0,0 +1,37 @@
<div class="page-header">
<h2><?php echo $title ?></h2>
<nav>
<ul>
<li><a href="?action=config"><?php echo t('general') ?></a></li>
<li><a href="?action=profile"><?php echo t('profile') ?></a></li>
<?php if (Miniflux\Helper\is_admin()): ?>
<li class="active"><a href="?action=users"><?php echo t('users') ?></a></li>
<?php endif ?>
<li><a href="?action=services"><?php echo t('external services') ?></a></li>
<li><a href="?action=api"><?php echo t('api') ?></a></li>
<li><a href="?action=help"><?php echo t('help') ?></a></li>
<li><a href="?action=about"><?php echo t('about') ?></a></li>
</ul>
</nav>
</div>
<section>
<form method="post" action="?action=new-user" autocomplete="off" id="config-form">
<div class="options">
<?php echo Miniflux\Helper\form_hidden('csrf', $values) ?>
<?php echo Miniflux\Helper\form_label(t('Username'), 'username') ?>
<?php echo Miniflux\Helper\form_text('username', $values, $errors, array('required', 'autofocus')) ?>
<?php echo Miniflux\Helper\form_label(t('Password'), 'password') ?>
<?php echo Miniflux\Helper\form_password('password', $values, $errors) ?>
<?php echo Miniflux\Helper\form_label(t('Confirmation'), 'confirmation') ?>
<?php echo Miniflux\Helper\form_password('confirmation', $values, $errors) ?>
<?php echo Miniflux\Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1) ?>
</div>
<div class="form-actions">
<input type="submit" value="<?php echo t('Save') ?>" class="btn btn-blue">
</div>
</form>
</section>

39
app/templates/profile.php Normal file
View File

@ -0,0 +1,39 @@
<div class="page-header">
<h2><?php echo $title ?></h2>
<nav>
<ul>
<li><a href="?action=config"><?php echo t('general') ?></a></li>
<li class="active"><a href="?action=profile"><?php echo t('profile') ?></a></li>
<?php if (Miniflux\Helper\is_admin()): ?>
<li><a href="?action=users"><?php echo t('users') ?></a></li>
<?php endif ?>
<li><a href="?action=services"><?php echo t('external services') ?></a></li>
<li><a href="?action=api"><?php echo t('api') ?></a></li>
<li><a href="?action=help"><?php echo t('help') ?></a></li>
<li><a href="?action=about"><?php echo t('about') ?></a></li>
</ul>
</nav>
</div>
<section>
<form method="post" action="?action=profile" autocomplete="off" id="config-form">
<h3><?php echo t('Authentication') ?></h3>
<div class="options">
<?php echo Miniflux\Helper\form_hidden('csrf', $values) ?>
<?php echo Miniflux\Helper\form_hidden('id', $values) ?>
<?php echo Miniflux\Helper\form_label(t('Username'), 'username') ?>
<?php echo Miniflux\Helper\form_text('username', $values, $errors, array('required')) ?><br/>
<?php echo Miniflux\Helper\form_label(t('Password'), 'password') ?>
<?php echo Miniflux\Helper\form_password('password', $values, $errors) ?><br/>
<?php echo Miniflux\Helper\form_label(t('Confirmation'), 'confirmation') ?>
<?php echo Miniflux\Helper\form_password('confirmation', $values, $errors) ?><br/>
</div>
<div class="form-actions">
<input type="submit" value="<?php echo t('Save') ?>" class="btn btn-blue"/>
</div>
</form>
</section>

View File

@ -3,9 +3,12 @@
<nav> <nav>
<ul> <ul>
<li><a href="?action=config"><?php echo t('general') ?></a></li> <li><a href="?action=config"><?php echo t('general') ?></a></li>
<li><a href="?action=profile"><?php echo t('profile') ?></a></li>
<?php if (Miniflux\Helper\is_admin()): ?>
<li><a href="?action=users"><?php echo t('users') ?></a></li>
<?php endif ?>
<li class="active"><a href="?action=services"><?php echo t('external services') ?></a></li> <li class="active"><a href="?action=services"><?php echo t('external services') ?></a></li>
<li><a href="?action=api"><?php echo t('api') ?></a></li> <li><a href="?action=api"><?php echo t('api') ?></a></li>
<li><a href="?action=database"><?php echo t('database') ?></a></li>
<li><a href="?action=help"><?php echo t('help') ?></a></li> <li><a href="?action=help"><?php echo t('help') ?></a></li>
<li><a href="?action=about"><?php echo t('about') ?></a></li> <li><a href="?action=about"><?php echo t('about') ?></a></li>
</ul> </ul>

View File

@ -29,7 +29,7 @@
</nav> </nav>
<?php endif ?> <?php endif ?>
<h1 <?php echo Miniflux\Helper\is_rtl($item + array('rtl' => $feed['rtl'])) ? 'dir="rtl"' : 'dir="ltr"' ?>> <h1 <?php echo Miniflux\Helper\rtl($item) ?>>
<a href="<?php echo $item['url'] ?>" rel="noreferrer" target="_blank" class="original"><?php echo Miniflux\Helper\escape($item['title']) ?></a> <a href="<?php echo $item['url'] ?>" rel="noreferrer" target="_blank" class="original"><?php echo Miniflux\Helper\escape($item['title']) ?></a>
</h1> </h1>
@ -54,9 +54,9 @@
<li class="hide-mobile"> <li class="hide-mobile">
<span title="<?php echo dt('%e %B %Y %k:%M', $item['updated']) ?>"><?php echo Miniflux\Helper\relative_time($item['updated']) ?></span> <span title="<?php echo dt('%e %B %Y %k:%M', $item['updated']) ?>"><?php echo Miniflux\Helper\relative_time($item['updated']) ?></span>
</li> </li>
<?php if ($item['enclosure']): ?> <?php if ($item['enclosure_url']): ?>
<li> <li>
<a href="<?php echo $item['enclosure'] ?>" rel="noreferrer" target="_blank"><?php echo t('attachment') ?></a> <a href="<?php echo $item['enclosure_url'] ?>" rel="noreferrer" target="_blank"><?php echo t('attachment') ?></a>
</li> </li>
<?php endif ?> <?php endif ?>
<li class="hide-mobile"> <li class="hide-mobile">
@ -74,24 +74,24 @@
<?php endif; ?> <?php endif; ?>
</ul> </ul>
<div id="item-content" <?php echo Miniflux\Helper\is_rtl($item + array('rtl' => $feed['rtl'])) ? 'dir="rtl"' : 'dir="ltr"' ?>> <div id="item-content" <?php echo Miniflux\Helper\rtl($item) ?>>
<?php if ($item['enclosure']): ?> <?php if ($item['enclosure_url']): ?>
<?php if (strpos($item['enclosure_type'], 'audio') !== false): ?> <?php if (strpos($item['enclosure_type'], 'audio') !== false): ?>
<div id="item-content-enclosure"> <div id="item-content-enclosure">
<audio controls> <audio controls>
<source src="<?php echo $item['enclosure'] ?>" type="<?php echo $item['enclosure_type'] ?>"> <source src="<?php echo $item['enclosure_url'] ?>" type="<?php echo $item['enclosure_type'] ?>">
</audio> </audio>
</div> </div>
<?php elseif (strpos($item['enclosure_type'], 'video') !== false): ?> <?php elseif (strpos($item['enclosure_type'], 'video') !== false): ?>
<div id="item-content-enclosure"> <div id="item-content-enclosure">
<video controls> <video controls>
<source src="<?php echo $item['enclosure'] ?>" type="<?php echo $item['enclosure_type'] ?>"> <source src="<?php echo $item['enclosure_url'] ?>" type="<?php echo $item['enclosure_type'] ?>">
</video> </video>
</div> </div>
<?php elseif (strpos($item['enclosure_type'], 'image') !== false && $item['content'] === ''): ?> <?php elseif (strpos($item['enclosure_type'], 'image') !== false && $item['content'] === ''): ?>
<div id="item-content-enclosure"> <div id="item-content-enclosure">
<img src="<?php echo $item['enclosure'] ?>" alt="enclosure"/> <img src="<?php echo $item['enclosure_url'] ?>" alt="enclosure"/>
</div> </div>
<?php endif ?> <?php endif ?>
<?php endif ?> <?php endif ?>

View File

@ -1,6 +1,6 @@
<?php echo Miniflux\Template\load('search_form') ?>
<?php echo Miniflux\Template\load('search_form') ?> <div class="page-header">
<div class="page-header">
<h2><?php echo t('Unread') ?><span id="page-counter"><?php echo isset($nb_items) ? $nb_items : '' ?></span></h2> <h2><?php echo t('Unread') ?><span id="page-counter"><?php echo isset($nb_items) ? $nb_items : '' ?></span></h2>
<?php if (!empty($groups)): ?> <?php if (!empty($groups)): ?>
<nav> <nav>
@ -22,9 +22,9 @@
<a href="?action=mark-all-read<?php echo $group_id === null ? '' : '&amp;group_id='.$group_id ?>"><?php echo t('mark all as read') ?></a> <a href="?action=mark-all-read<?php echo $group_id === null ? '' : '&amp;group_id='.$group_id ?>"><?php echo t('mark all as read') ?></a>
</li> </li>
</ul> </ul>
</div> </div>
<section class="items" id="listing"> <section class="items" id="listing">
<?php if (empty($items)): ?> <?php if (empty($items)): ?>
<p class="alert alert-info"><?php echo t('Nothing to read') ?></p> <p class="alert alert-info"><?php echo t('Nothing to read') ?></p>
<?php else: ?> <?php else: ?>
@ -48,4 +48,4 @@
<?php echo Miniflux\Template\load('paging', array('menu' => $menu, 'nb_items' => $nb_items, 'items_per_page' => $items_per_page, 'offset' => $offset, 'order' => $order, 'direction' => $direction, 'group_id' => $group_id)) ?> <?php echo Miniflux\Template\load('paging', array('menu' => $menu, 'nb_items' => $nb_items, 'items_per_page' => $items_per_page, 'offset' => $offset, 'order' => $order, 'direction' => $direction, 'group_id' => $group_id)) ?>
<?php endif ?> <?php endif ?>
</section> </section>

40
app/templates/users.php Normal file
View File

@ -0,0 +1,40 @@
<div class="page-header">
<h2><?php echo $title ?></h2>
<nav>
<ul>
<li><a href="?action=config"><?php echo t('general') ?></a></li>
<li><a href="?action=profile"><?php echo t('profile') ?></a></li>
<?php if (Miniflux\Helper\is_admin()): ?>
<li class="active"><a href="?action=users"><?php echo t('users') ?></a></li>
<?php endif ?>
<li><a href="?action=services"><?php echo t('external services') ?></a></li>
<li><a href="?action=api"><?php echo t('api') ?></a></li>
<li><a href="?action=help"><?php echo t('help') ?></a></li>
<li><a href="?action=about"><?php echo t('about') ?></a></li>
</ul>
</nav>
</div>
<section>
<p><a href="?action=new-user"><?php echo t('New User') ?></a><br><br></p>
<table>
<tr>
<th><?php echo t('Username') ?></th>
<th><?php echo t('Administrator') ?></th>
<th><?php echo t('Action') ?></th>
</tr>
<?php foreach ($users as $user): ?>
<tr>
<td>
<?php echo Miniflux\Helper\escape($user['username']) ?>
</td>
<td><?php echo $user['is_admin'] ? t('Yes') : t('No') ?></td>
<td>
<?php if (Miniflux\Helper\get_user_id() != $user['id']): ?>
<a href="?action=edit-user&amp;user_id=<?php echo $user['id'] ?>"><?php echo t('Edit') ?></a> -
<a href="?action=confirm-remove-user&amp;user_id=<?php echo $user['id'] ?>"><?php echo t('Remove') ?></a>
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
</table>
</section>

View File

@ -8,31 +8,17 @@ use SimpleValidator\Validators;
function validate_modification(array $values) function validate_modification(array $values)
{ {
$rules = array( $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')), new Validators\Required('autoflush', t('Value required')),
new Validators\Required('autoflush_unread', t('Value required')), new Validators\Required('autoflush_unread', t('Value required')),
new Validators\Required('items_per_page', t('Value required')), new Validators\Required('items_per_page', t('Value required')),
new Validators\Integer('items_per_page', t('Must be an integer')), new Validators\Integer('items_per_page', t('Must be an integer')),
new Validators\Required('theme', t('Value required')), new Validators\Required('theme', t('Value required')),
new Validators\Integer('frontend_updatecheck_interval', t('Must be an integer')), new Validators\Integer('frontend_updatecheck_interval', t('Must be an integer')),
new Validators\Integer('debug_mode', t('Must be an integer')),
new Validators\Integer('nocontent', t('Must be an integer')), new Validators\Integer('nocontent', t('Must be an integer')),
new Validators\Integer('favicons', 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')), new Validators\Integer('original_marks_read', t('Must be an integer')),
); );
if (ENABLE_AUTO_UPDATE) {
$rules[] = new Validators\Required('auto_update_url', t('Value required'));
}
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'));
$rules[] = new Validators\Equals('password', 'confirmation', t('Passwords don\'t match'));
}
$v = new Validator($values, $rules); $v = new Validator($values, $rules);
return array( return array(

View File

@ -2,25 +2,41 @@
namespace Miniflux\Validator\User; namespace Miniflux\Validator\User;
use SimpleValidator\Validator; use Miniflux\Session\SessionStorage;
use SimpleValidator\Validators;
use Miniflux\Model\Config;
use Miniflux\Model\User as UserModel; use Miniflux\Model\User as UserModel;
use Miniflux\Model\Database as DatabaseModel;
use Miniflux\Model\RememberMe; use Miniflux\Model\RememberMe;
use Miniflux\Request; use Miniflux\Request;
use PicoDb\Database;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
function validate_modification(array $values)
{
$v = new Validator($values, array(
new Validators\Required('id', t('The user id required')),
new Validators\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\MinLength('password', t('The minimum length is 6 characters'), 6),
new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
new Validators\Unique('username', t('The username must be unique'), Database::getInstance('db')->getConnection(), 'users', 'id'),
));
return array(
$v->execute(),
$v->getErrors()
);
}
function validate_creation(array $values) function validate_creation(array $values)
{ {
$v = new Validator($values, array( $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\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50), new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\Required('password', t('The password is required')), new Validators\Required('password', t('The password is required')),
new Validators\MinLength('password', t('The minimum length is 6 characters'), 6), new Validators\MinLength('password', t('The minimum length is 6 characters'), 6),
new Validators\Required('confirmation', t('The confirmation is required')), new Validators\Required('confirmation', t('The confirmation is required')),
new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')), new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
new Validators\Unique('username', t('The username must be unique'), Database::getInstance('db')->getConnection(), 'users', 'id'),
)); ));
return array( return array(
@ -41,16 +57,15 @@ function validate_login(array $values)
$errors = $v->getErrors(); $errors = $v->getErrors();
if ($result) { if ($result) {
$credentials = UserModel\get_credentials(); $user = UserModel\get_user_by_username($values['username']);
if ($credentials && $credentials['username'] === $values['username'] && password_verify($values['password'], $credentials['password'])) { if (! empty($user) && password_verify($values['password'], $user['password'])) {
UserModel\set_last_login(); SessionStorage::getInstance()->setUser($user);
$_SESSION['loggedin'] = true; UserModel\set_last_login_date($user['id']);
$_SESSION['config'] = Config\get_all();
// Setup the remember me feature // Setup the remember me feature
if (! empty($values['remember_me'])) { if (! empty($values['remember_me'])) {
$cookie = RememberMe\create(DatabaseModel\select(), $values['username'], Request\get_ip_address(), Request\get_user_agent()); $cookie = RememberMe\create($user['id'], Request\get_ip_address(), Request\get_user_agent());
RememberMe\write_cookie($cookie['token'], $cookie['sequence'], $cookie['expiration']); RememberMe\write_cookie($cookie['token'], $cookie['sequence'], $cookie['expiration']);
} }
} else { } else {

View File

@ -13,7 +13,7 @@
}, },
"require": { "require": {
"fguillot/simple-validator": "v1.0.0", "fguillot/simple-validator": "v1.0.0",
"fguillot/json-rpc": "v1.2.1", "fguillot/json-rpc": "v1.2.3",
"fguillot/picodb": "v1.0.14 ", "fguillot/picodb": "v1.0.14 ",
"fguillot/picofeed": "v0.1.25", "fguillot/picofeed": "v0.1.25",
"pda/pheanstalk": "v3.1.0", "pda/pheanstalk": "v3.1.0",
@ -28,6 +28,7 @@
"files": [ "files": [
"app/schemas/sqlite.php", "app/schemas/sqlite.php",
"app/helpers/app.php", "app/helpers/app.php",
"app/helpers/config.php",
"app/helpers/csrf.php", "app/helpers/csrf.php",
"app/helpers/favicon.php", "app/helpers/favicon.php",
"app/helpers/form.php", "app/helpers/form.php",
@ -40,6 +41,8 @@
"app/core/template.php", "app/core/template.php",
"app/handlers/scraper.php", "app/handlers/scraper.php",
"app/handlers/service.php", "app/handlers/service.php",
"app/handlers/feed.php",
"app/handlers/item.php",
"app/handlers/opml.php", "app/handlers/opml.php",
"app/handlers/proxy.php", "app/handlers/proxy.php",
"app/models/config.php", "app/models/config.php",
@ -51,7 +54,6 @@
"app/models/item_group.php", "app/models/item_group.php",
"app/models/bookmark.php", "app/models/bookmark.php",
"app/models/auto_update.php", "app/models/auto_update.php",
"app/models/database.php",
"app/models/remember_me.php", "app/models/remember_me.php",
"app/models/group.php", "app/models/group.php",
"app/models/favicon.php", "app/models/favicon.php",

View File

@ -18,8 +18,8 @@ define('FAVICON_URL_PATH', 'data/favicons');
// DB_FILENAME => default value is db.sqlite (default database filename) // DB_FILENAME => default value is db.sqlite (default database filename)
define('DB_FILENAME', 'db.sqlite'); define('DB_FILENAME', 'db.sqlite');
// ENABLE_MULTIPLE_DB => default value is true (multiple users support) // Enable/disable debug mode
define('ENABLE_MULTIPLE_DB', true); define('DEBUG_MODE', false);
// DEBUG_FILENAME => default is data/debug.log // DEBUG_FILENAME => default is data/debug.log
define('DEBUG_FILENAME', DATA_DIRECTORY.'/debug.log'); define('DEBUG_FILENAME', DATA_DIRECTORY.'/debug.log');
@ -48,3 +48,6 @@ define('ENABLE_AUTO_UPDATE', true);
// SUBSCRIPTION_CONCURRENT_REQUESTS => number of concurrent feeds to refresh at once // SUBSCRIPTION_CONCURRENT_REQUESTS => number of concurrent feeds to refresh at once
// Reduce this number on systems with limited processing power // Reduce this number on systems with limited processing power
define('SUBSCRIPTION_CONCURRENT_REQUESTS', 5); define('SUBSCRIPTION_CONCURRENT_REQUESTS', 5);
// Allow the cronjob to be accessible from the browser
define('ENABLE_CRONJOB_HTTP_ACCESS', true);

View File

@ -2,6 +2,7 @@
require __DIR__.'/app/common.php'; require __DIR__.'/app/common.php';
use Miniflux\Handler;
use Miniflux\Model; use Miniflux\Model;
if (php_sapi_name() === 'cli') { if (php_sapi_name() === 'cli') {
@ -9,29 +10,30 @@ if (php_sapi_name() === 'cli') {
'limit::', 'limit::',
'call-interval::', 'call-interval::',
'update-interval::', 'update-interval::',
'database::',
)); ));
} } else {
else { $token = isset($_GET['token']) ? $_GET['token'] : '';
$user = Model\User\get_user_by_token('cronjob_token', $token);
if (empty($user) || !ENABLE_CRONJOB_HTTP_ACCESS) {
die('Access Denied');
}
$options = $_GET; $options = $_GET;
} }
if (! empty($options['database'])) { $limit = get_cli_option('limit', $options);
if (! Model\Database\select($options['database'])) { $update_interval = get_cli_option('update-interval', $options);
die("Database ".$options['database']." not found\r\n"); $call_interval = get_cli_option('call-interval', $options);
}
}
$limit = ! empty($options['limit']) && ctype_digit($options['limit']) ? (int) $options['limit'] : Model\Feed\LIMIT_ALL; foreach (Model\User\get_all_users() as $user) {
$update_interval = ! empty($options['update-interval']) && ctype_digit($options['update-interval']) ? (int) $options['update-interval'] : null; if ($update_interval !== null && $call_interval !== null && $limit === null && $update_interval >= $call_interval) {
$call_interval = ! empty($options['call-interval']) && ctype_digit($options['call-interval']) ? (int) $options['call-interval'] : null; $feeds_count = Model\Feed\count_feeds($user['id']);
if ($update_interval !== null && $call_interval !== null && $limit === Model\Feed\LIMIT_ALL && $update_interval >= $call_interval) {
$feeds_count = PicoDb\Database::getInstance('db')->table('feeds')->count();
$limit = ceil($feeds_count / ($update_interval / $call_interval)); $limit = ceil($feeds_count / ($update_interval / $call_interval));
} }
Model\Feed\refresh_all($limit); Handler\Feed\update_feeds($user['id'], $limit);
Model\Item\autoflush_read(); Model\Item\autoflush_read($user['id']);
Model\Item\autoflush_unread(); Model\Item\autoflush_unread($user['id']);
Model\Config\write_debug(); Miniflux\Helper\write_debug_file();
}

View File

@ -1,15 +0,0 @@
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteBase /
# only if the requested file does not exists
RewriteCond %{REQUEST_FILENAME} !-f
# Store the current location in an environment variable CWD
RewriteCond $0#%{REQUEST_URI} ([^#]*)#(.*)\1$
RewriteRule ^.*$ - [E=CWD:%2]
# Just by prefixing the environment variable, we can safely rewrite anything now
RewriteRule ^([^/]*) %{ENV:CWD}index.php?database=$1 [QSA,L]
</IfModule>

View File

@ -4,9 +4,10 @@ require __DIR__.'/../app/common.php';
use Miniflux\Handler; use Miniflux\Handler;
use Miniflux\Model; use Miniflux\Model;
use Miniflux\Model\Feed;
use Miniflux\Model\Group; register_shutdown_function(function () {
use PicoDb\Database; Miniflux\Helper\write_debug_file();
});
// Route handler // Route handler
function route($name, Closure $callback = null) function route($name, Closure $callback = null)
@ -31,37 +32,27 @@ function response(array $response)
// Fever authentication // Fever authentication
function auth() function auth()
{ {
if (! empty($_GET['database'])) { $api_key = isset($_POST['api_key']) && ctype_alnum($_POST['api_key']) ? $_POST['api_key'] : null;
// Return unauthorized if the requested database could not be found $user = Model\User\get_user_by_token('fever_api_key', $api_key);
if (! Model\Database\select($_GET['database'])) { $authenticated = $user !== null;
return array(
'api_version' => 3,
'auth' => 0,
);
}
}
$credentials = Database::getInstance('db')->hashtable('settings')->get('username', 'fever_token');
$api_key = md5($credentials['username'].':'.$credentials['fever_token']);
$response = array( $response = array(
'api_version' => 3, 'api_version' => 3,
'auth' => (int) (isset($_POST['api_key']) && (strcasecmp($_POST['api_key'], $api_key) === 0)), 'auth' => (int) $authenticated,
'last_refreshed_on_time' => time(), 'last_refreshed_on_time' => time(),
); );
return $response; return array($user, $authenticated, $response);
} }
// Call: ?api&groups // Call: ?api&groups
route('groups', function () { route('groups', function () {
list($user, $authenticated, $response) = auth();
$response = auth(); if ($authenticated) {
$response['groups'] = Model\Group\get_all($user['id']);
if ($response['auth']) {
$response['groups'] = Group\get_all();
$response['feeds_groups'] = array(); $response['feeds_groups'] = array();
$group_map = Group\get_map(); $group_map = Model\Group\get_groups_feed_ids($user['id']);
foreach ($group_map as $group_id => $feed_ids) { foreach ($group_map as $group_id => $feed_ids) {
$response['feeds_groups'][] = array( $response['feeds_groups'][] = array(
@ -76,14 +67,13 @@ route('groups', function () {
// Call: ?api&feeds // Call: ?api&feeds
route('feeds', function () { route('feeds', function () {
list($user, $authenticated, $response) = auth();
$response = auth(); if ($authenticated) {
if ($response['auth']) {
$response['feeds'] = array(); $response['feeds'] = array();
$response['feeds_groups'] = array(); $response['feeds_groups'] = array();
$feeds = Feed\get_all(); $feeds = Model\Feed\get_feeds($user['id']);
foreach ($feeds as $feed) { foreach ($feeds as $feed) {
$response['feeds'][] = array( $response['feeds'][] = array(
@ -97,7 +87,7 @@ route('feeds', function () {
); );
} }
$group_map = Group\get_map(); $group_map = Model\Group\get_groups_feed_ids($user['id']);
foreach ($group_map as $group_id => $feed_ids) { foreach ($group_map as $group_id => $feed_ids) {
$response['feeds_groups'][] = array( $response['feeds_groups'][] = array(
'group_id' => $group_id, 'group_id' => $group_id,
@ -111,24 +101,16 @@ route('feeds', function () {
// Call: ?api&favicons // Call: ?api&favicons
route('favicons', function () { route('favicons', function () {
list($user, $authenticated, $response) = auth();
$response = auth(); if ($authenticated) {
$favicons = Model\Favicon\get_favicons_with_data_url($user['id']);
if ($response['auth']) {
$favicons = Database::getInstance('db')
->table('favicons')
->columns(
'feed_id',
'file',
'type'
)
->findAll();
$response['favicons'] = array(); $response['favicons'] = array();
foreach ($favicons as $favicon) { foreach ($favicons as $favicon) {
$response['favicons'][] = array( $response['favicons'][] = array(
'id' => (int) $favicon['feed_id'], 'id' => (int) $favicon['feed_id'],
'data' => 'data:'.$favicon['type'].';base64,'.base64_encode(file_get_contents(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$favicon['file'])) 'data' => $favicon['url'],
); );
} }
} }
@ -138,39 +120,17 @@ route('favicons', function () {
// Call: ?api&items // Call: ?api&items
route('items', function () { route('items', function () {
list($user, $authenticated, $response) = auth();
$response = auth(); if ($authenticated) {
$since_id = isset($_GET['since_id']) && ctype_digit($_GET['since_id']) ? $_GET['since_id'] : null;
if ($response['auth']) { $item_ids = ! empty($_GET['with_ids']) ? explode(',', $_GET['with_ids']) : array();
$query = Database::getInstance('db') $items = Model\Item\get_items($user['id'], $since_id, $item_ids);
->table('items')
->columns(
'rowid',
'feed_id',
'title',
'author',
'content',
'url',
'updated',
'status',
'bookmark'
)
->limit(50)
->neq('status', 'removed');
if (isset($_GET['since_id']) && is_numeric($_GET['since_id'])) {
$items = $query->gt('rowid', $_GET['since_id'])
->asc('rowid');
} elseif (! empty($_GET['with_ids'])) {
$query->in('rowid', explode(',', $_GET['with_ids']));
}
$items = $query->findAll();
$response['items'] = array(); $response['items'] = array();
foreach ($items as $item) { foreach ($items as $item) {
$response['items'][] = array( $response['items'][] = array(
'id' => (int) $item['rowid'], 'id' => (int) $item['id'],
'feed_id' => (int) $item['feed_id'], 'feed_id' => (int) $item['feed_id'],
'title' => $item['title'], 'title' => $item['title'],
'author' => $item['author'], 'author' => $item['author'],
@ -182,10 +142,10 @@ route('items', function () {
); );
} }
$response['total_items'] = Database::getInstance('db') $response['total_items'] = Model\Item\count_by_status(
->table('items') $user['id'],
->neq('status', 'removed') array(Model\Item\STATUS_READ, Model\Item\STATUS_UNREAD)
->count(); );
} }
response($response); response($response);
@ -193,10 +153,9 @@ route('items', function () {
// Call: ?api&links // Call: ?api&links
route('links', function () { route('links', function () {
list(, $authenticated, $response) = auth();
$response = auth(); if ($authenticated) {
if ($response['auth']) {
$response['links'] = array(); $response['links'] = array();
} }
@ -205,15 +164,10 @@ route('links', function () {
// Call: ?api&unread_item_ids // Call: ?api&unread_item_ids
route('unread_item_ids', function () { route('unread_item_ids', function () {
list($user, $authenticated, $response) = auth();
$response = auth(); if ($authenticated) {
$item_ids = Model\Item\get_item_ids_by_status($user['id'], Model\Item\STATUS_UNREAD);
if ($response['auth']) {
$item_ids = Database::getInstance('db')
->table('items')
->eq('status', 'unread')
->findAllByColumn('rowid');
$response['unread_item_ids'] = implode(',', $item_ids); $response['unread_item_ids'] = implode(',', $item_ids);
} }
@ -222,15 +176,10 @@ route('unread_item_ids', function () {
// Call: ?api&saved_item_ids // Call: ?api&saved_item_ids
route('saved_item_ids', function () { route('saved_item_ids', function () {
list($user, $authenticated, $response) = auth();
$response = auth(); if ($authenticated) {
$item_ids = Model\Bookmark\get_bookmarked_item_ids($user['id']);
if ($response['auth']) {
$item_ids = Database::getInstance('db')
->table('items')
->eq('bookmark', 1)
->findAllByColumn('rowid');
$response['saved_item_ids'] = implode(',', $item_ids); $response['saved_item_ids'] = implode(',', $item_ids);
} }
@ -239,30 +188,20 @@ route('saved_item_ids', function () {
// handle write items // handle write items
route('write_items', function () { route('write_items', function () {
list($user, $authenticated, $response) = auth();
$response = auth(); if ($authenticated && ctype_digit($_POST['id'])) {
$item_id = $_POST['id'];
if ($response['auth']) {
$query = Database::getInstance('db')
->table('items')
->eq('rowid', $_POST['id']);
if ($_POST['as'] === 'saved') { if ($_POST['as'] === 'saved') {
$query->update(array('bookmark' => 1)); Model\Bookmark\set_flag($user['id'], $item_id, 1);
Handler\Service\sync($user['id'], $item_id);
// Send bookmark to third-party services if enabled
$item_id = Database::getInstance('db')
->table('items')
->eq('rowid', $_POST['id'])
->findOneColumn('id');
Handler\Service\sync($item_id);
} elseif ($_POST['as'] === 'unsaved') { } elseif ($_POST['as'] === 'unsaved') {
$query->update(array('bookmark' => 0)); Model\Bookmark\set_flag($user['id'], $item_id, 0);
} elseif ($_POST['as'] === 'read') { } elseif ($_POST['as'] === 'read') {
$query->update(array('status' => 'read')); Model\Item\change_item_status($user['id'], $item_id, Model\Item\STATUS_READ);
} elseif ($_POST['as'] === 'unread') { } elseif ($_POST['as'] === 'unread') {
$query->update(array('status' => 'unread')); Model\Item\change_item_status($user['id'], $item_id, Model\Item\STATUS_UNREAD);
} }
} }
@ -271,15 +210,16 @@ route('write_items', function () {
// handle write feeds // handle write feeds
route('write_feeds', function () { route('write_feeds', function () {
list($user, $authenticated, $response) = auth();
$response = auth(); if ($authenticated && ctype_digit($_POST['id']) && ctype_digit($_POST['before'])) {
Model\ItemFeed\change_items_status(
if ($response['auth']) { $user['id'],
Database::getInstance('db') $_POST['id'],
->table('items') Model\Item\STATUS_UNREAD,
->eq('feed_id', $_POST['id']) Model\Item\STATUS_READ,
->lte('updated', $_POST['before']) $_POST['before']
->update(array('status' => 'read')); );
} }
response($response); response($response);
@ -287,19 +227,16 @@ route('write_feeds', function () {
// handle write groups // handle write groups
route('write_groups', function () { route('write_groups', function () {
list($user, $authenticated, $response) = auth();
$response = auth(); if ($authenticated && ctype_digit($_POST['id']) && ctype_digit($_POST['before'])) {
Model\ItemGroup\change_items_status(
if ($response['auth']) { $user['id'],
$db = Database::getInstance('db') $_POST['id'],
->table('items') Model\Item\STATUS_UNREAD,
->lte('updated', $_POST['before']); Model\Item\STATUS_READ,
$_POST['before']
if ($_POST['id'] > 0) { );
$db->in('feed_id', Model\Group\get_feeds_by_group($_POST['id']));
}
$db->update(array('status' => 'read'));
} }
response($response); response($response);
@ -320,4 +257,5 @@ if (! empty($_POST['mark']) && ! empty($_POST['as'])
} }
} }
response(auth()); list(, , $response) = auth();
response($response);

View File

@ -5,17 +5,27 @@ require __DIR__.'/app/common.php';
use Miniflux\Router; use Miniflux\Router;
use Miniflux\Response; use Miniflux\Response;
register_shutdown_function(function () {
Miniflux\Helper\write_debug_file();
});
Router\bootstrap( Router\bootstrap(
__DIR__.'/app/controllers', __DIR__.'/app/controllers',
'common', 'common',
'console', 'about',
'user', 'api',
'config', 'auth',
'item',
'history',
'bookmark', 'bookmark',
'config',
'feed', 'feed',
'search' 'help',
'history',
'item',
'opml',
'profile',
'search',
'services',
'users'
); );
Router\notfound(function() { Router\notfound(function() {

View File

@ -2,229 +2,129 @@
require __DIR__.'/app/common.php'; require __DIR__.'/app/common.php';
use JsonRPC\Exception\AuthenticationFailureException;
use JsonRPC\MiddlewareInterface;
use JsonRPC\Server; use JsonRPC\Server;
use Miniflux\Handler;
use Miniflux\Model; use Miniflux\Model;
use Miniflux\Session\SessionStorage;
class AuthMiddleware implements MiddlewareInterface
{
public function execute($username, $password, $procedureName)
{
$user = Model\User\get_user_by_token('api_token', $password);
if (empty($user)) {
throw new AuthenticationFailureException('Wrong credentials!');
}
SessionStorage::getInstance()->setUser($user);
}
}
$server = new Server(); $server = new Server();
$server->getMiddlewareHandler()->withMiddleware(new AuthMiddleware());
$server->authentication(array(
Model\Config\get('username') => Model\Config\get('api_token')
));
$procedureHandler = $server->getProcedureHandler(); $procedureHandler = $server->getProcedureHandler();
// Get version // Get version
$procedureHandler->withCallback('app.version', function () { $procedureHandler->withCallback('getVersion', function () {
return array('version' => APP_VERSION); return array('version' => APP_VERSION);
}); });
// Get all feeds // Get all feeds
$procedureHandler->withCallback('feed.list', function () { $procedureHandler->withCallback('getFeeds', function () {
$feeds = Model\Feed\get_all(); $user_id = SessionStorage::getInstance()->getUserId();
if (empty($feeds)) { $feeds = Model\Feed\get_feeds($user_id);
return array();
}
$groups = Model\Group\get_feeds_map();
foreach ($feeds as &$feed) { foreach ($feeds as &$feed) {
$feed_id = $feed['id']; $feed['groups'] = Model\Group\get_feed_groups($feed['id']);
$feed['feed_group_ids'] = array();
if (isset($groups[$feed_id])) {
$feed['feed_group_ids'] = $groups[$feed_id];
}
} }
return $feeds; return $feeds;
}); });
// Get one feed // Get one feed
$procedureHandler->withCallback('feed.info', function ($feed_id) { $procedureHandler->withCallback('getFeed', function ($feed_id) {
$result = Model\Feed\get($feed_id); $user_id = SessionStorage::getInstance()->getUserId();
$result['feed_group_ids'] = Model\Group\get_feed_group_ids($feed_id); return Model\Feed\get_feed($user_id, $feed_id);
return $result;
}); });
// Add a new feed // Add a new feed
$procedureHandler->withCallback('feed.create', function($url) { $procedureHandler->withCallback('createFeed', function ($url) {
try { $user_id = SessionStorage::getInstance()->getUserId();
$result = Model\Feed\create($url); list($feed_id,) = Handler\Feed\create_feed($user_id, $url);
} catch (Exception $e) {
$result = false; if ($feed_id > 0) {
return $feed_id;
} }
Model\Config\write_debug(); return false;
return $result;
}); });
// Delete a feed // Delete a feed
$procedureHandler->withCallback('feed.delete', function($feed_id) { $procedureHandler->withCallback('deleteFeed', function ($feed_id) {
return Model\Feed\remove($feed_id); $user_id = SessionStorage::getInstance()->getUserId();
return Model\Feed\remove_feed($user_id, $feed_id);
}); });
// Delete all feeds // Refresh a feed
$procedureHandler->withCallback('feed.delete_all', function() { $procedureHandler->withCallback('refreshFeed', function ($feed_id) {
return Model\Feed\remove_all(); $user_id = SessionStorage::getInstance()->getUserId();
return Handler\Feed\update_feed($user_id, $feed_id);
}); });
// Enable a feed // Get all items
$procedureHandler->withCallback('feed.enable', function($feed_id) { $procedureHandler->withCallback('getItems', function ($since_id = null, array $item_ids = array(), $offset = 50) {
return Model\Feed\enable($feed_id); $user_id = SessionStorage::getInstance()->getUserId();
}); return Model\Item\get_items($user_id, $since_id, $item_ids, $offset);
// Disable a feed
$procedureHandler->withCallback('feed.disable', function($feed_id) {
return Model\Feed\disable($feed_id);
});
// Update a feed
$procedureHandler->withCallback('feed.update', function($feed_id) {
return Model\Feed\refresh($feed_id);
});
// Get all groups
$procedureHandler->withCallback('group.list', function () {
return Model\Group\get_all();
});
// Add a new group
$procedureHandler->withCallback('group.create', function($title) {
return Model\Group\create($title);
});
// Get assoc array of group ids with assigned feeds ids
$procedureHandler->withCallback('group.map', function() {
return Model\Group\get_map();
});
// Get the id of a group
$procedureHandler->withCallback('group.id', function($title) {
return Model\Group\get_group_id($title);
});
// Get all feed ids assigned to a group
$procedureHandler->withCallback('group.feeds', function($group_id) {
return Model\Group\get_feeds_by_group($group_id);
});
// Add groups to feed
$procedureHandler->withCallback('group.add', function($feed_id, $group_ids) {
return Model\Group\add($feed_id, $group_ids);
});
// Remove groups from feed
$procedureHandler->withCallback('group.remove', function($feed_id, $group_ids) {
return Model\Group\remove($feed_id, $group_ids);
});
// Remove all groups from feed
$procedureHandler->withCallback('group.remove_all', function($feed_id) {
return Model\Group\remove_all($feed_id);
});
// Update feed group associations
$procedureHandler->withCallback('group.update_feed_groups', function($feed_id, $group_ids, $create_group = '') {
return Model\Group\update_feed_groups($feed_id, $group_ids, $create_group);
});
// Get all items for a specific feed
$procedureHandler->withCallback('item.feed.list', function ($feed_id, $offset = null, $limit = null) {
return Model\ItemFeed\get_all_items($feed_id, $offset, $limit);
});
// Count all feed items
$procedureHandler->withCallback('item.feed.count', function ($feed_id) {
return Model\ItemFeed\count_items($feed_id);
});
// Get all bookmark items
$procedureHandler->withCallback('item.bookmark.list', function ($offset = null, $limit = null) {
return Model\Bookmark\get_all_items($offset, $limit);
});
// Count bookmarks
$procedureHandler->withCallback('item.bookmark.count', function () {
return Model\Bookmark\count_items();
});
// Add a bookmark
$procedureHandler->withCallback('item.bookmark.create', function ($item_id) {
return Model\Bookmark\set_flag($item_id, 1);
});
// Remove a bookmark
$procedureHandler->withCallback('item.bookmark.delete', function ($item_id) {
return Model\Bookmark\set_flag($item_id, 0);
});
// Get all unread items
$procedureHandler->withCallback('item.list_unread', function ($offset = null, $limit = null) {
return Model\Item\get_all_by_status('unread', array(), $offset, $limit);
});
// Count all unread items
$procedureHandler->withCallback('item.count_unread', function () {
return Model\Item\count_by_status('unread');
});
// Get all read items
$procedureHandler->withCallback('item.list_read', function ($offset = null, $limit = null) {
return Model\Item\get_all_by_status('read', array(), $offset, $limit);
});
// Count all read items
$procedureHandler->withCallback('item.count_read', function () {
return Model\Item\count_by_status('read');
}); });
// Get one item // Get one item
$procedureHandler->withCallback('item.info', function ($item_id) { $procedureHandler->withCallback('getItem', function ($item_id) {
return Model\Item\get($item_id); $user_id = SessionStorage::getInstance()->getUserId();
return Model\Item\get_item($user_id, $item_id);
}); });
// Delete an item // Change items status
$procedureHandler->withCallback('item.delete', function($item_id) { $procedureHandler->withCallback('changeItemsStatus', function (array $item_ids, $status) {
return Model\Item\set_removed($item_id); $user_id = SessionStorage::getInstance()->getUserId();
return Model\Item\change_item_ids_status($user_id, $item_ids, $status);
}); });
// Mark item as read // Add a bookmark
$procedureHandler->withCallback('item.mark_as_read', function($item_id) { $procedureHandler->withCallback('addBookmark', function ($item_id) {
return Model\Item\set_read($item_id); $user_id = SessionStorage::getInstance()->getUserId();
return Model\Bookmark\set_flag($user_id, $item_id, 1);
}); });
// Mark item as unread // Remove a bookmark
$procedureHandler->withCallback('item.mark_as_unread', function($item_id) { $procedureHandler->withCallback('removeBookmark', function ($item_id) {
return Model\Item\set_unread($item_id); $user_id = SessionStorage::getInstance()->getUserId();
return Model\Bookmark\set_flag($user_id, $item_id, 0);
}); });
// Change the status of list of items // Get all groups
$procedureHandler->withCallback('item.set_list_status', function($status, array $items) { $procedureHandler->withCallback('getGroups', function () {
return Model\Item\set_status($status, $items); $user_id = SessionStorage::getInstance()->getUserId();
return Model\Group\get_all($user_id);
}); });
// Flush all read items // Add a new group
$procedureHandler->withCallback('item.flush', function() { $procedureHandler->withCallback('createGroup', function ($title) {
return Model\Item\mark_all_as_removed(); $user_id = SessionStorage::getInstance()->getUserId();
return Model\Group\create_group($user_id, $title);
}); });
// Mark all unread items as read // Add/Update groups for a feed
$procedureHandler->withCallback('item.mark_all_as_read', function() { $procedureHandler->withCallback('setFeedGroups', function ($feed_id, $group_ids) {
return Model\Item\mark_all_as_read(); $user_id = SessionStorage::getInstance()->getUserId();
return Model\Group\update_feed_groups($user_id, $feed_id, $group_ids);
}); });
// Get all items with the content // Get favicons
$procedureHandler->withCallback('item.get_all', function() { $procedureHandler->withCallback('getFavicons', function () {
return Model\Item\get_all(); $user_id = SessionStorage::getInstance()->getUserId();
}); return Model\Favicon\get_favicons_with_data_url($user_id);
// Get all items since a date
$procedureHandler->withCallback('item.get_all_since', function($timestamp) {
return Model\Item\get_all_since($timestamp);
});
// Get all items id and status
$procedureHandler->withCallback('item.get_all_status', function() {
return Model\Item\get_all_status();
}); });
echo $server->execute(); echo $server->execute();

View File

@ -13,11 +13,19 @@ $options = getopt('', array(
'limit::', 'limit::',
)); ));
$limit = ! empty($options['limit']) && ctype_digit($options['limit']) ? (int) $options['limit'] : Model\Feed\LIMIT_ALL; $limit = get_cli_option('limit', $options);
$connection = new Pheanstalk(BEANSTALKD_HOST); $connection = new Pheanstalk(BEANSTALKD_HOST);
foreach (Model\Feed\get_ids($limit) as $feed_id) { foreach (Model\User\get_all_users() as $user) {
foreach (Model\Feed\get_feed_ids($user['id'], $limit) as $feed_id) {
$payload = serialize(array(
'feed_id' => $feed_id,
'user_id' => $user['id'],
));
$connection $connection
->useTube(BEANSTALKD_QUEUE) ->useTube(BEANSTALKD_QUEUE)
->put($feed_id, Pheanstalk::DEFAULT_PRIORITY, Pheanstalk::DEFAULT_DELAY, BEANSTALKD_TTL); ->put($payload, Pheanstalk::DEFAULT_PRIORITY, Pheanstalk::DEFAULT_DELAY, BEANSTALKD_TTL);
}
} }

245
scripts/migrate-db.php Normal file
View File

@ -0,0 +1,245 @@
<?php
require_once __DIR__.'/../app/common.php';
use Miniflux\Helper;
use Miniflux\Model;
if (php_sapi_name() !== 'cli') {
die('This script can run only from the command line.'.PHP_EOL);
}
$options = getopt('', array(
'sqlite-db:',
'admin::',
));
if (empty($options)) {
die('Usage: '.$argv[0].' --sqlite-db=/path/to/my/db.sqlite --admin=1|0'.PHP_EOL);
}
$src_file = $options['sqlite-db'];
$is_admin = isset($options['admin']) ? (int) $options['admin'] : 0;
$src = new PDO('sqlite:' . $src_file);
$dst = PicoDb\Database::getInstance('db')->getConnection();
function get_settings(PDO $db)
{
$rq = $db->prepare('SELECT * FROM settings');
$rq->execute();
$rows = $rq->fetchAll(PDO::FETCH_ASSOC);
$settings = array();
foreach ($rows as $row) {
$settings[$row['key']] = $row['value'];
}
return $settings;
}
function get_table(PDO $db, $table)
{
$rq = $db->prepare('SELECT * FROM '.$table);
$rq->execute();
return $rq->fetchAll(PDO::FETCH_ASSOC);
}
function create_user(PDO $db, array $settings, $is_admin)
{
$rq = $db->prepare('
INSERT INTO users
(username, password, is_admin, last_login, api_token, bookmarklet_token, cronjob_token, feed_token, fever_token, fever_api_key)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
');
$rq->execute(array(
$settings['username'],
$settings['password'],
$is_admin,
$settings['last_login'],
$settings['api_token'],
$settings['bookmarklet_token'],
Helper\generate_token(),
$settings['feed_token'],
$settings['fever_token'],
md5($settings['username'] . ':' . $settings['fever_token']),
));
return $db->lastInsertId();
}
function copy_settings(PDO $db, $user_id, array $settings)
{
$exclude_keys = array(
'username',
'password',
'last_login',
'api_token',
'bookmarklet_token',
'feed_token',
'fever_token',
'debug_mode',
);
$rq = $db->prepare('INSERT INTO user_settings ("user_id", "key", "value") VALUES (?, ?, ?)');
foreach ($settings as $key => $value) {
if (! in_array($key, $exclude_keys)) {
$rq->execute(array($user_id, $key, $value));
}
}
}
function copy_feeds(PDO $db, $user_id, array $feeds)
{
$feed_ids = array();
$rq = $db->prepare('INSERT INTO feeds
(user_id, feed_url, site_url, title, enabled, download_content, rtl, cloak_referrer)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?)
');
foreach ($feeds as $feed) {
$rq->execute(array(
$user_id,
$feed['feed_url'],
$feed['site_url'],
$feed['title'],
$feed['enabled'],
$feed['download_content'],
$feed['rtl'],
$feed['cloak_referrer'],
));
$feed_ids[$feed['id']] = $db->lastInsertId();
}
return $feed_ids;
}
function copy_items(PDO $db, $user_id, array $feed_ids, array $items)
{
$rq = $db->prepare('INSERT INTO items
(user_id, feed_id, checksum, status, bookmark, url, title, author, content, updated, enclosure_url, enclosure_type, language)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
');
foreach ($items as $item) {
$rq->execute(array(
$user_id,
$feed_ids[$item['feed_id']],
$item['id'],
$item['status'],
$item['bookmark'],
$item['url'],
$item['title'],
$item['author'],
$item['content'],
$item['updated'],
$item['enclosure'],
$item['enclosure_type'],
$item['language'],
));
}
}
function copy_favicons(PDO $db, array $feed_ids, array $favicons, array $favicons_feeds)
{
$favicon_ids = array();
foreach ($favicons as $favicon) {
$rq = $db->prepare('SELECT id from favicons WHERE "hash"=?');
$rq->execute(array($favicon['hash']));
$favicon_id = $rq->fetch(PDO::FETCH_COLUMN);
if ($favicon_id) {
$favicon_ids[$favicon['id']] = $favicon_id;
} else {
$rq = $db->prepare('INSERT INTO favicons
(hash, type)
VALUES
(?, ?)
');
$rq->execute(array(
$favicon['hash'],
$favicon['type'],
));
$favicon_ids[$favicon['id']] = $db->lastInsertId();
}
}
$rq = $db->prepare('INSERT INTO favicons_feeds
(feed_id, favicon_id)
VALUES
(?, ?)
');
foreach ($favicons_feeds as $row) {
$rq->execute(array(
$feed_ids[$row['feed_id']],
$favicon_ids[$row['favicon_id']],
));
}
}
function copy_groups(PDO $db, $user_id, array $feed_ids, array $groups, array $feeds_groups)
{
$group_ids = array();
foreach ($groups as $group) {
$rq = $db->prepare('INSERT INTO groups
(user_id, title)
VALUES
(?, ?)
');
$rq->execute(array(
$user_id,
$group['title'],
));
$group_ids[$group['id']] = $db->lastInsertId();
}
$rq = $db->prepare('INSERT INTO feeds_groups
(feed_id, group_id)
VALUES
(?, ?)
');
foreach ($feeds_groups as $row) {
$rq->execute(array(
$feed_ids[$row['feed_id']],
$group_ids[$row['group_id']],
));
}
}
$settings = get_settings($src);
$feeds = get_table($src, 'feeds');
$items = get_table($src, 'items');
$groups = get_table($src, 'groups');
$feeds_groups = get_table($src, 'feeds_groups');
$favicons = get_table($src, 'favicons');
$favicons_feeds = get_table($src, 'favicons_feeds');
try {
$dst->beginTransaction();
$user_id = create_user($dst, $settings, $is_admin);
copy_settings($dst, $user_id, $settings);
$feed_ids = copy_feeds($dst, $user_id, $feeds);
copy_items($dst, $user_id, $feed_ids, $items);
copy_favicons($dst, $feed_ids, $favicons, $favicons_feeds);
copy_groups($dst, $user_id, $feed_ids, $groups, $feeds_groups);
$dst->commit();
} catch (PDOException $e) {
$dst->rollBack();
echo $e->getMessage().PHP_EOL;
}

View File

@ -1,10 +1,79 @@
<?php <?php
require_once __DIR__.'/../../vendor/autoload.php'; use Miniflux\Model;
use Miniflux\Session\SessionStorage;
use PicoDb\Database;
use PicoFeed\Parser\Feed;
use PicoFeed\Parser\Item;
require_once __DIR__.'/../../app/common.php';
abstract class BaseTest extends PHPUnit_Framework_TestCase abstract class BaseTest extends PHPUnit_Framework_TestCase
{ {
public function setUp() public function setUp()
{ {
SessionStorage::getInstance()->flush();
PicoDb\Database::setInstance('db', function () {
$db = new PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => DB_FILENAME,
));
$db->getStatementHandler()->withLogging();
if (! $db->schema('\Miniflux\Schema')->check(Miniflux\Schema\VERSION)) {
var_dump($db->getLogMessages());
}
return $db;
});
}
public function tearDown()
{
Database::getInstance('db')->closeConnection();
}
public function buildItem($itemId)
{
$item = new Item();
$item->setId($itemId);
$item->setTitle('Item #1');
$item->setUrl('some url');
$item->setContent('some content');
$item->setDate(new DateTime());
return $item;
}
public function buildFeed()
{
$items = array();
$item = new Item();
$item->setId('ID 1');
$item->setTitle('Item #1');
$item->setUrl('some url');
$item->setContent('some content');
$item->setDate(new DateTime());
$items[] = $item;
$item = new Item();
$item->setId('ID 2');
$item->setTitle('Item #2');
$item->setUrl('some url');
$item->setDate(new DateTime());
$items[] = $item;
$feed = new Feed();
$feed->setTitle('My feed');
$feed->setFeedUrl('feed url');
$feed->setSiteUrl('site url');
$feed->setItems($items);
return $feed;
}
public function assertCreateFeed(Feed $feed)
{
$this->assertEquals(1, Model\Feed\create(1, $feed, 'etag', 'last modified'));
} }
} }

View File

@ -0,0 +1,38 @@
<?php
use Miniflux\Model;
require_once __DIR__.'/BaseTest.php';
class BookmarkModelTest extends BaseTest
{
public function testSetBookmark()
{
$this->assertCreateFeed($this->buildFeed());
$this->assertTrue(Model\Bookmark\set_flag(1, 1, 1));
$item = Model\Item\get_item(1, 1);
$this->assertEquals(1, $item['bookmark']);
$this->assertTrue(Model\Bookmark\set_flag(1, 1, 0));
$item = Model\Item\get_item(1, 1);
$this->assertEquals(0, $item['bookmark']);
}
public function testCountBookmarkedItems()
{
$this->assertCreateFeed($this->buildFeed());
$this->assertTrue(Model\Bookmark\set_flag(1, 1, 1));
$this->assertEquals(1, Model\Bookmark\count_bookmarked_items(1));
$this->assertEquals(0, Model\Bookmark\count_bookmarked_items(1, array(2)));
}
public function testGetBookmarkedItems()
{
$this->assertCreateFeed($this->buildFeed());
$this->assertTrue(Model\Bookmark\set_flag(1, 1, 1));
$items = Model\Bookmark\get_bookmarked_items(1);
$this->assertCount(1, $items);
}
}

View File

@ -0,0 +1,25 @@
<?php
use Miniflux\Model;
require_once __DIR__.'/BaseTest.php';
class ConfigModelTest extends BaseTest
{
public function testGetAllAndSave()
{
$settings = Model\Config\get_all(1);
$this->assertNotEmpty($settings);
$this->assertArrayHasKey('pinboard_enabled', $settings);
$this->assertTrue(Model\Config\save(1, array('foobar' => 'something')));
$settings = Model\Config\get_all(1);
$this->assertEquals('something', $settings['foobar']);
$this->assertTrue(Model\Config\save(1, array('foobar' => 'something else')));
$settings = Model\Config\get_all(1);
$this->assertEquals('something else', $settings['foobar']);
}
}

View File

@ -0,0 +1,162 @@
<?php
use Miniflux\Model;
use PicoFeed\Parser\Feed;
use PicoFeed\Parser\Item;
require_once __DIR__.'/BaseTest.php';
class FeedModelTest extends BaseTest
{
public function testCreate()
{
$feed = new Feed();
$feed->setTitle('My feed');
$feed->setFeedUrl('feed url');
$feed->setSiteUrl('site url');
$this->assertEquals(1, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$this->assertEquals(-1, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$subscription = Model\Feed\get_feed(1, 1);
$this->assertNotEmpty($subscription);
$this->assertEquals('1', $subscription['user_id']);
$this->assertEquals('My feed', $subscription['title']);
$this->assertEquals('site url', $subscription['site_url']);
$this->assertEquals('feed url', $subscription['feed_url']);
$this->assertEquals('etag', $subscription['etag']);
$this->assertEquals('last modified', $subscription['last_modified']);
$this->assertEquals('0', $subscription['download_content']);
$this->assertEquals('0', $subscription['rtl']);
$this->assertEquals('0', $subscription['cloak_referrer']);
$this->assertEquals('1', $subscription['enabled']);
}
public function testGetAll()
{
$feed = new Feed();
$feed->setTitle('My feed');
$feed->setFeedUrl('feed url');
$feed->setSiteUrl('site url');
$this->assertEquals(1, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$feed = new Feed();
$feed->setTitle('Some feed');
$feed->setFeedUrl('another feed url');
$feed->setSiteUrl('site url');
$this->assertEquals(2, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$feeds = Model\Feed\get_feeds(1);
$this->assertCount(2, $feeds);
}
public function testGetFeedIds()
{
$feed = new Feed();
$feed->setTitle('My feed');
$feed->setFeedUrl('feed url');
$feed->setSiteUrl('site url');
$this->assertEquals(1, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$feed = new Feed();
$feed->setTitle('Some feed');
$feed->setFeedUrl('another feed url');
$feed->setSiteUrl('site url');
$this->assertEquals(2, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$feed_ids = Model\Feed\get_feed_ids(1);
$this->assertEquals(array(1, 2), $feed_ids);
$feed_ids = Model\Feed\get_feed_ids(1, 1);
$this->assertEquals(array(1), $feed_ids);
}
public function testGetFeedWithItemsCount()
{
$item = new Item();
$item->setId('ID 1');
$item->setTitle('Item #1');
$item->setUrl('some url');
$item->setDate(new DateTime());
$feed = new Feed();
$feed->setTitle('My feed');
$feed->setFeedUrl('feed url');
$feed->setSiteUrl('site url');
$feed->setItems(array($item));
$this->assertEquals(1, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$feed = new Feed();
$feed->setTitle('Some feed');
$feed->setFeedUrl('another feed url');
$feed->setSiteUrl('site url');
$this->assertEquals(2, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$feeds = Model\Feed\get_feeds_with_items_count(1);
$this->assertCount(2, $feeds);
$this->assertEquals(1, $feeds[0]['items_unread']);
$this->assertEquals(1, $feeds[0]['items_total']);
$this->assertEquals(0, $feeds[1]['items_unread']);
$this->assertEquals(0, $feeds[1]['items_total']);
}
public function testUpdate()
{
$feed = new Feed();
$feed->setTitle('My feed');
$feed->setFeedUrl('feed url');
$feed->setSiteUrl('site url');
$this->assertEquals(1, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$this->assertTrue(Model\Feed\update_feed(1, 1, array('title' => 'new title')));
$subscription = Model\Feed\get_feed(1, 1);
$this->assertNotEmpty($subscription);
$this->assertEquals('1', $subscription['user_id']);
$this->assertEquals('new title', $subscription['title']);
}
public function testChangeStatus()
{
$feed = new Feed();
$feed->setTitle('My feed');
$feed->setFeedUrl('feed url');
$feed->setSiteUrl('site url');
$this->assertEquals(1, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$this->assertTrue(Model\Feed\change_feed_status(1, 1, Model\Feed\STATUS_INACTIVE));
$subscription = Model\Feed\get_feed(1, 1);
$this->assertEquals(0, $subscription['enabled']);
}
public function testRemoveFeed()
{
$feed = new Feed();
$feed->setTitle('My feed');
$feed->setFeedUrl('feed url');
$feed->setSiteUrl('site url');
$this->assertEquals(1, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$this->assertTrue(Model\Feed\remove_feed(1, 1));
$this->assertNull(Model\Feed\get_feed(1, 1));
}
public function testPeopleCanHaveSameFeed()
{
$feed = new Feed();
$feed->setTitle('My feed');
$feed->setFeedUrl('feed url');
$feed->setSiteUrl('site url');
$this->assertEquals(2, Model\User\create_user('foobar', 'test'));
$this->assertEquals(1, Model\Feed\create(1, $feed, 'etag', 'last modified'));
$this->assertEquals(2, Model\Feed\create(2, $feed, 'etag', 'last modified'));
}
}

View File

@ -0,0 +1,76 @@
<?php
use Miniflux\Model;
require_once __DIR__.'/BaseTest.php';
class GroupModelTest extends BaseTest
{
public function testCreateGroup()
{
$this->assertEquals(2, Model\User\create_user('somebody', 'test'));
$this->assertEquals(1, Model\Group\create_group(1, 'tag'));
$this->assertEquals(1, Model\Group\create_group(1, 'tag'));
$this->assertEquals(1, Model\Group\get_group_id_from_title(1, 'tag'));
$this->assertFalse(Model\Group\get_group_id_from_title(1, 'notfound'));
$this->assertEquals(2, Model\Group\create_group(2, 'tag'));
$this->assertEquals(2, Model\Group\create_group(2, 'tag'));
}
public function testGetAll()
{
$this->assertSame(array(), Model\Group\get_all(1));
$this->assertEquals(1, Model\Group\create_group(1, 'tag 1'));
$this->assertEquals(2, Model\Group\create_group(1, 'tag 2'));
$groups = Model\Group\get_all(1);
$this->assertCount(2, $groups);
$this->assertEquals(1, $groups[0]['id']);
$this->assertEquals('tag 1', $groups[0]['title']);
$this->assertEquals(2, $groups[1]['id']);
$this->assertEquals('tag 2', $groups[1]['title']);
}
public function testAssociation()
{
$this->assertCreateFeed($this->buildFeed());
$this->assertEquals(1, Model\Group\create_group(1, 'tag 1'));
$this->assertEquals(2, Model\Group\create_group(1, 'tag 2'));
$this->assertEquals(3, Model\Group\create_group(1, 'tag 3'));
$this->assertTrue(Model\Group\update_feed_groups(1, 1, array(1, 2), 'tag 4'));
$this->assertEquals(array(1), Model\Group\get_feed_ids_by_group(1));
$this->assertEquals(array(1), Model\Group\get_feed_ids_by_group(2));
$this->assertEquals(array(), Model\Group\get_feed_ids_by_group(3));
$this->assertEquals(array(1), Model\Group\get_feed_ids_by_group(4));
$groups = Model\Group\get_feed_groups(1);
$expected_groups = array(
array('id' => 1, 'title' => 'tag 1'),
array('id' => 2, 'title' => 'tag 2'),
array('id' => 4, 'title' => 'tag 4'),
);
$this->assertEquals($expected_groups, $groups);
$this->assertEquals(array(1, 2, 4), Model\Group\get_feed_group_ids(1));
$this->assertEquals(array(), Model\Group\get_feed_group_ids(2));
$expected = array(
1 => array(1),
2 => array(1),
4 => array(1),
);
$this->assertEquals($expected, Model\Group\get_groups_feed_ids(1));
$this->assertTrue(Model\Group\update_feed_groups(1, 1, array(1, 3)));
$this->assertEquals(array(1, 3), Model\Group\get_feed_group_ids(1));
}
}

View File

@ -1,9 +1,28 @@
<?php <?php
use Miniflux\Helper; use Miniflux\Helper;
use Miniflux\Model;
use Miniflux\Session\SessionStorage;
require_once __DIR__.'/BaseTest.php';
class HelperTest extends BaseTest class HelperTest extends BaseTest
{ {
public function testConfig()
{
SessionStorage::getInstance()->setUser(array('id' => 1, 'user_id' => 1, 'username' => 'admin', 'is_admin' => 1));
$this->assertNull(Helper\config('option'));
$this->assertSame('default', Helper\config('option', 'default'));
$this->assertTrue(Model\Config\save(1, array('option1' => '1', 'option2' => '0')));
$this->assertTrue(Helper\bool_config('option1'));
$this->assertFalse(Helper\bool_config('option2'));
$this->assertFalse(Helper\bool_config('option3'));
$this->assertTrue(Helper\bool_config('option4', true));
}
public function testGenerateToken() public function testGenerateToken()
{ {
$token1 = Helper\generate_token(); $token1 = Helper\generate_token();

Some files were not shown because too many files have changed in this diff Show More