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
config.php
!app/helpers/*
!app/models/*
!app/controllers/*
!app/templates/*

View File

@ -16,4 +16,4 @@ before_script:
- composer install
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-push
.PHONY: docker-destroy
.PHONY: docker-run
.PHONY: archive
.PHONY: js
.PHONY: unit-test-sqlite
JS_FILE = assets/js/all.js
CONTAINER = miniflux
@ -36,3 +37,6 @@ $(JS_FILE): assets/js/app.js \
# Build a new archive: make archive version=1.2.3 dst=/tmp
archive:
@ 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__.'/constants.php';
require __DIR__.'/check_setup.php';
require __DIR__.'/functions.php';
require_once __DIR__.'/constants.php';
require_once __DIR__.'/check_setup.php';
require_once __DIR__.'/functions.php';
PicoDb\Database::setInstance('db', function() {
$db = new PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => Miniflux\Model\Database\get_path(),
'filename' => DB_FILENAME,
));
$db->getStatementHandler()->withLogging();
if ($db->schema('\Miniflux\Schema')->check(Miniflux\Schema\VERSION)) {
return $db;
}
else {
} else {
$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 .= '<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 .= '- SAPI: '.php_sapi_name().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$'));
define('HTTP_USER_AGENT', 'Miniflux (https://miniflux.net)');
defined('HTTP_TIMEOUT') or define('HTTP_TIMEOUT', 20);
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_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', 'db.sqlite');
defined('DB_FILENAME') or define('DB_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'db.sqlite');
defined('DEBUG_MODE') or define('DEBUG_MODE', false);
defined('DEBUG_FILENAME') or define('DEBUG_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'debug.log');
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('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_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
namespace Miniflux\Controller;
use Miniflux\Session\SessionManager;
use Miniflux\Session\SessionStorage;
use Miniflux\Validator;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Model\RememberMe;
use Miniflux\Request;
use Miniflux\Template;
use Miniflux\Helper;
@ -10,13 +15,15 @@ use Miniflux\Model;
// Logout and destroy session
Router\get_action('logout', function () {
Model\User\logout();
SessionStorage::getInstance()->flush();
SessionManager::close();
RememberMe\destroy();
Response\redirect('?action=login');
});
// Display form login
Router\get_action('login', function () {
if (Model\User\is_loggedin()) {
if (SessionStorage::getInstance()->isLogged()) {
Response\redirect('?action=unread');
}
@ -25,8 +32,6 @@ Router\get_action('login', function () {
'values' => array(
'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(
'errors' => $errors,
'values' => $values + array('csrf' => Helper\generate_csrf()),
'databases' => Model\Database\get_list(),
'current_database' => Model\Database\select()
)));
});

View File

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

View File

@ -1,10 +1,12 @@
<?php
namespace Miniflux\Controller;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Request;
use Miniflux\Session;
use Miniflux\Template;
use Miniflux\Session\SessionManager;
use Miniflux\Session\SessionStorage;
use Miniflux\Helper;
use Miniflux\Model;
use Miniflux\Translator;
@ -12,39 +14,22 @@ use Miniflux\Handler;
// Called before each action
Router\before(function ($action) {
Session\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');
}
}
SessionManager::open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH, 0);
// These actions are considered to be safe even for unauthenticated users
$safe_actions = array('login', 'bookmark-feed', 'select-db', 'logout', 'notfound');
if (! Model\User\is_loggedin() && ! in_array($action, $safe_actions)) {
$safe_actions = array('login', 'bookmark-feed', 'logout', 'notfound');
if (! SessionStorage::getInstance()->isLogged() && ! in_array($action, $safe_actions)) {
if (! Model\RememberMe\authenticate()) {
Model\User\logout();
Response\redirect('?action=login');
}
} elseif (Model\RememberMe\has_cookie()) {
Model\RememberMe\refresh();
}
// Load translations
$language = Model\Config\get('language') ?: 'en_US';
$language = Helper\config('language', 'en_US');
Translator\load($language);
// Set timezone
date_default_timezone_set(Model\Config\get('timezone') ?: 'UTC');
date_default_timezone_set(Helper\config('timezone', 'UTC'));
// HTTP secure headers
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)
Router\get_action('proxy', function () {
Handler\Proxy\download(rawurldecode(Request\param('url')));
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
use PicoDb\Database;
namespace Miniflux\Controller;
use Miniflux\Session\SessionStorage;
use Miniflux\Validator;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Request;
use Miniflux\Session;
use Miniflux\Template;
use Miniflux\Helper;
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
Router\get_action('confirm-auto-update', function () {
Response\html(Template\layout('confirm_auto_update', array(
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
'title' => t('Confirmation')
)));
@ -68,10 +22,10 @@ Router\get_action('confirm-auto-update', function () {
// Auto-update
Router\get_action('auto-update', function () {
if (ENABLE_AUTO_UPDATE) {
if (Model\AutoUpdate\execute(Model\Config\get('auto_update_url'))) {
Session\flash(t('Miniflux is updated!'));
if (Model\AutoUpdate\execute(Helper\config('auto_update_url'))) {
SessionStorage::getInstance()->setFlashMessage(t('Miniflux is updated!'));
} 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
Router\get_action('generate-tokens', function () {
$user_id = SessionStorage::getInstance()->getUserId();
if (Helper\check_csrf(Request\param('csrf'))) {
Model\Config\new_tokens();
Model\User\regenerate_tokens($user_id);
}
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
Router\get_action('config', function () {
$user_id = SessionStorage::getInstance()->getUserId();
Response\html(Template\layout('config', 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(),
'timezones' => Model\Config\get_timezones(),
'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(),
'item_title_link' => Model\Config\get_item_title_link(),
'redirect_nothing_to_read_options' => Model\Config\get_nothing_to_read_redirections(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
'title' => t('Preferences')
)));
@ -127,15 +67,16 @@ Router\get_action('config', function () {
// Update preferences
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);
list($valid, $errors) = Validator\Config\validate_modification($values);
if ($valid) {
if (Model\Config\save($values)) {
Session\flash(t('Your preferences are updated.'));
if (Model\Config\save($user_id, $values)) {
SessionStorage::getInstance()->setFlashMessage(t('Your preferences are updated.'));
} else {
Session\flash_error(t('Unable to update your preferences.'));
SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to update your preferences.'));
}
Response\redirect('?action=config');
@ -143,7 +84,7 @@ Router\post_action('config', function () {
Response\html(Template\layout('config', array(
'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(),
'timezones' => Model\Config\get_timezones(),
'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(),
'display_mode' => Model\Config\get_display_mode(),
'item_title_link' => Model\Config\get_item_title_link(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'config',
'title' => t('Preferences')
)));
@ -162,84 +102,17 @@ Router\post_action('config', function () {
// Get configuration parameters (AJAX request)
Router\post_action('get-config', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$return = array();
$options = Request\values();
if (empty($options)) {
$return = Model\Config\get_all();
$return = Model\Config\get_all($user_id);
} else {
foreach ($options as $name) {
$return[$name] = Model\Config\get($name);
$return[$name] = Helper\config($name);
}
}
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
use PicoFeed\Parser\MalformedXmlException;
namespace Miniflux\Controller;
use Miniflux\Session\SessionStorage;
use Miniflux\Validator;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Request;
use Miniflux\Session;
use Miniflux\Template;
use Miniflux\Helper;
use Miniflux\Handler;
@ -13,25 +14,26 @@ use Miniflux\Model;
// Refresh all feeds, used when Javascript is disabled
Router\get_action('refresh-all', function () {
Model\Feed\refresh_all();
Session\flash(t('Your subscriptions are updated'));
$user_id = SessionStorage::getInstance()->getUserId();
Handler\Feed\update_feeds($user_id);
SessionStorage::getInstance()->setFlashErrorMessage(t('Your subscriptions are updated'));
Response\redirect('?action=unread');
});
// Edit feed form
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(
'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(
'values' => $values,
'errors' => array(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'groups' => Model\Group\get_all(),
'groups' => Model\Group\get_all($user_id),
'menu' => 'feeds',
'title' => t('Edit subscription')
)));
@ -39,32 +41,32 @@ Router\get_action('edit-feed', function () {
// Submit edit feed form
Router\post_action('edit-feed', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$values = Request\values();
$values += array(
'enabled' => 0,
'download_content' => 0,
'rtl' => 0,
'cloak_referrer' => 0,
'parsing_error' => 0,
'feed_group_ids' => array(),
'create_group' => ''
);
list($valid, $errors) = Validator\Feed\validate_modification($values);
if ($valid) {
if (Model\Feed\update($values)) {
Session\flash(t('Your subscription has been updated.'));
if (Model\Feed\update_feed($user_id, $values['id'], $values)) {
SessionStorage::getInstance()->setFlashMessage(t('Your subscription has been updated.'));
Response\redirect('?action=feeds');
} 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(
'values' => $values,
'errors' => $errors,
'nb_unread_items' => Model\Item\count_by_status('unread'),
'groups' => Model\Group\get_all(),
'groups' => Model\Group\get_all($user_id),
'menu' => 'feeds',
'title' => t('Edit subscription')
)));
@ -72,11 +74,11 @@ Router\post_action('edit-feed', function () {
// Confirmation box to remove a feed
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(
'feed' => Model\Feed\get($id),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'feed' => Model\Feed\get_feed($user_id, $feed_id),
'menu' => 'feeds',
'title' => t('Confirmation')
)));
@ -84,12 +86,13 @@ Router\get_action('confirm-remove-feed', function () {
// Remove a feed
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)) {
Session\flash(t('This subscription has been removed successfully.'));
if (Model\Feed\remove_feed($user_id, $feed_id)) {
SessionStorage::getInstance()->setFlashMessage(t('This subscription has been removed successfully.'));
} else {
Session\flash_error(t('Unable to remove this subscription.'));
SessionStorage::getInstance()->setFlashErrorMessage(t('Unable to remove this subscription.'));
}
Response\redirect('?action=feeds');
@ -97,40 +100,43 @@ Router\get_action('remove-feed', function () {
// Refresh one feed and redirect to unread items
Router\get_action('refresh-feed', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$feed_id = Request\int_param('feed_id');
$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);
});
// Ajax call to refresh one feed
Router\post_action('refresh-feed', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$feed_id = Request\int_param('feed_id', 0);
Response\json(array(
'feed_id' => $feed_id,
'result' => Model\Feed\refresh($feed_id),
'items_count' => Model\Feed\count_items($feed_id),
'result' => Handler\Feed\update_feed($user_id, $feed_id),
'items_count' => Model\ItemFeed\count_items_by_status($user_id, $feed_id),
));
});
// Display all feeds
Router\get_action('feeds', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$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) {
Response\redirect('?action=unread');
}
Response\html(Template\layout('feeds', array(
'favicons' => Model\Favicon\get_all_favicons(),
'feeds' => Model\Feed\get_all_item_counts(),
'favicons' => Model\Favicon\get_feeds_favicons($feeds),
'feeds' => $feeds,
'nothing_to_read' => $nothing_to_read,
'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',
'title' => t('Subscriptions')
)));
@ -138,21 +144,21 @@ Router\get_action('feeds', function () {
// Display form to add one feed
Router\get_action('add', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$values = array(
'download_content' => 0,
'rtl' => 0,
'cloak_referrer' => 0,
'create_group' => '',
'feed_group_ids' => array()
'feed_group_ids' => array(),
);
Response\html(Template\layout('add', array(
'values' => $values + array('csrf' => Helper\generate_csrf()),
'errors' => array(),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'groups' => Model\Group\get_all(),
'groups' => Model\Group\get_all($user_id),
'menu' => 'feeds',
'title' => t('New subscription')
'title' => t('New subscription'),
)));
});
@ -162,14 +168,18 @@ Router\action('subscribe', function () {
$values = Request\values();
Helper\check_csrf_values($values);
$url = isset($values['url']) ? $values['url'] : '';
$user_id = SessionStorage::getInstance()->getUserId();
} else {
$values = array();
$url = Request\param('url');
$token = Request\param('token');
$user = Model\User\get_user_by_token('bookmarklet_token', $token);
$values = array();
if ($token !== Model\Config\get('bookmarklet_token')) {
Response\text('Access Forbidden', 403);
if (empty($user)) {
Response\text('Unauthorized', 401);
}
$user_id = $user['id'];
}
$values += array(
@ -177,85 +187,30 @@ Router\action('subscribe', function () {
'download_content' => 0,
'rtl' => 0,
'cloak_referrer' => 0,
'create_group' => '',
'feed_group_ids' => array()
'feed_group_ids' => array(),
);
try {
$feed_id = Model\Feed\create(
list($feed_id, $error_message) = Handler\Feed\create_feed(
$user_id,
$values['url'],
$values['download_content'],
$values['rtl'],
$values['cloak_referrer'],
$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 (isset($feed_id) && $feed_id !== false) {
Session\flash(t('Subscription added successfully.'));
if ($feed_id >= 1) {
SessionStorage::getInstance()->setFlashMessage(t('Subscription added successfully.'));
Response\redirect('?action=feed-items&feed_id='.$feed_id);
} else {
if (! isset($error_message)) {
$error_message = t('Error occured.');
}
Session\flash_error($error_message);
SessionStorage::getInstance()->setFlashErrorMessage($error_message);
}
Response\html(Template\layout('add', array(
'values' => $values + array('csrf' => Helper\generate_csrf()),
'nb_unread_items' => Model\Item\count_by_status('unread'),
'groups' => Model\Group\get_all(),
'groups' => Model\Group\get_all($user_id),
'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
namespace Miniflux\Controller;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Request;
use Miniflux\Session\SessionStorage;
use Miniflux\Template;
use Miniflux\Helper;
use Miniflux\Model;
// Display history page
Router\get_action('history', function () {
$order = Request\param('order', 'updated');
$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();
$params = items_list(Model\Item\STATUS_READ);
if ($group_id !== null) {
$feed_ids = Model\Group\get_feeds_by_group($group_id);
}
$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'),
Response\html(Template\layout('history', $params + array(
'title' => t('History') . ' (' . $params['nb_items'] . ')',
'menu' => 'history',
'groups' => Model\Group\get_all(),
'title' => t('History').' ('.$nb_items.')'
)));
});
// Confirmation box to flush history
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(
'group_id' => $group_id,
'nb_unread_items' => Model\Item\count_by_status('unread'),
'menu' => 'history',
'title' => t('Confirmation')
)));
@ -64,12 +32,13 @@ Router\get_action('confirm-flush-history', function () {
// Flush history
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) {
Model\ItemGroup\mark_all_as_removed($group_id);
if ($group_id !== 0) {
Model\ItemGroup\change_items_status($user_id, $group_id, Model\Item\STATUS_READ, Model\Item\STATUS_REMOVED);
} 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');

View File

@ -1,101 +1,73 @@
<?php
namespace Miniflux\Controller;
use Miniflux\Helper;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Request;
use Miniflux\Session\SessionStorage;
use Miniflux\Template;
use Miniflux\Helper;
use Miniflux\Handler;
use Miniflux\Model;
// Display unread items
Router\get_action('unread', function () {
Model\Item\autoflush_read();
Model\Item\autoflush_unread();
$user_id = SessionStorage::getInstance()->getUserId();
$order = Request\param('order', 'updated');
$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();
Model\Item\autoflush_read($user_id);
Model\Item\autoflush_unread($user_id);
if ($group_id !== null) {
$feed_ids = Model\Group\get_feeds_by_group($group_id);
}
$params = items_list(Model\Item\STATUS_UNREAD);
$items = Model\Item\get_all_by_status(
'unread',
$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');
if ($params['nb_unread_items'] === 0) {
$action = Helper\config('redirect_nothing_to_read', 'feeds');
Response\redirect('?action='.$action.'&nothing_to_read=1');
}
Response\html(Template\layout('unread_items', array(
'favicons' => Model\Favicon\get_item_favicons($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.')',
Response\html(Template\layout('unread_items', $params + array(
'title' => 'Miniflux (' . $params['nb_items'] . ')',
'menu' => 'unread',
'groups' => Model\Group\get_all()
)));
});
// Show item
Router\get_action('show', function () {
$id = Request\param('id');
$user_id = SessionStorage::getInstance()->getUserId();
$item_id = Request\param('id');
$menu = Request\param('menu');
$item = Model\Item\get($id);
$feed = Model\Feed\get($item['feed_id']);
$item = Model\Item\get_item($user_id, $item_id);
$feed = Model\Feed\get_feed($user_id, $item['feed_id']);
$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';
switch ($menu) {
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;
case 'history':
$nav = Model\Item\get_nav($item, array('read'));
$nav = Model\Item\get_item_nav($user_id, $item, array('read'));
break;
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;
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;
}
$image_proxy = (bool) Model\Config\get('image_proxy');
$image_proxy = (bool) Helper\config('image_proxy');
// add the image proxy if requested and required
$item['content'] = Handler\Proxy\rewrite_html($item['content'], $item['url'], $image_proxy, $feed['cloak_referrer']);
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(
'nb_unread_items' => Model\Item\count_by_status('unread'),
'item' => $item,
'feed' => $feed,
'item_nav' => isset($nav) ? $nav : null,
@ -107,27 +79,27 @@ Router\get_action('show', function () {
// Display feed items page
Router\get_action('feed-items', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$feed_id = Request\int_param('feed_id', 0);
$offset = Request\int_param('offset', 0);
$nb_items = Model\ItemFeed\count_items($feed_id);
$feed = Model\Feed\get($feed_id);
$feed = Model\Feed\get_feed($user_id, $feed_id);
$order = Request\param('order', 'updated');
$direction = Request\param('direction', Model\Config\get('items_sorting_direction'));
$items = Model\ItemFeed\get_all_items($feed_id, $offset, Model\Config\get('items_per_page'), $order, $direction);
$direction = Request\param('direction', Helper\config('items_sorting_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(
'favicons' => Model\Favicon\get_favicons(array($feed['id'])),
'original_marks_read' => Model\Config\get('original_marks_read'),
'favicons' => Model\Favicon\get_favicons_by_feed_ids(array($feed['id'])),
'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'),
'feed' => $feed,
'items' => $items,
'nb_items' => $nb_items,
'nb_unread_items' => Model\Item\count_by_status('unread'),
'offset' => $offset,
'items_per_page' => Model\Config\get('items_per_page'),
'item_title_link' => Model\Config\get('item_title_link'),
'items_per_page' => Helper\config('items_per_page'),
'item_title_link' => Helper\config('item_title_link'),
'menu' => 'feed-items',
'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)
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);
$feed = Model\Feed\get($item['feed_id']);
$item = Model\Item\get_item($user_id, $item_id);
$feed = Model\Feed\get_feed($user_id, $item['feed_id']);
$download = Model\Item\download_contents($id);
$download['content'] = Handler\Proxy\rewrite_html($download['content'], $item['url'], Model\Config\get('image_proxy'), $feed['cloak_referrer']);
$download = Handler\Item\download_item_content($user_id, $item_id);
$download['content'] = Handler\Proxy\rewrite_html(
$download['content'],
$item['url'],
Helper\bool_config('image_proxy'),
(bool) $feed['cloak_referrer']
);
Response\json($download);
});
// Ajax call to mark item read
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'));
});
// Ajax call to mark item as removed
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'));
});
// Ajax call to mark item unread
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'));
});
// Mark unread items as read
Router\get_action('mark-all-read', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$group_id = Request\int_param('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 {
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');
@ -179,9 +164,11 @@ Router\get_action('mark-all-read', function () {
// Mark all unread items as read for a specific feed
Router\get_action('mark-feed-as-read', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$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);
});
@ -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
// on page 2+ is unknown.
Router\post_action('mark-feed-as-read', function () {
Model\ItemFeed\mark_all_as_read(Request\int_param('feed_id'));
$nb_items = Model\Item\count_by_status('unread');
$user_id = SessionStorage::getInstance()->getUserId();
$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);
});
// Mark item as read and redirect to the listing page
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');
$offset = Request\int_param('offset', 0);
$feed_id = Request\int_param('feed_id', 0);
Model\Item\set_read($id);
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$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-'.$item_id);
});
// Mark item as unread and redirect to the listing page
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');
$offset = Request\int_param('offset', 0);
$feed_id = Request\int_param('feed_id', 0);
Model\Item\set_unread($id);
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$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-'.$item_id);
});
// Mark item as removed and redirect to the listing page
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');
$offset = Request\int_param('offset', 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);
});
Router\post_action('latest-feeds-items', function () {
$items = Model\Item\get_latest_feeds_items();
$nb_unread_items = Model\Item\count_by_status('unread');
$user_id = SessionStorage::getInstance()->getUserId();
$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) {
$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
namespace Miniflux\Controller;
use Miniflux\Helper;
use Miniflux\Router;
use Miniflux\Response;
use Miniflux\Request;
use Miniflux\Session\SessionStorage;
use Miniflux\Template;
use Miniflux\Helper;
use Miniflux\Model;
// Display search results page
Router\get_action('search', function() {
$user_id = SessionStorage::getInstance()->getUserId();
$text = Request\param('text', '');
$offset = Request\int_param('offset', 0);
$items = array();
$nb_items = 0;
if ($text) {
$items = Model\Search\get_all_items($text, $offset, Model\Config\get('items_per_page'));
$nb_items = Model\Search\count_items($text);
$items = Model\ItemSearch\get_all_items($user_id, $text, $offset, Helper\config('items_per_page'));
$nb_items = Model\ItemSearch\count_items($user_id, $text);
}
Response\html(Template\layout('search', array(
'favicons' => Model\Favicon\get_item_favicons($items),
'original_marks_read' => Model\Config\get('original_marks_read'),
'favicons' => Model\Favicon\get_items_favicons($items),
'original_marks_read' => Helper\config('original_marks_read'),
'text' => $text,
'items' => $items,
'order' => '',
'direction' => '',
'display_mode' => Model\Config\get('items_display_mode'),
'item_title_link' => Model\Config\get('item_title_link'),
'display_mode' => Helper\config('items_display_mode'),
'item_title_link' => Helper\config('item_title_link'),
'group_id' => array(),
'nb_items' => $nb_items,
'nb_unread_items' => Model\Item\count_by_status('unread'),
'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'),
'menu' => 'search',
'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,9 +2,13 @@
namespace Miniflux\Session;
use Miniflux\Helper;
class SessionManager
{
const SESSION_LIFETIME = 2678400;
function open($base_path = '/', $save_path = '', $session_lifetime = SESSION_LIFETIME)
public static function open($base_path = '/', $save_path = '', $duration = self::SESSION_LIFETIME)
{
if ($save_path !== '') {
session_save_path($save_path);
@ -12,10 +16,10 @@ function open($base_path = '/', $save_path = '', $session_lifetime = SESSION_LIF
// HttpOnly and secure flags for session cookie
session_set_cookie_params(
$session_lifetime,
$duration,
$base_path ?: '/',
null,
isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
Helper\is_secure_connection(),
true
);
@ -28,7 +32,7 @@ function open($base_path = '/', $save_path = '', $session_lifetime = SESSION_LIF
ini_set('session.hash_bits_per_character', 6);
// Custom session name
session_name('__$');
session_name('MX_SID');
session_start();
@ -39,17 +43,114 @@ function open($base_path = '/', $save_path = '', $session_lifetime = SESSION_LIF
}
}
function close()
public static function close()
{
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;
return $this;
}
function flash_error($message)
public function setFlashErrorMessage($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;
use Miniflux\Model;
const PATH = 'app/templates/';
// Template\load('template_name', ['bla' => 'value']);
@ -30,5 +32,8 @@ function load()
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());
}
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;
use Miniflux\Model\Feed;
use Miniflux\Model\Group;
use Miniflux\Model;
use PicoDb\Database;
use PicoFeed\Serialization\Subscription;
use PicoFeed\Serialization\SubscriptionList;
use PicoFeed\Serialization\SubscriptionListBuilder;
use PicoFeed\Serialization\SubscriptionListParser;
function export_all_feeds()
function export_all_feeds($user_id)
{
$feeds = Feed\get_all();
$feeds = Model\Feed\get_feeds($user_id);
$subscriptionList = SubscriptionList::create()->setTitle(t('Subscriptions'));
foreach ($feeds as $feed) {
$groups = Group\get_feed_groups($feed['id']);
$groups = Model\Group\get_feed_groups($feed['id']);
$category = '';
if (!empty($groups)) {
@ -32,3 +32,36 @@ function export_all_feeds()
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;
use Miniflux\Helper;
use Miniflux\Model\Config;
use PicoFeed\Client\ClientException;
use PicoFeed\Config\Config as PicoFeedConfig;
use PicoFeed\Filter\Filter;
@ -51,15 +50,14 @@ function rewrite_html($html, $website, $proxy_images, $cloak_referrer)
function download($url)
{
try {
if ((bool) Config\get('debug_mode')) {
if (DEBUG_MODE) {
Logger::enable();
}
$client = Client::getInstance();
$client->setUserAgent(Config\HTTP_USER_AGENT);
$client->setUserAgent(HTTP_USER_AGENT);
$client->enablePassthroughMode();
$client->execute($url);
} catch (ClientException $e) {}
Config\write_debug();
} catch (ClientException $e) {
}
}

View File

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

View File

@ -2,24 +2,24 @@
namespace Miniflux\Handler\Service;
use Miniflux\Model;
use Miniflux\Helper;
use PicoFeed\Client\Client;
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);
}
if ((bool) Config\get('instapaper_enabled')) {
if (Helper\bool_config('instapaper_enabled')) {
instapaper_sync($item);
}
if ((bool) Config\get('wallabag_enabled')) {
if (Helper\bool_config('wallabag_enabled')) {
wallabag_sync($item);
}
}
@ -27,8 +27,8 @@ function sync($item_id)
function instapaper_sync(array $item)
{
$params = array(
'username' => Config\get('instapaper_username'),
'password' => Config\get('instapaper_password'),
'username' => Helper\config('instapaper_username'),
'password' => Helper\config('instapaper_password'),
'url' => $item['url'],
'title' => $item['title'],
);
@ -47,11 +47,11 @@ function instapaper_sync(array $item)
function pinboard_sync(array $item)
{
$params = array(
'auth_token' => Config\get('pinboard_token'),
'auth_token' => Helper\config('pinboard_token'),
'format' => 'json',
'url' => $item['url'],
'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);
@ -79,7 +79,7 @@ function wallabag_has_url($url)
if ($token === 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);
$response = api_get_call($apiUrl, $headers);
if ($response !== false) {
@ -94,7 +94,7 @@ function wallabag_add_item($url, $title)
if ($token === 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);
$data = array(
'url' => $url,
@ -112,13 +112,13 @@ function wallabag_get_access_token()
if (!empty($_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(
'grant_type' => 'password',
'client_id' => Config\get('wallabag_client_id'),
'client_secret' => Config\get('wallabag_client_secret'),
'username' => Config\get('wallabag_username'),
'password' => Config\get('wallabag_password')
'client_id' => Helper\config('wallabag_client_id'),
'client_secret' => Helper\config('wallabag_client_secret'),
'username' => Helper\config('wallabag_username'),
'password' => Helper\config('wallabag_password')
);
$response = api_post_call($url, $data);
if ($response !== false) {
@ -135,7 +135,7 @@ function api_get_call($url, array $headers = array())
{
try {
$client = Client::getInstance();
$client->setUserAgent(Config\HTTP_USER_AGENT);
$client->setUserAgent(HTTP_USER_AGENT);
if ($headers) {
$client->setHeaders($headers);
}

View File

@ -2,6 +2,8 @@
namespace Miniflux\Helper;
use PicoFeed\Logging\Logger;
function escape($value)
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
@ -43,7 +45,11 @@ function get_current_base_url()
{
$url = is_secure_connection() ? 'https://' : 'http://';
$url .= $_SERVER['HTTP_HOST'];
if (strpos($_SERVER['HTTP_HOST'], ':') === false) {
$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'])).'/' : '/';
return $url;
@ -53,3 +59,9 @@ function is_secure_connection()
{
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)
{
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 '';

View File

@ -2,7 +2,17 @@
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)
{
@ -16,14 +26,18 @@ function flash($type, $html)
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()
{
$theme = Config\get('theme');
$theme = config('theme');
if ($theme !== 'original') {
$css_file = THEME_DIRECTORY.'/'.$theme.'/css/app.css';

View File

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

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'URL automatické aktualizace',
'Update Miniflux' => 'Aktualizovat Miniflux',
'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',
'The name must have only alpha-numeric characters' => 'Jméno smí obsahovat pouze písmena a číslice',
'New database' => 'Nová databáze',
@ -200,7 +199,6 @@ return array(
'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í?',
'database' => 'databáze',
'Console' => 'konzole',
'Miniflux API' => 'Miniflux API',
'menu' => 'nabídka',
'Default' => 'Výchozí',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'Auto-Update URL',
'Update Miniflux' => 'Miniflux aktualisieren',
'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',
'The name must have only alpha-numeric characters' => 'Der Name darf nur alphanumerische Zeichen enthalten',
'New database' => 'Neue Datenbank',
@ -200,7 +199,6 @@ return array(
'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?',
'database' => 'Datenbank',
'Console' => 'Konsole',
'Miniflux API' => 'Miniflux API',
'menu' => 'Menü',
'Default' => 'Standard',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'Actualizar automáticamente la URL',
'Update Miniflux' => 'Actualizar Miniflux',
'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',
'The name must have only alpha-numeric characters' => 'El nombre sólo puede contener caractéres alfanuméricos',
'New database' => 'Nueva base de datos',
@ -200,7 +199,6 @@ return array(
'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?',
'database' => 'base de datos',
'Console' => 'Consola',
'Miniflux API' => 'API Miniflux',
'menu' => 'menú',
'Default' => 'Por defecto',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'URL de mise à jour automatique',
'Update Miniflux' => 'Mettre à jour Miniflux',
'Miniflux is updated!' => 'Miniflux a été mis à jour avec succès !',
'Unable to update Miniflux, check the console for errors.' => 'Impossible de mettre à jour Miniflux, allez-voir les erreurs dans la console.',
'Don\'t forget to backup your database' => 'N\'oubliez pas de sauvegarder votre base de données',
'The name must have only alpha-numeric characters' => 'Le nom doit avoir seulement des caractères alphanumériques',
'New database' => 'Nouvelle base de données',
@ -200,7 +199,6 @@ return array(
'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 ?',
'database' => 'base de données',
'Console' => 'Console',
'Miniflux API' => 'Miniflux API',
'menu' => 'menu',
'Default' => 'Défaut',

View File

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

View File

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

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'URL de atualização automática',
'Update Miniflux' => 'Atualizar Miniflux',
'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',
'The name must have only alpha-numeric characters' => 'O nome deve conter apenas caracteres alfa-numéricos',
'New database' => 'Novo banco de dados',
@ -200,7 +199,6 @@ return array(
'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?',
'database' => 'banco de dados',
'Console' => 'Console',
'Miniflux API' => 'API do Miniflux',
'menu' => 'menu',
'Default' => 'Padrão',

View File

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

View File

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

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'URL za automatsko ažuriranje',
'Update Miniflux' => 'Ažuriraj Miniflux',
'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',
'The name must have only alpha-numeric characters' => 'Ime može sadržati samo brojeve ili slova',
'New database' => 'Nova baza podataka',
@ -200,7 +199,6 @@ return array(
'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?',
'database' => 'baza podataka',
'Console' => 'Konzola',
'Miniflux API' => 'API Minifluxa',
'menu' => 'meni',
'Default' => 'Osnovna',

View File

@ -157,7 +157,6 @@ return array(
'Auto-Update URL' => 'Otomatik güncelleme bağlantısı',
'Update Miniflux' => 'Miniflux\'ı Güncelle',
'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',
'The name must have only alpha-numeric characters' => 'İsim yalnızca alfanümerik karakterler içermeli',
'New database' => 'Yeni veritabanı',
@ -200,7 +199,6 @@ return array(
'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?',
'database' => 'veritabanı',
'Console' => 'Konsol',
'Miniflux API' => 'Miniflux API',
'menu' => 'menü',
'Default' => 'Varsayılan',

View File

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

View File

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

View File

@ -2,60 +2,68 @@
namespace Miniflux\Model\Bookmark;
use Miniflux\Helper;
use Miniflux\Model;
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')
->table('items')
->table(Model\Item\TABLE)
->eq('bookmark', 1)
->eq('user_id', $user_id)
->in('feed_id', $feed_ids)
->in('status', array('read', 'unread'))
->in('status', array(Model\Item\STATUS_READ, Model\Item\STATUS_UNREAD))
->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')
->table('items')
->table(Model\Item\TABLE)
->columns(
'items.id',
'items.checksum',
'items.title',
'items.updated',
'items.url',
'items.enclosure',
'items.enclosure_url',
'items.enclosure_type',
'items.bookmark',
'items.status',
'items.content',
'items.feed_id',
'items.language',
'items.rtl',
'items.author',
'feeds.site_url',
'feeds.title AS feed_title',
'feeds.rtl'
'feeds.title AS feed_title'
)
->join('feeds', 'id', 'feed_id')
->in('feed_id', $feed_ids)
->in('status', array('read', 'unread'))
->eq('bookmark', 1)
->orderBy('updated', Config\get('items_sorting_direction'))
->join(Model\Feed\TABLE, 'id', 'feed_id')
->eq('items.user_id', $user_id)
->in('items.feed_id', $feed_ids)
->neq('items.status', Model\Item\STATUS_REMOVED)
->eq('items.bookmark', 1)
->orderBy('items.updated', Helper\config('items_sorting_direction'))
->offset($offset)
->limit($limit)
->findAll();
}
function set_flag($id, $value)
function get_bookmarked_item_ids($user_id)
{
if ($value == 1) {
Service\sync($id);
return Database::getInstance('db')
->table(Model\Item\TABLE)
->eq('user_id', $user_id)
->eq('bookmark', 1)
->findAllByColumn('id');
}
function set_flag($user_id, $item_id, $value)
{
return Database::getInstance('db')
->table('items')
->eq('id', $id)
->in('status', array('read', 'unread'))
->save(array('bookmark' => $value));
->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;
use Miniflux\Helper;
use Miniflux\Translator;
use Miniflux\Model;
use DirectoryIterator;
use Miniflux\Session\SessionStorage;
use PicoDb\Database;
use PicoFeed\Config\Config as ReaderConfig;
use PicoFeed\Logging\Logger;
const HTTP_USER_AGENT = 'Miniflux (https://miniflux.net)';
// 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;
}
const TABLE = 'user_settings';
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()
{
$timezones = timezone_identifiers_list();
return array_combine(array_values($timezones), $timezones);
}
// Returns true if the language is RTL
function is_language_rtl()
{
$languages = array(
'ar_AR'
);
return in_array(get('language'), $languages);
return in_array(Helper\config('language'), $languages);
}
// Get all supported languages
function get_languages()
{
return array(
@ -109,7 +57,6 @@ function get_languages()
);
}
// Get all skins
function get_themes()
{
$themes = array(
@ -129,7 +76,6 @@ function get_themes()
return $themes;
}
// Sorting direction choices for items
function get_sorting_directions()
{
return array(
@ -138,26 +84,23 @@ function get_sorting_directions()
);
}
// Display summaries or full contents on lists
function get_display_mode()
{
return array(
'titles' => t('Titles'),
'summaries' => t('Summaries'),
'full' => t('Full contents')
'full' => t('Full contents'),
);
}
// Item title links to original or full contents
function get_item_title_link()
{
return array(
'original' => t('Original'),
'full' => t('Full contents')
'full' => t('Full contents'),
);
}
// Autoflush choices for read items
function get_autoflush_read_options()
{
return array(
@ -166,11 +109,10 @@ function get_autoflush_read_options()
'1' => t('After %d day', 1),
'5' => t('After %d day', 5),
'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()
{
return array(
@ -182,7 +124,6 @@ function get_autoflush_unread_options()
);
}
// Number of items per pages
function get_paging_options()
{
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()
{
return array(
@ -207,74 +147,87 @@ function get_nothing_to_read_redirections()
);
}
// Regenerate tokens for the API and bookmark feed
function new_tokens()
function get_default_values()
{
$values = array(
'api_token' => Helper\generate_token(),
'feed_token' => Helper\generate_token(),
'bookmarklet_token' => Helper\generate_token(),
'fever_token' => substr(Helper\generate_token(), 0, 8),
return array(
'language' => 'en_US',
'timezone' => 'UTC',
'theme' => 'original',
'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($name)
function get_all($user_id)
{
if (! isset($_SESSION)) {
return current(Database::getInstance('db')->hashtable('settings')->get($name));
} else {
if (! isset($_SESSION['config'][$name])) {
$_SESSION['config'] = get_all();
$settings = Database::getInstance('db')
->hashtable(TABLE)
->eq('user_id', $user_id)
->getAll('key', 'value');
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 $_SESSION['config'][$name];
}
return $settings;
}
return null;
}
// Get all config parameters
function get_all()
function save_defaults($user_id)
{
$config = Database::getInstance('db')->hashtable('settings')->get();
unset($config['password']);
return $config;
return save($user_id, get_default_values());
}
// Save config into the database and update the session
function save(array $values)
function save($user_id, array $values)
{
// Update the password if needed
if (! empty($values['password'])) {
$values['password'] = password_hash($values['password'], PASSWORD_BCRYPT);
} else {
unset($values['password']);
}
$db = Database::getInstance('db');
$results = array();
$db->startTransaction();
unset($values['confirmation']);
// If the user does not want content of feeds, remove it in previous ones
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)) {
reload();
return true;
foreach ($values as $key => $value) {
if ($db->table(TABLE)->eq('user_id', $user_id)->eq('key', $key)->exists()) {
$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;
}
// Reload the cache in session
function reload()
{
$_SESSION['config'] = get_all();
Translator\load(get('language'));
$db->closeTransaction();
SessionStorage::getInstance()->flushConfig();
return true;
}

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

View File

@ -2,251 +2,65 @@
namespace Miniflux\Model\Feed;
use UnexpectedValueException;
use Miniflux\Model\Config;
use Miniflux\Model\Item;
use Miniflux\Model\Group;
use Miniflux\Model\Favicon;
use Miniflux\Helper;
use PicoDb\Database;
use PicoFeed\Reader\Reader;
use PicoFeed\PicoFeedException;
use PicoFeed\Serialization\SubscriptionListParser;
use PicoFeed\Parser\Feed;
const LIMIT_ALL = -1;
const STATUS_ACTIVE = 1;
const STATUS_INACTIVE = 0;
const TABLE = 'feeds';
// Update feed information
function update(array $values)
function create($user_id, Feed $feed, $etag, $last_modified, $rtl = false, $scraper = false, $cloak_referrer = false)
{
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');
// Discover the feed
$reader = new Reader(Config\get_reader_config());
$resource = $reader->discover($url);
// Feed already there
if ($db->table('feeds')->eq('feed_url', $resource->getUrl())->count()) {
throw new UnexpectedValueException;
if ($db->table('feeds')->eq('user_id', $user_id)->eq('feed_url', $feed->getFeedUrl())->exists()) {
return -1;
}
// Parse the feed
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
if ($enable_grabber) {
$parser->enableContentGrabber();
}
$feed = $parser->execute();
// Save the feed
$result = $db->table('feeds')->save(array(
$feed_id = $db
->table(TABLE)
->persist(array(
'user_id' => $user_id,
'title' => $feed->getTitle(),
'site_url' => $feed->getSiteUrl(),
'feed_url' => $feed->getFeedUrl(),
'download_content' => $enable_grabber ? 1 : 0,
'rtl' => $force_rtl ? 1 : 0,
'last_modified' => $resource->getLastModified(),
'download_content' => $scraper ? 1 : 0,
'rtl' => $rtl ? 1 : 0,
'etag' => $etag,
'last_modified' => $last_modified,
'last_checked' => time(),
'etag' => $resource->getEtag(),
'cloak_referrer' => $cloak_referrer ? 1 : 0,
));
if ($result) {
$feed_id = $db->getLastId();
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());
if ($feed_id !== false) {
Item\update_feed_items($user_id, $feed_id, $feed->getItems(), $rtl);
}
return $feed_id;
}
// Refresh all feeds
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()
function get_feeds($user_id)
{
return Database::getInstance('db')
->table('feeds')
->eq('parsing_error', '1')
->count();
}
// Get all feeds
function get_all()
{
return Database::getInstance('db')
->table('feeds')
->table(TABLE)
->eq('user_id', $user_id)
->asc('title')
->findAll();
}
// Get all feeds with the number unread/total items in the order failed, working, disabled
function get_all_item_counts()
function get_feeds_with_items_count($user_id)
{
return Database::getInstance('db')
->table('feeds')
->table(TABLE)
->columns(
'feeds.*',
'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"'
)
->join('items', 'feed_id', 'id')
->eq('feeds.user_id', $user_id)
->groupBy('feeds.id')
->desc('feeds.parsing_error')
->desc('feeds.enabled')
@ -254,98 +68,94 @@ function get_all_item_counts()
->findAll();
}
// Get unread/total count for one feed
function count_items($feed_id)
function get_feed_ids($user_id, $limit = null)
{
$counts = Database::getInstance('db')
->table('items')
->columns('status', 'count(*) as item_count')
->in('status', array('read', 'unread'))
->eq('feed_id', $feed_id)
->groupBy('status')
->findAll();
$query = Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->eq('enabled', STATUS_ACTIVE)
->asc('last_checked')
->asc('id');
$result = array(
'items_unread' => 0,
'items_total' => 0,
);
foreach ($counts as &$count) {
if ($count['status'] === 'unread') {
$result['items_unread'] = (int) $count['item_count'];
if ($limit !== null) {
$query->limit($limit);
}
$result['items_total'] += $count['item_count'];
return $query->findAllByColumn('id');
}
return $result;
}
// Get one feed
function get($feed_id)
function get_feed($user_id, $feed_id)
{
return Database::getInstance('db')
->table('feeds')
->table(TABLE)
->eq('user_id', $user_id)
->eq('id', $feed_id)
->findOne();
}
// Update parsing error column
function update_parsing_error($feed_id, $value)
function update_feed($user_id, $feed_id, array $values)
{
Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save(array('parsing_error' => $value));
}
$db = Database::getInstance('db');
$db->startTransaction();
// Update last check date
function update_last_checked($feed_id)
{
Database::getInstance('db')
$feed = $values;
unset($feed['id']);
unset($feed['group_name']);
unset($feed['feed_group_ids']);
$result = Database::getInstance('db')
->table('feeds')
->eq('user_id', $user_id)
->eq('id', $feed_id)
->save(array(
'last_checked' => time()
));
->save($feed);
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;
}
// Update Etag and last Modified columns
function update_cache($feed_id, $last_modified, $etag)
$db->closeTransaction();
return true;
}
$db->cancelTransaction();
return false;
}
function change_feed_status($user_id, $feed_id, $status = STATUS_ACTIVE)
{
Database::getInstance('db')
->table('feeds')
return Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->eq('id', $feed_id)
->save(array(
'last_modified' => $last_modified,
'etag' => $etag
));
->save((array('enabled' => $status)));
}
// Remove one feed
function remove($feed_id)
function remove_feed($user_id, $feed_id)
{
Group\remove_all($feed_id);
// Items are removed by a sql constraint
$result = Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->remove();
Favicon\purge_favicons();
return $result;
return Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->eq('id', $feed_id)
->remove();
}
// Remove all feeds
function remove_all()
function count_failed_feeds($user_id)
{
$result = Database::getInstance('db')->table('feeds')->remove();
Favicon\purge_favicons();
return $result;
return Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
->eq('parsing_error', 1)
->count();
}
// Enable a feed (activate refresh)
function enable($feed_id)
function count_feeds($user_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;
/**
* Get all groups
*
* @return array
*/
function get_all()
const TABLE = 'groups';
const JOIN_TABLE = 'feeds_groups';
function get_all($user_id)
{
return Database::getInstance('db')
->table('groups')
->table(TABLE)
->eq('user_id', $user_id)
->orderBy('title')
->findAll();
}
/**
* Get assoc array of group ids with assigned feeds ids
*
* @return array
*/
function get_map()
function get_groups_feed_ids($user_id)
{
$result = Database::getInstance('db')
->table('feeds_groups')
$result = array();
$rows = Database::getInstance('db')
->table(JOIN_TABLE)
->columns('feed_id', 'group_id')
->join(TABLE, 'id', 'group_id')
->eq('user_id', $user_id)
->findAll();
// TODO: add PDO::FETCH_COLUMN|PDO::FETCH_GROUP to picodb and use it instead
// of the following lines
$map = array();
foreach ($result as $row) {
foreach ($rows as $row) {
$group_id = $row['group_id'];
$feed_id = $row['feed_id'];
if (isset($map[$group_id])) {
$map[$group_id][] = $feed_id;
if (isset($result[$group_id])) {
$result[$group_id][] = $feed_id;
} 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)
{
return Database::getInstance('db')
->table('groups')
->join('feeds_groups', 'group_id', 'id')
->table(TABLE)
->join(JOIN_TABLE, 'group_id', 'id')
->eq('feed_id', $feed_id)
->findAllByColumn('id');
}
@ -82,84 +52,77 @@ function get_feed_group_ids($feed_id)
function get_feed_groups($feed_id)
{
return Database::getInstance('db')
->table('groups')
->table(TABLE)
->columns('groups.id', 'groups.title')
->join('feeds_groups', 'group_id', 'id')
->join(JOIN_TABLE, 'group_id', 'id')
->eq('feed_id', $feed_id)
->findAll();
}
/**
* 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)
function get_group_id_from_title($user_id, $title)
{
return Database::getInstance('db')
->table('groups')
->eq('user_id', $user_id)
->eq('title', $title)
->findOneColumn('id');
}
/**
* Get all feed ids assigned to a group
*
* @param integer $group_id
* @return array
*/
function get_feeds_by_group($group_id)
function get_feed_ids_by_group($group_id)
{
return Database::getInstance('db')
->table('feeds_groups')
->table(JOIN_TABLE)
->eq('group_id', $group_id)
->findAllByColumn('feed_id');
}
/**
* 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)
function create_group($user_id, $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) {
Database::getInstance('db')
->table('groups')
->insert($data);
$group_id = get_group_id($title);
$group_id = Database::getInstance('db')
->table(TABLE)
->persist(array('title' => $title, 'user_id' => $user_id));
}
return $group_id;
}
/**
* Add groups to feed
*
* @param integer $feed_id feed id
* @param array $group_ids array of group ids
* @return boolean true on success, false on error
*/
function add($feed_id, array $group_ids)
function update_feed_groups($user_id, $feed_id, array $group_ids, $group_name = '')
{
if ($group_name !== '') {
$group_id = create_group($user_id, $group_name);
if ($group_id === false) {
return false;
}
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) {
$data = array('feed_id' => $feed_id, 'group_id' => $group_id);
$result = Database::getInstance('db')
->table('feeds_groups')
->insert($data);
->table(JOIN_TABLE)
->insert(array('feed_id' => $feed_id, 'group_id' => $group_id));
if ($result === false) {
return false;
@ -169,104 +132,15 @@ function add($feed_id, array $group_ids)
return true;
}
/**
* 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)
function dissociate_feed_groups($feed_id, array $group_ids)
{
$result = Database::getInstance('db')
->table('feeds_groups')
if (empty($group_ids)) {
return false;
}
return Database::getInstance('db')
->table(JOIN_TABLE)
->eq('feed_id', $feed_id)
->in('group_id', $group_ids)
->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;
use PicoDb\Database;
use PicoFeed\Logging\Logger;
use Miniflux\Handler\Service;
use Miniflux\Model\Config;
use Miniflux\Model\Feed;
use Miniflux\Model\Group;
use Miniflux\Handler;
use Miniflux\Helper;
use PicoFeed\Parser\Parser;
// Get all items without filtering
function get_all()
const TABLE = 'items';
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')
->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'))
->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)
->eq('user_id', $user_id)
->eq('id', $item_id)
->findOne();
}
// Get item naviguation (next/prev items)
function get_nav($item, $status = array('unread'), $bookmark = array(1, 0), $feed_id = null, $group_id = null)
function get_item_nav($user_id, array $item, $status = array(STATUS_UNREAD), $bookmark = array(1, 0), $feed_id = null, $group_id = null)
{
$query = Database::getInstance('db')
->table('items')
->table(TABLE)
->columns('id', 'status', 'title', 'bookmark')
->neq('status', 'removed')
->orderBy('updated', Config\get('items_sorting_direction'));
->neq('status', STATUS_REMOVED)
->eq('user_id', $user_id)
->orderBy('updated', Helper\config('items_sorting_direction'))
->desc('id')
;
if ($feed_id) {
$query->eq('feed_id', $feed_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();
@ -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 set_removed($id)
function get_items_by_status($user_id, $status, $feed_ids = array(), $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc')
{
return Database::getInstance('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'removed', 'content' => ''));
->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)
->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 set_read($id)
function get_items($user_id, $since_id = null, array $item_ids = array(), $limit = 50)
{
$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')
->table('items')
->eq('id', $id)
->save(array('status' => 'read'));
}
// 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')
->eq('user_id', $user_id)
->eq('status', $status)
->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 download_contents($item_id)
function get_latest_feeds_items($user_id)
{
$item = get($item_id);
$content = Handler\Scraper\download_contents($item['url']);
return Database::getInstance('db')
->table(Feed\TABLE)
->columns(
'feeds.id',
'MAX(items.updated) as updated',
'items.status'
)
->join(TABLE, 'feed_id', 'id')
->eq('feeds.user_id', $user_id)
->groupBy('feeds.id')
->orderBy('feeds.id')
->findAll();
}
if (! empty($content)) {
if (! Config\get('nocontent')) {
Database::getInstance('db')
function count_by_status($user_id, $status, $feed_ids = array())
{
$query = Database::getInstance('db')
->table('items')
->eq('id', $item['id'])
->save(array('content' => $content));
->eq('user_id', $user_id)
->in('feed_id', $feed_ids);
if (is_array($status)) {
$query->in('status', $status);
} else {
$query->eq('status', $status);
}
Config\write_debug();
return array(
'result' => true,
'content' => $content
);
return $query->count();
}
Config\write_debug();
function autoflush_read($user_id)
{
$autoflush = Helper\int_config('autoflush');
return array(
'result' => false,
'content' => ''
);
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;
use Miniflux\Model\Feed;
use Miniflux\Model\Item;
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')
->table('items')
->table(Item\TABLE)
->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();
}
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')
->table('items')
@ -22,31 +52,39 @@ function get_all_items($feed_id, $offset = null, $limit = null, $order_column =
'items.title',
'items.updated',
'items.url',
'items.enclosure',
'items.enclosure_url',
'items.enclosure_type',
'items.feed_id',
'items.status',
'items.content',
'items.bookmark',
'items.language',
'items.rtl',
'items.author',
'feeds.site_url',
'feeds.rtl'
'feeds.title AS feed_title'
)
->join('feeds', 'id', 'feed_id')
->in('status', array('unread', 'read'))
->eq('feed_id', $feed_id)
->join(Feed\TABLE, 'id', 'feed_id')
->in('status', array(Item\STATUS_UNREAD, Item\STATUS_READ))
->eq('items.feed_id', $feed_id)
->eq('items.user_id', $user_id)
->orderBy($order_column, $order_direction)
->offset($offset)
->limit($limit)
->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')
->table('items')
->eq('status', 'unread')
$query = Database::getInstance('db')
->table(Item\TABLE)
->eq('status', $current_status)
->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;
use PicoDb\Database;
use Miniflux\Model\Item;
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')
->table('items')
->eq('status', 'unread')
->in('feed_id', $feed_ids)
->update(array('status' => 'read'));
if (empty($feed_ids)) {
return false;
}
function mark_all_as_removed($group_id)
{
$feed_ids = Group\get_feeds_by_group($group_id);
$query = Database::getInstance('db')
->table(Item\TABLE)
->eq('user_id', $user_id)
->eq('status', $current_status)
->in('feed_id', $feed_ids);
return Database::getInstance('db')
->table('items')
->eq('status', 'read')
->eq('bookmark', 0)
->in('feed_id', $feed_ids)
->save(array('status' => 'removed', 'content' => ''));
if ($before !== null) {
$query->lte('updated', $before);
}
return $query->update(array('status' => $new_status));
}

View File

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

View File

@ -1,43 +1,47 @@
<?php
namespace Miniflux\Model\Search;
namespace Miniflux\Model\ItemSearch;
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')
->table('items')
->neq('status', 'removed')
->table(Item\TABLE)
->eq('user_id', $user_id)
->neq('status', Item\STATUS_REMOVED)
->ilike('title', '%' . $text . '%')
->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')
->table('items')
->table(Item\TABLE)
->columns(
'items.id',
'items.title',
'items.updated',
'items.url',
'items.enclosure',
'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',
'feeds.rtl'
'feeds.title AS feed_title'
)
->join('feeds', 'id', 'feed_id')
->neq('status', 'removed')
->join(Feed\TABLE, 'id', 'feed_id')
->eq('items.user_id', $user_id)
->neq('items.status', Item\STATUS_REMOVED)
->ilike('items.title', '%' . $text . '%')
->orderBy('updated', 'desc')
->orderBy('items.updated', 'desc')
->offset($offset)
->limit($limit)
->findAll();

View File

@ -3,37 +3,142 @@
namespace Miniflux\Model\User;
use PicoDb\Database;
use Miniflux\Session;
use Miniflux\Request;
use Miniflux\Model\Config;
use Miniflux\Model\RememberMe;
use Miniflux\Model\Database as DatabaseModel;
use Miniflux\Helper;
// Check if the user is logged in
function is_loggedin()
const TABLE = 'users';
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 logout()
function update_user($user_id, $username, $password = null, $is_admin = null)
{
RememberMe\destroy();
Session\close();
$user = get_user_by_id($user_id);
$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;
}
// Get the credentials from the current selected database
function get_credentials()
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;
}
function regenerate_tokens($user_id)
{
$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')
->hashtable('settings')
->get('username', 'password');
->table(TABLE)
->eq('id', $user_id)
->remove();
}
// Set last login date
function set_last_login()
function generate_fever_api_key($username)
{
$token = Helper\generate_token();
$api_key = md5($username . ':' . $token);
return array($token, $api_key);
}
function get_all_users()
{
return Database::getInstance('db')
->hashtable('settings')
->put(array('last_login' => time()));
->table(TABLE)
->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 Miniflux\Helper;
const VERSION = 44;
const VERSION = 1;
function version_44(PDO $pdo)
function version_1(PDO $pdo)
{
$pdo->exec('INSERT INTO settings ("key", "value") VALUES ("item_title_link", "full")');
}
function version_43(PDO $pdo)
{
$pdo->exec('DROP TABLE favicons');
$pdo->exec(
'CREATE TABLE favicons (
$pdo->exec('CREATE TABLE users (
id INTEGER PRIMARY KEY,
hash TEXT UNIQUE,
type TEXT
)'
);
username TEXT UNIQUE,
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('
CREATE TABLE "favicons_feeds" (
$pdo->exec('CREATE TABLE user_settings (
"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,
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
)
');
}
checksum TEXT NOT NULL,
status TEXT,
bookmark INTEGER DEFAULT 0,
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('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" (
$pdo->exec('CREATE TABLE "groups" (
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('
CREATE TABLE "feeds_groups" (
$pdo->exec('CREATE TABLE "feeds_groups" (
feed_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
PRIMARY KEY(feed_id, group_id),
FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
)
');
}
)');
function version_40(PDO $pdo)
{
$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 (
$pdo->exec('CREATE TABLE favicons (
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,
user_agent TEXT,
token TEXT,
sequence TEXT,
expiration INTEGER,
date_creation INTEGER
)'
);
}
date_creation INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)');
function version_24(PDO $pdo)
{
$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)
{
$fever_token = Helper\generate_token();
$rq = $pdo->prepare('
SELECT
items.id, items.url AS item_url, feeds.site_url
FROM items
LEFT JOIN feeds ON feeds.id=items.feed_id
INSERT INTO users
(username, password, is_admin, api_token, bookmarklet_token, cronjob_token, feed_token, fever_token, fever_api_key)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
');
$rq->execute();
$items = $rq->fetchAll(PDO::FETCH_ASSOC);
foreach ($items as $item) {
if ($item['id'] !== $item['item_url']) {
$id = hash('crc32b', $item['id'].$item['site_url']);
} else {
$id = hash('crc32b', $item['item_url'].$item['site_url']);
}
$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
)
');
$rq->execute(array(
'admin',
password_hash('admin', PASSWORD_BCRYPT),
'1',
Helper\generate_token(),
Helper\generate_token(),
Helper\generate_token(),
Helper\generate_token(),
$fever_token,
md5('admin:'.$fever_token),
));
}

View File

@ -3,9 +3,12 @@
<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><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=database"><?php echo t('database') ?></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>
</ul>
@ -16,24 +19,26 @@
<h3><?php echo t('Bookmarks') ?></h3>
<ul>
<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>
</ul>
</div>
<div class="panel panel-default">
<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') ?>)
<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)"/>
<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($user['bookmarklet_token']) ?>&amp;url='+encodeURIComponent(location.href)"/>
</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">
<h3><?php echo t('About') ?></h3>
<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('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>
</div>
</section>

View File

@ -11,7 +11,6 @@
</div>
<form method="post" action="?action=subscribe" autocomplete="off">
<?php echo Miniflux\Helper\form_hidden('csrf', $values) ?>
<?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>
<?php echo Miniflux\Helper\form_label(t('Groups'), 'groups'); ?>
<?php echo Miniflux\Helper\form_label(t('Groups'), 'group_name'); ?>
<div id="grouplist">
<?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 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 class="form-actions">

View File

@ -3,9 +3,12 @@
<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><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 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=about"><?php echo t('about') ?></a></li>
</ul>
@ -16,16 +19,16 @@
<h3 id="fever"><?php echo t('Fever API') ?></h3>
<ul>
<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 token:') ?> <strong><?php echo Miniflux\Helper\escape($config['fever_token']) ?></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($user['fever_token']) ?></strong></li>
</ul>
</div>
<div class="panel panel-default">
<h3 id="api"><?php echo t('Miniflux API') ?></h3>
<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 username:') ?> <strong><?php echo Miniflux\Helper\escape($config['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 username:') ?> <strong><?php echo Miniflux\Helper\escape($user['username']) ?></strong></li>
<li><?php echo t('API token:') ?> <strong><?php echo Miniflux\Helper\escape($user['api_token']) ?></strong></li>
</ul>
</div>
</section>

View File

@ -3,9 +3,12 @@
<nav>
<ul>
<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=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=about"><?php echo t('about') ?></a></li>
</ul>
@ -13,19 +16,7 @@
</div>
<section>
<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_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>
<div class="options">
@ -40,11 +31,9 @@
<?php if (ENABLE_AUTO_UPDATE): ?>
<?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 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) ?>
<div class="form-help"><?php echo t('Avoid mixed content warnings with HTTPS') ?></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><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>
<?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>
</div>
</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_label(t('Groups'), 'groups'); ?>
<?php echo Miniflux\Helper\form_label(t('Groups'), 'group_name'); ?>
<div id="grouplist">
<?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 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 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']): ?>
<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>
<?php endif; ?>

View File

@ -18,7 +18,7 @@
<?php else: ?>
<?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): ?>
<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 ?>

View File

@ -3,9 +3,12 @@
<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><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=database"><?php echo t('database') ?></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>
</ul>

View File

@ -6,7 +6,7 @@
data-item-bookmark="<?php echo $item['bookmark'] ?>"
<?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">
<a
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>
</li>
<?php endif ?>
<?php if ($item['enclosure']): ?>
<?php if ($item['enclosure_url']): ?>
<li>
<?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): ?>
<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): ?>
<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: ?>
<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 ?>
</li>
<?php endif ?>
@ -87,9 +87,9 @@
<?php echo Miniflux\Template\load('status_links', array('item' => $item, 'menu' => $menu, 'offset' => $offset)) ?>
</ul>
<?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'): ?>
<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: ?>
<p class="no-preview"></p>
<?php endif ?>

View File

@ -39,16 +39,6 @@
<?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">
<input type="submit" value="<?php echo t('Sign in') ?>" class="btn btn-blue"/>
</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>
<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><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><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=about"><?php echo t('about') ?></a></li>
</ul>

View File

@ -29,7 +29,7 @@
</nav>
<?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>
</h1>
@ -54,9 +54,9 @@
<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>
</li>
<?php if ($item['enclosure']): ?>
<?php if ($item['enclosure_url']): ?>
<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>
<?php endif ?>
<li class="hide-mobile">
@ -74,24 +74,24 @@
<?php endif; ?>
</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): ?>
<div id="item-content-enclosure">
<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>
</div>
<?php elseif (strpos($item['enclosure_type'], 'video') !== false): ?>
<div id="item-content-enclosure">
<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>
</div>
<?php elseif (strpos($item['enclosure_type'], 'image') !== false && $item['content'] === ''): ?>
<div id="item-content-enclosure">
<img src="<?php echo $item['enclosure'] ?>" alt="enclosure"/>
<img src="<?php echo $item['enclosure_url'] ?>" alt="enclosure"/>
</div>
<?php endif ?>
<?php endif ?>

View File

@ -1,5 +1,5 @@
<?php echo Miniflux\Template\load('search_form') ?>
<div class="page-header">
<h2><?php echo t('Unread') ?><span id="page-counter"><?php echo isset($nb_items) ? $nb_items : '' ?></span></h2>
<?php if (!empty($groups)): ?>

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)
{
$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_unread', 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\Required('theme', t('Value required')),
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('favicons', 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);
return array(

View File

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

View File

@ -13,7 +13,7 @@
},
"require": {
"fguillot/simple-validator": "v1.0.0",
"fguillot/json-rpc": "v1.2.1",
"fguillot/json-rpc": "v1.2.3",
"fguillot/picodb": "v1.0.14 ",
"fguillot/picofeed": "v0.1.25",
"pda/pheanstalk": "v3.1.0",
@ -28,6 +28,7 @@
"files": [
"app/schemas/sqlite.php",
"app/helpers/app.php",
"app/helpers/config.php",
"app/helpers/csrf.php",
"app/helpers/favicon.php",
"app/helpers/form.php",
@ -40,6 +41,8 @@
"app/core/template.php",
"app/handlers/scraper.php",
"app/handlers/service.php",
"app/handlers/feed.php",
"app/handlers/item.php",
"app/handlers/opml.php",
"app/handlers/proxy.php",
"app/models/config.php",
@ -51,7 +54,6 @@
"app/models/item_group.php",
"app/models/bookmark.php",
"app/models/auto_update.php",
"app/models/database.php",
"app/models/remember_me.php",
"app/models/group.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)
define('DB_FILENAME', 'db.sqlite');
// ENABLE_MULTIPLE_DB => default value is true (multiple users support)
define('ENABLE_MULTIPLE_DB', true);
// Enable/disable debug mode
define('DEBUG_MODE', false);
// DEBUG_FILENAME => default is data/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
// Reduce this number on systems with limited processing power
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';
use Miniflux\Handler;
use Miniflux\Model;
if (php_sapi_name() === 'cli') {
@ -9,29 +10,30 @@ if (php_sapi_name() === 'cli') {
'limit::',
'call-interval::',
'update-interval::',
'database::',
));
} 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');
}
else {
$options = $_GET;
}
if (! empty($options['database'])) {
if (! Model\Database\select($options['database'])) {
die("Database ".$options['database']." not found\r\n");
}
}
$limit = get_cli_option('limit', $options);
$update_interval = get_cli_option('update-interval', $options);
$call_interval = get_cli_option('call-interval', $options);
$limit = ! empty($options['limit']) && ctype_digit($options['limit']) ? (int) $options['limit'] : Model\Feed\LIMIT_ALL;
$update_interval = ! empty($options['update-interval']) && ctype_digit($options['update-interval']) ? (int) $options['update-interval'] : null;
$call_interval = ! empty($options['call-interval']) && ctype_digit($options['call-interval']) ? (int) $options['call-interval'] : null;
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();
foreach (Model\User\get_all_users() as $user) {
if ($update_interval !== null && $call_interval !== null && $limit === null && $update_interval >= $call_interval) {
$feeds_count = Model\Feed\count_feeds($user['id']);
$limit = ceil($feeds_count / ($update_interval / $call_interval));
}
Model\Feed\refresh_all($limit);
Model\Item\autoflush_read();
Model\Item\autoflush_unread();
Model\Config\write_debug();
Handler\Feed\update_feeds($user['id'], $limit);
Model\Item\autoflush_read($user['id']);
Model\Item\autoflush_unread($user['id']);
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\Model;
use Miniflux\Model\Feed;
use Miniflux\Model\Group;
use PicoDb\Database;
register_shutdown_function(function () {
Miniflux\Helper\write_debug_file();
});
// Route handler
function route($name, Closure $callback = null)
@ -31,37 +32,27 @@ function response(array $response)
// Fever authentication
function auth()
{
if (! empty($_GET['database'])) {
// Return unauthorized if the requested database could not be found
if (! Model\Database\select($_GET['database'])) {
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']);
$api_key = isset($_POST['api_key']) && ctype_alnum($_POST['api_key']) ? $_POST['api_key'] : null;
$user = Model\User\get_user_by_token('fever_api_key', $api_key);
$authenticated = $user !== null;
$response = array(
'api_version' => 3,
'auth' => (int) (isset($_POST['api_key']) && (strcasecmp($_POST['api_key'], $api_key) === 0)),
'auth' => (int) $authenticated,
'last_refreshed_on_time' => time(),
);
return $response;
return array($user, $authenticated, $response);
}
// Call: ?api&groups
route('groups', function () {
list($user, $authenticated, $response) = auth();
$response = auth();
if ($response['auth']) {
$response['groups'] = Group\get_all();
if ($authenticated) {
$response['groups'] = Model\Group\get_all($user['id']);
$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) {
$response['feeds_groups'][] = array(
@ -76,14 +67,13 @@ route('groups', function () {
// Call: ?api&feeds
route('feeds', function () {
list($user, $authenticated, $response) = auth();
$response = auth();
if ($response['auth']) {
if ($authenticated) {
$response['feeds'] = array();
$response['feeds_groups'] = array();
$feeds = Feed\get_all();
$feeds = Model\Feed\get_feeds($user['id']);
foreach ($feeds as $feed) {
$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) {
$response['feeds_groups'][] = array(
'group_id' => $group_id,
@ -111,24 +101,16 @@ route('feeds', function () {
// Call: ?api&favicons
route('favicons', function () {
list($user, $authenticated, $response) = auth();
$response = auth();
if ($response['auth']) {
$favicons = Database::getInstance('db')
->table('favicons')
->columns(
'feed_id',
'file',
'type'
)
->findAll();
if ($authenticated) {
$favicons = Model\Favicon\get_favicons_with_data_url($user['id']);
$response['favicons'] = array();
foreach ($favicons as $favicon) {
$response['favicons'][] = array(
'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
route('items', function () {
list($user, $authenticated, $response) = auth();
$response = auth();
if ($response['auth']) {
$query = Database::getInstance('db')
->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();
if ($authenticated) {
$since_id = isset($_GET['since_id']) && ctype_digit($_GET['since_id']) ? $_GET['since_id'] : null;
$item_ids = ! empty($_GET['with_ids']) ? explode(',', $_GET['with_ids']) : array();
$items = Model\Item\get_items($user['id'], $since_id, $item_ids);
$response['items'] = array();
foreach ($items as $item) {
$response['items'][] = array(
'id' => (int) $item['rowid'],
'id' => (int) $item['id'],
'feed_id' => (int) $item['feed_id'],
'title' => $item['title'],
'author' => $item['author'],
@ -182,10 +142,10 @@ route('items', function () {
);
}
$response['total_items'] = Database::getInstance('db')
->table('items')
->neq('status', 'removed')
->count();
$response['total_items'] = Model\Item\count_by_status(
$user['id'],
array(Model\Item\STATUS_READ, Model\Item\STATUS_UNREAD)
);
}
response($response);
@ -193,10 +153,9 @@ route('items', function () {
// Call: ?api&links
route('links', function () {
list(, $authenticated, $response) = auth();
$response = auth();
if ($response['auth']) {
if ($authenticated) {
$response['links'] = array();
}
@ -205,15 +164,10 @@ route('links', function () {
// Call: ?api&unread_item_ids
route('unread_item_ids', function () {
list($user, $authenticated, $response) = auth();
$response = auth();
if ($response['auth']) {
$item_ids = Database::getInstance('db')
->table('items')
->eq('status', 'unread')
->findAllByColumn('rowid');
if ($authenticated) {
$item_ids = Model\Item\get_item_ids_by_status($user['id'], Model\Item\STATUS_UNREAD);
$response['unread_item_ids'] = implode(',', $item_ids);
}
@ -222,15 +176,10 @@ route('unread_item_ids', function () {
// Call: ?api&saved_item_ids
route('saved_item_ids', function () {
list($user, $authenticated, $response) = auth();
$response = auth();
if ($response['auth']) {
$item_ids = Database::getInstance('db')
->table('items')
->eq('bookmark', 1)
->findAllByColumn('rowid');
if ($authenticated) {
$item_ids = Model\Bookmark\get_bookmarked_item_ids($user['id']);
$response['saved_item_ids'] = implode(',', $item_ids);
}
@ -239,30 +188,20 @@ route('saved_item_ids', function () {
// handle write items
route('write_items', function () {
list($user, $authenticated, $response) = auth();
$response = auth();
if ($response['auth']) {
$query = Database::getInstance('db')
->table('items')
->eq('rowid', $_POST['id']);
if ($authenticated && ctype_digit($_POST['id'])) {
$item_id = $_POST['id'];
if ($_POST['as'] === 'saved') {
$query->update(array('bookmark' => 1));
// 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);
Model\Bookmark\set_flag($user['id'], $item_id, 1);
Handler\Service\sync($user['id'], $item_id);
} elseif ($_POST['as'] === 'unsaved') {
$query->update(array('bookmark' => 0));
Model\Bookmark\set_flag($user['id'], $item_id, 0);
} 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') {
$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
route('write_feeds', function () {
list($user, $authenticated, $response) = auth();
$response = auth();
if ($response['auth']) {
Database::getInstance('db')
->table('items')
->eq('feed_id', $_POST['id'])
->lte('updated', $_POST['before'])
->update(array('status' => 'read'));
if ($authenticated && ctype_digit($_POST['id']) && ctype_digit($_POST['before'])) {
Model\ItemFeed\change_items_status(
$user['id'],
$_POST['id'],
Model\Item\STATUS_UNREAD,
Model\Item\STATUS_READ,
$_POST['before']
);
}
response($response);
@ -287,19 +227,16 @@ route('write_feeds', function () {
// handle write groups
route('write_groups', function () {
list($user, $authenticated, $response) = auth();
$response = auth();
if ($response['auth']) {
$db = Database::getInstance('db')
->table('items')
->lte('updated', $_POST['before']);
if ($_POST['id'] > 0) {
$db->in('feed_id', Model\Group\get_feeds_by_group($_POST['id']));
}
$db->update(array('status' => 'read'));
if ($authenticated && ctype_digit($_POST['id']) && ctype_digit($_POST['before'])) {
Model\ItemGroup\change_items_status(
$user['id'],
$_POST['id'],
Model\Item\STATUS_UNREAD,
Model\Item\STATUS_READ,
$_POST['before']
);
}
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\Response;
register_shutdown_function(function () {
Miniflux\Helper\write_debug_file();
});
Router\bootstrap(
__DIR__.'/app/controllers',
'common',
'console',
'user',
'config',
'item',
'history',
'about',
'api',
'auth',
'bookmark',
'config',
'feed',
'search'
'help',
'history',
'item',
'opml',
'profile',
'search',
'services',
'users'
);
Router\notfound(function() {

View File

@ -2,229 +2,129 @@
require __DIR__.'/app/common.php';
use JsonRPC\Exception\AuthenticationFailureException;
use JsonRPC\MiddlewareInterface;
use JsonRPC\Server;
use Miniflux\Handler;
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->authentication(array(
Model\Config\get('username') => Model\Config\get('api_token')
));
$server->getMiddlewareHandler()->withMiddleware(new AuthMiddleware());
$procedureHandler = $server->getProcedureHandler();
// Get version
$procedureHandler->withCallback('app.version', function () {
$procedureHandler->withCallback('getVersion', function () {
return array('version' => APP_VERSION);
});
// Get all feeds
$procedureHandler->withCallback('feed.list', function () {
$feeds = Model\Feed\get_all();
if (empty($feeds)) {
return array();
}
$procedureHandler->withCallback('getFeeds', function () {
$user_id = SessionStorage::getInstance()->getUserId();
$feeds = Model\Feed\get_feeds($user_id);
$groups = Model\Group\get_feeds_map();
foreach ($feeds as &$feed) {
$feed_id = $feed['id'];
$feed['feed_group_ids'] = array();
if (isset($groups[$feed_id])) {
$feed['feed_group_ids'] = $groups[$feed_id];
}
$feed['groups'] = Model\Group\get_feed_groups($feed['id']);
}
return $feeds;
});
// Get one feed
$procedureHandler->withCallback('feed.info', function ($feed_id) {
$result = Model\Feed\get($feed_id);
$result['feed_group_ids'] = Model\Group\get_feed_group_ids($feed_id);
return $result;
$procedureHandler->withCallback('getFeed', function ($feed_id) {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Feed\get_feed($user_id, $feed_id);
});
// Add a new feed
$procedureHandler->withCallback('feed.create', function($url) {
try {
$result = Model\Feed\create($url);
} catch (Exception $e) {
$result = false;
$procedureHandler->withCallback('createFeed', function ($url) {
$user_id = SessionStorage::getInstance()->getUserId();
list($feed_id,) = Handler\Feed\create_feed($user_id, $url);
if ($feed_id > 0) {
return $feed_id;
}
Model\Config\write_debug();
return $result;
return false;
});
// Delete a feed
$procedureHandler->withCallback('feed.delete', function($feed_id) {
return Model\Feed\remove($feed_id);
$procedureHandler->withCallback('deleteFeed', function ($feed_id) {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Feed\remove_feed($user_id, $feed_id);
});
// Delete all feeds
$procedureHandler->withCallback('feed.delete_all', function() {
return Model\Feed\remove_all();
// Refresh a feed
$procedureHandler->withCallback('refreshFeed', function ($feed_id) {
$user_id = SessionStorage::getInstance()->getUserId();
return Handler\Feed\update_feed($user_id, $feed_id);
});
// Enable a feed
$procedureHandler->withCallback('feed.enable', function($feed_id) {
return Model\Feed\enable($feed_id);
});
// 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 all items
$procedureHandler->withCallback('getItems', function ($since_id = null, array $item_ids = array(), $offset = 50) {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Item\get_items($user_id, $since_id, $item_ids, $offset);
});
// Get one item
$procedureHandler->withCallback('item.info', function ($item_id) {
return Model\Item\get($item_id);
$procedureHandler->withCallback('getItem', function ($item_id) {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Item\get_item($user_id, $item_id);
});
// Delete an item
$procedureHandler->withCallback('item.delete', function($item_id) {
return Model\Item\set_removed($item_id);
// Change items status
$procedureHandler->withCallback('changeItemsStatus', function (array $item_ids, $status) {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Item\change_item_ids_status($user_id, $item_ids, $status);
});
// Mark item as read
$procedureHandler->withCallback('item.mark_as_read', function($item_id) {
return Model\Item\set_read($item_id);
// Add a bookmark
$procedureHandler->withCallback('addBookmark', function ($item_id) {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Bookmark\set_flag($user_id, $item_id, 1);
});
// Mark item as unread
$procedureHandler->withCallback('item.mark_as_unread', function($item_id) {
return Model\Item\set_unread($item_id);
// Remove a bookmark
$procedureHandler->withCallback('removeBookmark', function ($item_id) {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Bookmark\set_flag($user_id, $item_id, 0);
});
// Change the status of list of items
$procedureHandler->withCallback('item.set_list_status', function($status, array $items) {
return Model\Item\set_status($status, $items);
// Get all groups
$procedureHandler->withCallback('getGroups', function () {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Group\get_all($user_id);
});
// Flush all read items
$procedureHandler->withCallback('item.flush', function() {
return Model\Item\mark_all_as_removed();
// Add a new group
$procedureHandler->withCallback('createGroup', function ($title) {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Group\create_group($user_id, $title);
});
// Mark all unread items as read
$procedureHandler->withCallback('item.mark_all_as_read', function() {
return Model\Item\mark_all_as_read();
// Add/Update groups for a feed
$procedureHandler->withCallback('setFeedGroups', function ($feed_id, $group_ids) {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Group\update_feed_groups($user_id, $feed_id, $group_ids);
});
// Get all items with the content
$procedureHandler->withCallback('item.get_all', function() {
return Model\Item\get_all();
});
// 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();
// Get favicons
$procedureHandler->withCallback('getFavicons', function () {
$user_id = SessionStorage::getInstance()->getUserId();
return Model\Favicon\get_favicons_with_data_url($user_id);
});
echo $server->execute();

View File

@ -13,11 +13,19 @@ $options = getopt('', array(
'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);
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
->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
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
{
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
use Miniflux\Helper;
use Miniflux\Model;
use Miniflux\Session\SessionStorage;
require_once __DIR__.'/BaseTest.php';
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()
{
$token1 = Helper\generate_token();

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