diff --git a/.gitignore b/.gitignore index 2416d16..53d2a56 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ Thumbs.db .nbproject nbproject config.php +!app/helpers/* !app/models/* !app/controllers/* !app/templates/* diff --git a/.travis.yml b/.travis.yml index a5bd74a..91d6ddd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,4 @@ before_script: - composer install script: - - phpunit -c tests/phpunit.unit.xml + - ./vendor/bin/phpunit -c tests/phpunit.unit.xml diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..02287fb --- /dev/null +++ b/ChangeLog @@ -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 diff --git a/Makefile b/Makefile index ee6f298..3b14703 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/app/common.php b/app/common.php index 9dd5034..c5f36aa 100644 --- a/app/common.php +++ b/app/common.php @@ -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, please copy and paste this message and create a bug report:
';
- $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;
diff --git a/app/constants.php b/app/constants.php
index 18cdac5..0a39b49 100644
--- a/app/constants.php
+++ b/app/constants.php
@@ -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');
diff --git a/app/controllers/about.php b/app/controllers/about.php
new file mode 100644
index 0000000..07e3020
--- /dev/null
+++ b/app/controllers/about.php
@@ -0,0 +1,22 @@
+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'),
+ )));
+});
diff --git a/app/controllers/api.php b/app/controllers/api.php
new file mode 100644
index 0000000..3dc68cb
--- /dev/null
+++ b/app/controllers/api.php
@@ -0,0 +1,20 @@
+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'),
+ )));
+});
diff --git a/app/controllers/user.php b/app/controllers/auth.php
similarity index 78%
rename from app/controllers/user.php
rename to app/controllers/auth.php
index f0781ae..9ab62c2 100644
--- a/app/controllers/user.php
+++ b/app/controllers/auth.php
@@ -1,8 +1,13 @@
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()
)));
});
diff --git a/app/controllers/bookmark.php b/app/controllers/bookmark.php
index d220da7..6df4d7f 100644
--- a/app/controllers/bookmark.php
+++ b/app/controllers/bookmark.php
@@ -1,120 +1,127 @@
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'])
);
}
diff --git a/app/controllers/common.php b/app/controllers/common.php
index 71c067f..02a3d40 100644
--- a/app/controllers/common.php
+++ b/app/controllers/common.php
@@ -1,10 +1,12 @@
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),
+ );
+}
diff --git a/app/controllers/config.php b/app/controllers/config.php
index edce1c8..a72f400 100644
--- a/app/controllers/config.php
+++ b/app/controllers/config.php
@@ -1,65 +1,19 @@
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');
-});
diff --git a/app/controllers/console.php b/app/controllers/console.php
deleted file mode 100644
index 3b6c3eb..0000000
--- a/app/controllers/console.php
+++ /dev/null
@@ -1,22 +0,0 @@
- @file_get_contents(DEBUG_FILENAME),
- 'nb_unread_items' => Model\Item\count_by_status('unread'),
- 'menu' => 'config',
- 'title' => t('Console')
- )));
-});
diff --git a/app/controllers/feed.php b/app/controllers/feed.php
index c158cc6..9dff1d7 100644
--- a/app/controllers/feed.php
+++ b/app/controllers/feed.php
@@ -1,11 +1,12 @@
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()
+ 'rtl' => 0,
+ 'cloak_referrer' => 0,
+ 'create_group' => '',
+ '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(),
- 'menu' => 'feeds',
- 'title' => t('New subscription')
+ 'groups' => Model\Group\get_all($user_id),
+ 'menu' => 'feeds',
+ 'title' => t('New subscription'),
)));
});
@@ -162,100 +168,49 @@ 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(
- 'url' => trim($url),
+ 'url' => trim($url),
'download_content' => 0,
- 'rtl' => 0,
- 'cloak_referrer' => 0,
- 'create_group' => '',
- 'feed_group_ids' => array()
+ 'rtl' => 0,
+ 'cloak_referrer' => 0,
+ 'feed_group_ids' => array(),
);
- try {
- $feed_id = Model\Feed\create(
- $values['url'],
- $values['download_content'],
- $values['rtl'],
- $values['cloak_referrer'],
- $values['feed_group_ids'],
- $values['create_group']
- );
- } 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.');
- }
+ 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['groups']
+ );
- 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(),
- 'menu' => 'feeds',
- 'title' => t('Subscriptions')
+ 'groups' => Model\Group\get_all($user_id),
+ 'menu' => 'feeds',
+ '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');
- }
-});
diff --git a/app/controllers/help.php b/app/controllers/help.php
new file mode 100644
index 0000000..c7c5c9e
--- /dev/null
+++ b/app/controllers/help.php
@@ -0,0 +1,26 @@
+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'));
+});
+
diff --git a/app/controllers/history.php b/app/controllers/history.php
index bd9c865..690583f 100644
--- a/app/controllers/history.php
+++ b/app/controllers/history.php
@@ -1,62 +1,30 @@
Model\Favicon\get_item_favicons($items),
- 'original_marks_read' => Model\Config\get('original_marks_read'),
- 'items' => $items,
- 'order' => $order,
- 'direction' => $direction,
- 'display_mode' => Model\Config\get('items_display_mode'),
- 'item_title_link' => Model\Config\get('item_title_link'),
- 'group_id' => $group_id,
- 'nb_items' => $nb_items,
- 'nb_unread_items' => Model\Item\count_by_status('unread'),
- 'offset' => $offset,
- 'items_per_page' => Model\Config\get('items_per_page'),
- 'nothing_to_read' => Request\int_param('nothing_to_read'),
- 'menu' => 'history',
- 'groups' => Model\Group\get_all(),
- 'title' => t('History').' ('.$nb_items.')'
+ Response\html(Template\layout('history', $params + array(
+ 'title' => t('History') . ' (' . $params['nb_items'] . ')',
+ 'menu' => 'history',
)));
});
// 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');
diff --git a/app/controllers/item.php b/app/controllers/item.php
index eb3967e..53c2010 100644
--- a/app/controllers/item.php
+++ b/app/controllers/item.php
@@ -1,101 +1,73 @@
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.'¬hing_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.')',
- 'menu' => 'unread',
- 'groups' => Model\Group\get_all()
+ Response\html(Template\layout('unread_items', $params + array(
+ 'title' => 'Miniflux (' . $params['nb_items'] . ')',
+ 'menu' => 'unread',
)));
});
// 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(
diff --git a/app/controllers/opml.php b/app/controllers/opml.php
new file mode 100644
index 0000000..af4081e
--- /dev/null
+++ b/app/controllers/opml.php
@@ -0,0 +1,40 @@
+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');
+ }
+});
diff --git a/app/controllers/profile.php b/app/controllers/profile.php
new file mode 100644
index 0000000..e6b9dbe
--- /dev/null
+++ b/app/controllers/profile.php
@@ -0,0 +1,48 @@
+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')
+ )));
+});
diff --git a/app/controllers/search.php b/app/controllers/search.php
index 2b0ed56..47e5b28 100644
--- a/app/controllers/search.php
+++ b/app/controllers/search.php
@@ -1,39 +1,40 @@
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.')'
diff --git a/app/controllers/services.php b/app/controllers/services.php
new file mode 100644
index 0000000..a3fa059
--- /dev/null
+++ b/app/controllers/services.php
@@ -0,0 +1,38 @@
+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');
+});
diff --git a/app/controllers/users.php b/app/controllers/users.php
new file mode 100644
index 0000000..a4fd7a2
--- /dev/null
+++ b/app/controllers/users.php
@@ -0,0 +1,134 @@
+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');
+});
\ No newline at end of file
diff --git a/app/core/session.php b/app/core/session.php
index 430cde9..00d05d1 100644
--- a/app/core/session.php
+++ b/app/core/session.php
@@ -2,54 +2,155 @@
namespace Miniflux\Session;
-const SESSION_LIFETIME = 2678400;
+use Miniflux\Helper;
-function open($base_path = '/', $save_path = '', $session_lifetime = SESSION_LIFETIME)
+class SessionManager
{
- if ($save_path !== '') {
- session_save_path($save_path);
+ const SESSION_LIFETIME = 2678400;
+
+ public static function open($base_path = '/', $save_path = '', $duration = self::SESSION_LIFETIME)
+ {
+ if ($save_path !== '') {
+ session_save_path($save_path);
+ }
+
+ // HttpOnly and secure flags for session cookie
+ session_set_cookie_params(
+ $duration,
+ $base_path ?: '/',
+ null,
+ Helper\is_secure_connection(),
+ true
+ );
+
+ // Avoid session id in the URL
+ ini_set('session.use_only_cookies', true);
+
+ // Ensure session ID integrity
+ ini_set('session.entropy_file', '/dev/urandom');
+ ini_set('session.entropy_length', '32');
+ ini_set('session.hash_bits_per_character', 6);
+
+ // Custom session name
+ session_name('MX_SID');
+
+ session_start();
+
+ // Regenerate the session id to avoid session fixation issue
+ if (empty($_SESSION['__validated'])) {
+ session_regenerate_id(true);
+ $_SESSION['__validated'] = 1;
+ }
}
- // HttpOnly and secure flags for session cookie
- session_set_cookie_params(
- $session_lifetime,
- $base_path ?: '/',
- null,
- isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
- true
- );
-
- // Avoid session id in the URL
- ini_set('session.use_only_cookies', true);
-
- // Ensure session ID integrity
- ini_set('session.entropy_file', '/dev/urandom');
- ini_set('session.entropy_length', '32');
- ini_set('session.hash_bits_per_character', 6);
-
- // Custom session name
- session_name('__$');
-
- session_start();
-
- // Regenerate the session id to avoid session fixation issue
- if (empty($_SESSION['__validated'])) {
- session_regenerate_id(true);
- $_SESSION['__validated'] = 1;
+ public static function close()
+ {
+ session_destroy();
}
}
-function close()
-{
- session_destroy();
-}
-function flash($message)
+class SessionStorage
{
- $_SESSION['flash_message'] = $message;
-}
+ private static $instance = null;
-function flash_error($message)
-{
- $_SESSION['flash_error_message'] = $message;
+ 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;
+ }
+
+ 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;
+ }
}
diff --git a/app/core/template.php b/app/core/template.php
index b5e6297..a5dd94f 100644
--- a/app/core/template.php
+++ b/app/core/template.php
@@ -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))
+ );
}
diff --git a/app/functions.php b/app/functions.php
index 2c828d8..30da50e 100644
--- a/app/functions.php
+++ b/app/functions.php
@@ -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;
+}
diff --git a/app/handlers/feed.php b/app/handlers/feed.php
new file mode 100644
index 0000000..88495a4
--- /dev/null
+++ b/app/handlers/feed.php
@@ -0,0 +1,161 @@
+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;
+}
diff --git a/app/handlers/item.php b/app/handlers/item.php
new file mode 100644
index 0000000..8db9954
--- /dev/null
+++ b/app/handlers/item.php
@@ -0,0 +1,33 @@
+table('items')
+ ->eq('id', $item['id'])
+ ->save(array('content' => $content));
+ }
+
+ return array(
+ 'result' => true,
+ 'content' => $content
+ );
+ }
+
+ return array(
+ 'result' => false,
+ 'content' => ''
+ );
+}
diff --git a/app/handlers/opml.php b/app/handlers/opml.php
index 323788a..654040a 100644
--- a/app/handlers/opml.php
+++ b/app/handlers/opml.php
@@ -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;
+}
diff --git a/app/handlers/proxy.php b/app/handlers/proxy.php
index dd672cb..3023bc8 100644
--- a/app/handlers/proxy.php
+++ b/app/handlers/proxy.php
@@ -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) {
+ }
}
diff --git a/app/handlers/scraper.php b/app/handlers/scraper.php
index 957dd11..a806ebb 100644
--- a/app/handlers/scraper.php
+++ b/app/handlers/scraper.php
@@ -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();
diff --git a/app/handlers/service.php b/app/handlers/service.php
index 62de47b..88fb541 100644
--- a/app/handlers/service.php
+++ b/app/handlers/service.php
@@ -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);
}
diff --git a/app/helpers/app.php b/app/helpers/app.php
index ec3eea8..19d8122 100644
--- a/app/helpers/app.php
+++ b/app/helpers/app.php
@@ -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'];
- $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
+
+ 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);
+ }
+}
diff --git a/app/helpers/config.php b/app/helpers/config.php
new file mode 100644
index 0000000..e4f4ceb
--- /dev/null
+++ b/app/helpers/config.php
@@ -0,0 +1,38 @@
+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);
+}
diff --git a/app/helpers/favicon.php b/app/helpers/favicon.php
index 9d5a669..77a3456 100644
--- a/app/helpers/favicon.php
+++ b/app/helpers/favicon.php
@@ -22,7 +22,7 @@ function favicon_extension($type)
function favicon(array $favicons, $feed_id)
{
if (! empty($favicons[$feed_id])) {
- return '';
+ return '';
}
return '';
diff --git a/app/helpers/template.php b/app/helpers/template.php
index a0cf7b4..9c0d6fd 100644
--- a/app/helpers/template.php
+++ b/app/helpers/template.php
@@ -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';
diff --git a/app/locales/ar_AR/translations.php b/app/locales/ar_AR/translations.php
index 3abbf85..088dd3b 100644
--- a/app/locales/ar_AR/translations.php
+++ b/app/locales/ar_AR/translations.php
@@ -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' => 'إفتراضي',
diff --git a/app/locales/cs_CZ/translations.php b/app/locales/cs_CZ/translations.php
index 0a9bf6d..ab84c53 100644
--- a/app/locales/cs_CZ/translations.php
+++ b/app/locales/cs_CZ/translations.php
@@ -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í',
diff --git a/app/locales/de_DE/translations.php b/app/locales/de_DE/translations.php
index 10b59d9..2b155fb 100644
--- a/app/locales/de_DE/translations.php
+++ b/app/locales/de_DE/translations.php
@@ -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',
diff --git a/app/locales/es_ES/translations.php b/app/locales/es_ES/translations.php
index 5f8fb56..42e6c03 100644
--- a/app/locales/es_ES/translations.php
+++ b/app/locales/es_ES/translations.php
@@ -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',
diff --git a/app/locales/fr_FR/translations.php b/app/locales/fr_FR/translations.php
index 08f4a15..d74edf6 100644
--- a/app/locales/fr_FR/translations.php
+++ b/app/locales/fr_FR/translations.php
@@ -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',
diff --git a/app/locales/it_IT/translations.php b/app/locales/it_IT/translations.php
index 5fc5341..1c6869f 100644
--- a/app/locales/it_IT/translations.php
+++ b/app/locales/it_IT/translations.php
@@ -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' => '',
diff --git a/app/locales/ja_JP/translations.php b/app/locales/ja_JP/translations.php
index 44f9a58..17c534a 100644
--- a/app/locales/ja_JP/translations.php
+++ b/app/locales/ja_JP/translations.php
@@ -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' => 'デフォルト',
diff --git a/app/locales/pt_BR/translations.php b/app/locales/pt_BR/translations.php
index 7da3e3f..aa306d6 100644
--- a/app/locales/pt_BR/translations.php
+++ b/app/locales/pt_BR/translations.php
@@ -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',
diff --git a/app/locales/ru_RU/translations.php b/app/locales/ru_RU/translations.php
index 30bb13d..cd370d8 100644
--- a/app/locales/ru_RU/translations.php
+++ b/app/locales/ru_RU/translations.php
@@ -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' => 'По-умолчанию',
diff --git a/app/locales/sr_RS/translations.php b/app/locales/sr_RS/translations.php
index c1791bd..7a83e83 100644
--- a/app/locales/sr_RS/translations.php
+++ b/app/locales/sr_RS/translations.php
@@ -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' => 'Основна',
diff --git a/app/locales/sr_RS@latin/translations.php b/app/locales/sr_RS@latin/translations.php
index 5e1651b..b745f9e 100644
--- a/app/locales/sr_RS@latin/translations.php
+++ b/app/locales/sr_RS@latin/translations.php
@@ -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',
diff --git a/app/locales/tr_TR/translations.php b/app/locales/tr_TR/translations.php
index 40d39f1..d7b1761 100644
--- a/app/locales/tr_TR/translations.php
+++ b/app/locales/tr_TR/translations.php
@@ -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',
diff --git a/app/locales/zh_CN/translations.php b/app/locales/zh_CN/translations.php
index aab4bc4..1cefa92 100644
--- a/app/locales/zh_CN/translations.php
+++ b/app/locales/zh_CN/translations.php
@@ -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' => '默认',
diff --git a/app/models/auto_update.php b/app/models/auto_update.php
index 5bcf7d9..b45880c 100644
--- a/app/models/auto_update.php
+++ b/app/models/auto_update.php
@@ -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
diff --git a/app/models/bookmark.php b/app/models/bookmark.php
index aa4fbab..3f08768 100644
--- a/app/models/bookmark.php
+++ b/app/models/bookmark.php
@@ -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('items')
- ->eq('id', $id)
- ->in('status', array('read', 'unread'))
- ->save(array('bookmark' => $value));
+ ->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(Model\Item\TABLE)
+ ->eq('user_id', $user_id)
+ ->eq('id', $item_id)
+ ->update(array('bookmark' => (int) $value));
}
diff --git a/app/models/config.php b/app/models/config.php
index 87a13e9..043e29c 100644
--- a/app/models/config.php
+++ b/app/models/config.php
@@ -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,60 +22,41 @@ 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(
- 'ar_AR' => 'عربي',
- 'cs_CZ' => 'Čeština',
- 'de_DE' => 'Deutsch',
- 'en_US' => 'English',
- 'es_ES' => 'Español',
- 'fr_FR' => 'Français',
- 'it_IT' => 'Italiano',
- 'ja_JP' => '日本語',
- 'pt_BR' => 'Português',
- 'zh_CN' => '简体中国',
- 'sr_RS' => 'српски',
+ 'ar_AR' => 'عربي',
+ 'cs_CZ' => 'Čeština',
+ 'de_DE' => 'Deutsch',
+ 'en_US' => 'English',
+ 'es_ES' => 'Español',
+ 'fr_FR' => 'Français',
+ 'it_IT' => 'Italiano',
+ 'ja_JP' => '日本語',
+ 'pt_BR' => 'Português',
+ 'zh_CN' => '简体中国',
+ 'sr_RS' => 'српски',
'sr_RS@latin' => 'srpski',
- 'ru_RU' => 'Русский',
- 'tr_TR' => 'Türkçe',
+ 'ru_RU' => 'Русский',
+ 'tr_TR' => 'Türkçe',
);
}
-// Get all skins
function get_themes()
{
$themes = array(
@@ -129,52 +76,47 @@ function get_themes()
return $themes;
}
-// Sorting direction choices for items
function get_sorting_directions()
{
return array(
- 'asc' => t('Older items first'),
+ 'asc' => t('Older items first'),
'desc' => t('Most recent first'),
);
}
-// Display summaries or full contents on lists
function get_display_mode()
{
return array(
- 'titles' => t('Titles'),
+ '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(
- '0' => t('Never'),
+ '0' => t('Never'),
'-1' => t('Immediately'),
- '1' => t('After %d day', 1),
- '5' => t('After %d day', 5),
+ '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(
- '0' => t('Never'),
+ '0' => t('Never'),
'15' => t('After %d day', 15),
'30' => t('After %d day', 30),
'45' => t('After %d day', 45),
@@ -182,14 +124,13 @@ function get_autoflush_unread_options()
);
}
-// Number of items per pages
function get_paging_options()
{
return array(
- 10 => 10,
- 20 => 20,
- 30 => 30,
- 50 => 50,
+ 10 => 10,
+ 20 => 20,
+ 30 => 30,
+ 50 => 50,
100 => 100,
150 => 150,
200 => 200,
@@ -197,84 +138,96 @@ function get_paging_options()
);
}
-// Get redirect options when there is nothing to read
function get_nothing_to_read_redirections()
{
return array(
- 'feeds' => t('Subscriptions'),
- 'history' => t('History'),
+ 'feeds' => t('Subscriptions'),
+ 'history' => t('History'),
'bookmarks' => t('Bookmarks'),
);
}
-
-// 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 (isset($_SESSION['config'][$name])) {
- return $_SESSION['config'][$name];
- }
+ if (empty($settings)) {
+ save_defaults($user_id);
+ $settings = Database::getInstance('db')
+ ->hashtable(TABLE)
+ ->eq('user_id', $user_id)
+ ->getAll('key', 'value');
}
- return null;
+ return $settings;
}
-// 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,
+ ));
+ }
}
- return false;
-}
+ 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;
}
diff --git a/app/models/database.php b/app/models/database.php
deleted file mode 100644
index 1f21980..0000000
--- a/app/models/database.php
+++ /dev/null
@@ -1,102 +0,0 @@
- '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;
-}
diff --git a/app/models/favicon.php b/app/models/favicon.php
index 75cedcf..047d0c8 100644
--- a/app/models/favicon.php
+++ b/app/models/favicon.php
@@ -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')
- ->save(array(
- 'feed_id' => $feed_id,
- 'favicon_id' => $favicon_id
- ));
+ ->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(
- 'hash' => $hash,
- 'type' => $type
- ));
+ return Database::getInstance('db')
+ ->table(TABLE)
+ ->persist(array(
+ 'hash' => $hash,
+ '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')
- ->eq('hash', $hash)
- ->findOneColumn('id');
+ ->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()
-{
- $favicons = Database::getInstance('db')
- ->table('favicons')
- ->columns(
- 'favicons.type',
- 'favicons.hash',
- 'favicons_feeds.feed_id'
- )
- ->join('favicons_feeds', 'favicon_id', 'id')
- ->isNull('favicons_feeds.feed_id')
- ->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;
+ return Database::getInstance('db')
+ ->table(JOIN_TABLE)
+ ->eq('feed_id', $feed_id)
+ ->exists();
}
-// Get favicons for those feeds
-function get_favicons(array $feed_ids)
+function get_favicons_by_feed_ids(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();
+ if (! Helper\bool_config('favicons')) {
+ return $result;
+ }
+
+ $favicons = Database::getInstance('db')
+ ->table(TABLE)
+ ->columns(
+ 'favicons.type',
+ 'favicons.hash',
+ 'favicons_feeds.feed_id'
+ )
+ ->join('favicons_feeds', 'favicon_id', 'id')
+ ->in('favicons_feeds.feed_id', $feed_ids)
+ ->findAll();
+
+ foreach ($favicons as $favicon) {
+ $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')
- ->findAll();
-
- $map = array();
-
- foreach ($result as $row) {
- $map[$row['feed_id']] = array(
- "type" => $row['type'],
- "hash" => $row['hash']
- );
- }
-
- return $map;
+ 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();
+
+ foreach ($favicons as &$favicon) {
+ $favicon['url'] = get_favicon_data_url($favicon['file'], $favicon['mime_type']);
+ }
+
+ return $favicons;
}
diff --git a/app/models/feed.php b/app/models/feed.php
index 11a87b3..78a7420 100644
--- a/app/models/feed.php
+++ b/app/models/feed.php
@@ -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()
- );
+ $feed_id = $db
+ ->table(TABLE)
+ ->persist(array(
+ 'user_id' => $user_id,
+ 'title' => $feed->getTitle(),
+ 'site_url' => $feed->getSiteUrl(),
+ 'feed_url' => $feed->getFeedUrl(),
+ 'download_content' => $scraper ? 1 : 0,
+ 'rtl' => $rtl ? 1 : 0,
+ 'etag' => $etag,
+ 'last_modified' => $last_modified,
+ 'last_checked' => time(),
+ 'cloak_referrer' => $cloak_referrer ? 1 : 0,
+ ));
- if ($enable_grabber) {
- $parser->enableContentGrabber();
- }
-
- $feed = $parser->execute();
-
- // Save the feed
- $result = $db->table('feeds')->save(array(
- '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(),
- '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'];
- }
-
- $result['items_total'] += $count['item_count'];
+ if ($limit !== null) {
+ $query->limit($limit);
}
- return $result;
+ return $query->findAllByColumn('id');
}
-// 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();
+
+ $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($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;
+ }
+
+ $db->closeTransaction();
+ return true;
+ }
+
+ $db->cancelTransaction();
+ return false;
}
-// Update last check date
-function update_last_checked($feed_id)
+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_checked' => time()
- ));
+ ->save((array('enabled' => $status)));
}
-// Update Etag and last Modified columns
-function update_cache($feed_id, $last_modified, $etag)
+function remove_feed($user_id, $feed_id)
{
- 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
- ));
+ ->remove();
}
-// Remove one feed
-function remove($feed_id)
+function count_failed_feeds($user_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('parsing_error', 1)
+ ->count();
}
-// Remove all feeds
-function remove_all()
+function count_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)
+ ->count();
}
-// Enable a feed (activate refresh)
-function enable($feed_id)
-{
- return Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 1)));
-}
-
-// Disable feed
-function disable($feed_id)
-{
- return Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 0)));
-}
diff --git a/app/models/group.php b/app/models/group.php
index bc89f52..36f3933 100644
--- a/app/models/group.php
+++ b/app/models/group.php
@@ -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')
- ->orderBy('title')
- ->findAll();
+ ->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')
- ->findAll();
+ $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('title', $title)
- ->findOneColumn('id');
+ ->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')
- ->eq('group_id', $group_id)
- ->findAllByColumn('feed_id');
+ ->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')
- ->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)) {
+ if (empty($group_ids)) {
return false;
}
- // add requested groups to feed
- if (! empty($missing) && ! add($feed_id, $missing)) {
- return false;
- }
-
- return true;
+ return Database::getInstance('db')
+ ->table(JOIN_TABLE)
+ ->eq('feed_id', $feed_id)
+ ->in('group_id', $group_ids)
+ ->remove();
}
diff --git a/app/models/item.php b/app/models/item.php
index b43ab5e..5d8a90a 100644
--- a/app/models/item.php
+++ b/app/models/item.php
@@ -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)
{
- return Database::getInstance('db')
+ $query = Database::getInstance('db')
->table('items')
- ->eq('id', $id)
- ->save(array('status' => 'read'));
-}
+ ->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');
-// 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;
+ if ($since_id !== null) {
+ $query->gt('items.id', $since_id);
+ } elseif (! empty($item_ids)) {
+ $query->in('items.id', $item_ids);
}
- return Database::getInstance('db')
- ->table('items')
- ->in('id', $items)
- ->save(array('status' => $status));
+ return $query->findAll();
}
-// Mark all unread items as read
-function mark_all_as_read()
+function get_item_ids_by_status($user_id, $status)
{
return Database::getInstance('db')
->table('items')
- ->eq('status', 'unread')
- ->save(array('status' => 'read'));
+ ->eq('user_id', $user_id)
+ ->eq('status', $status)
+ ->findAllByColumn('id');
}
-// Mark all read items to removed
-function mark_all_as_removed()
+function get_latest_feeds_items($user_id)
{
return Database::getInstance('db')
- ->table('items')
- ->eq('status', 'read')
- ->eq('bookmark', 0)
- ->save(array('status' => 'removed', 'content' => ''));
+ ->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();
}
-// Mark all read items to removed after X days
-function autoflush_read()
+function count_by_status($user_id, $status, $feed_ids = array())
{
- $autoflush = (int) Config\get('autoflush');
+ $query = Database::getInstance('db')
+ ->table('items')
+ ->eq('user_id', $user_id)
+ ->in('feed_id', $feed_ids);
+
+ if (is_array($status)) {
+ $query->in('status', $status);
+ } else {
+ $query->eq('status', $status);
+ }
+
+ return $query->count();
+}
+
+function autoflush_read($user_id)
+{
+ $autoflush = Helper\int_config('autoflush');
if ($autoflush > 0) {
-
- // Mark read items removed after X days
Database::getInstance('db')
- ->table('items')
+ ->table(TABLE)
+ ->eq('user_id', $user_id)
->eq('bookmark', 0)
- ->eq('status', 'read')
+ ->eq('status', STATUS_READ)
->lt('updated', strtotime('-'.$autoflush.'day'))
- ->save(array('status' => 'removed', 'content' => ''));
+ ->save(array('status' => STATUS_REMOVED, 'content' => ''));
} elseif ($autoflush === -1) {
-
- // Mark read items removed immediately
Database::getInstance('db')
- ->table('items')
+ ->table(TABLE)
+ ->eq('user_id', $user_id)
->eq('bookmark', 0)
- ->eq('status', 'read')
- ->save(array('status' => 'removed', 'content' => ''));
+ ->eq('status', STATUS_READ)
+ ->save(array('status' => STATUS_REMOVED, 'content' => ''));
}
}
-// Mark all unread items to removed after X days
-function autoflush_unread()
+function autoflush_unread($user_id)
{
- $autoflush = (int) Config\get('autoflush_unread');
+ $autoflush = Helper\int_config('autoflush_unread');
if ($autoflush > 0) {
-
- // Mark read items removed after X days
Database::getInstance('db')
- ->table('items')
+ ->table(TABLE)
+ ->eq('user_id', $user_id)
->eq('bookmark', 0)
- ->eq('status', 'unread')
+ ->eq('status', STATUS_UNREAD)
->lt('updated', strtotime('-'.$autoflush.'day'))
- ->save(array('status' => 'removed', 'content' => ''));
+ ->save(array('status' => STATUS_REMOVED, 'content' => ''));
}
}
-
-// Update all items
-function update_all($feed_id, array $items)
-{
- $nocontent = (bool) Config\get('nocontent');
-
- $items_in_feed = array();
-
- $db = Database::getInstance('db');
- $db->startTransaction();
-
- foreach ($items as $item) {
- Logger::setMessage('Item => '.$item->getId().' '.$item->getUrl());
-
- // Item parsed correctly?
- if ($item->getId() && $item->getUrl()) {
- Logger::setMessage('Item parsed correctly');
-
- // Get item record in database, if any
- $itemrec = $db
- ->table('items')
- ->columns('enclosure')
- ->eq('id', $item->getId())
- ->findOne();
-
- // Insert a new item
- if ($itemrec === null) {
- Logger::setMessage('Item added to the database');
-
- $db->table('items')->save(array(
- 'id' => $item->getId(),
- 'title' => $item->getTitle(),
- 'url' => $item->getUrl(),
- 'updated' => $item->getDate()->getTimestamp(),
- 'author' => $item->getAuthor(),
- 'content' => $nocontent ? '' : $item->getContent(),
- 'status' => 'unread',
- 'feed_id' => $feed_id,
- 'enclosure' => $item->getEnclosureUrl(),
- 'enclosure_type' => $item->getEnclosureType(),
- 'language' => $item->getLanguage(),
- ));
- } elseif (! $itemrec['enclosure'] && $item->getEnclosureUrl()) {
- Logger::setMessage('Update item enclosure');
-
- $db->table('items')->eq('id', $item->getId())->save(array(
- 'status' => 'unread',
- 'enclosure' => $item->getEnclosureUrl(),
- 'enclosure_type' => $item->getEnclosureType(),
- ));
- } else {
- Logger::setMessage('Item already in the database');
- }
-
- // Items inside this feed
- $items_in_feed[] = $item->id;
- }
- }
-
- // Cleanup old items
- cleanup($feed_id, $items_in_feed);
-
- $db->closeTransaction();
-}
-
-// Remove from the database items marked as "removed"
-// and not present inside the feed
-function cleanup($feed_id, array $items_in_feed)
-{
- if (! empty($items_in_feed)) {
- $db = Database::getInstance('db');
-
- $removed_items = $db
- ->table('items')
- ->columns('id')
- ->notIn('id', $items_in_feed)
- ->eq('status', 'removed')
- ->eq('feed_id', $feed_id)
- ->desc('updated')
- ->findAllByColumn('id');
-
- // 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)
-{
- $item = get($item_id);
- $content = Handler\Scraper\download_contents($item['url']);
-
- if (! empty($content)) {
- if (! Config\get('nocontent')) {
- Database::getInstance('db')
- ->table('items')
- ->eq('id', $item['id'])
- ->save(array('content' => $content));
- }
-
- Config\write_debug();
-
- return array(
- 'result' => true,
- 'content' => $content
- );
- }
-
- Config\write_debug();
-
- return array(
- 'result' => false,
- 'content' => ''
- );
-}
diff --git a/app/models/item_feed.php b/app/models/item_feed.php
index a6bdf7d..5456223 100644
--- a/app/models/item_feed.php
+++ b/app/models/item_feed.php
@@ -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));
}
diff --git a/app/models/item_group.php b/app/models/item_group.php
index 53a7ef7..28e98b0 100644
--- a/app/models/item_group.php
+++ b/app/models/item_group.php
@@ -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'));
-}
-
-function mark_all_as_removed($group_id)
-{
- $feed_ids = Group\get_feeds_by_group($group_id);
-
- return Database::getInstance('db')
- ->table('items')
- ->eq('status', 'read')
- ->eq('bookmark', 0)
- ->in('feed_id', $feed_ids)
- ->save(array('status' => 'removed', 'content' => ''));
+ if (empty($feed_ids)) {
+ return false;
+ }
+
+ $query = Database::getInstance('db')
+ ->table(Item\TABLE)
+ ->eq('user_id', $user_id)
+ ->eq('status', $current_status)
+ ->in('feed_id', $feed_ids);
+
+ if ($before !== null) {
+ $query->lte('updated', $before);
+ }
+
+ return $query->update(array('status' => $new_status));
}
diff --git a/app/models/remember_me.php b/app/models/remember_me.php
index 1a97d7f..4ca30c1 100644
--- a/app/models/remember_me.php
+++ b/app/models/remember_me.php
@@ -2,73 +2,35 @@
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 TABLE = 'remember_me';
const COOKIE_NAME = '_R_';
-const EXPIRATION = 5184000;
+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)
- ->eq('token', $token)
- ->eq('sequence', $sequence)
- ->gt('expiration', time())
- ->findOne();
+ ->table(TABLE)
+ ->eq('token', $token)
+ ->eq('sequence', $sequence)
+ ->gt('expiration', time())
+ ->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,27 +79,14 @@ function create($dbname, $username, $ip, $user_agent)
);
}
-/**
- * Remove old sessions
- *
- * @access public
- * @return bool
- */
function cleanup()
{
return Database::getInstance('db')
- ->table(TABLE)
- ->lt('expiration', time())
- ->remove();
+ ->table(TABLE)
+ ->lt('expiration', time())
+ ->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(
diff --git a/app/models/search.php b/app/models/search.php
index 21e0599..05f5a8e 100644
--- a/app/models/search.php
+++ b/app/models/search.php
@@ -1,43 +1,47 @@
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();
diff --git a/app/models/user.php b/app/models/user.php
index 3206273..f8bc70f 100644
--- a/app/models/user.php
+++ b/app/models/user.php
@@ -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;
+ }
+
+ if ($password !== null) {
+ $values['password'] = password_hash($password, PASSWORD_BCRYPT);
+ }
+
+ if ($is_admin !== null) {
+ $values['is_admin'] = (int) $is_admin;
+ }
+
+ if (! empty($values)) {
+ return Database::getInstance('db')
+ ->table(TABLE)
+ ->eq('id', $user_id)
+ ->update($values);
+ }
+
+ return true;
}
-// Get the credentials from the current selected database
-function get_credentials()
+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()));
}
diff --git a/app/schemas/sqlite.php b/app/schemas/sqlite.php
index 8d4125c..611cdf5 100644
--- a/app/schemas/sqlite.php
+++ b/app/schemas/sqlite.php
@@ -5,396 +5,129 @@ namespace Miniflux\Schema;
use PDO;
use Miniflux\Helper;
-const VERSION = 44;
-
-
-function version_44(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 (
- id INTEGER PRIMARY KEY,
- 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
- )
- ');
-}
-
-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" (
- id INTEGER PRIMARY KEY,
- title TEXT
- )
- ');
-
- $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 (
- id INTEGER PRIMARY KEY,
- username TEXT,
- ip TEXT,
- user_agent TEXT,
- token TEXT,
- sequence TEXT,
- expiration INTEGER,
- date_creation INTEGER
- )'
- );
-}
-
-function version_24(PDO $pdo)
-{
- $pdo->exec("ALTER TABLE config ADD COLUMN auto_update_url TEXT DEFAULT 'https://github.com/fguillot/miniflux/archive/master.zip'");
-}
-
-function version_23(PDO $pdo)
-{
- $pdo->exec('ALTER TABLE items ADD COLUMN language TEXT');
-}
-
-function version_22(PDO $pdo)
-{
- $pdo->exec("ALTER TABLE config ADD COLUMN timezone TEXT DEFAULT 'UTC'");
-}
-
-function version_21(PDO $pdo)
-{
- $pdo->exec('ALTER TABLE items ADD COLUMN enclosure TEXT');
- $pdo->exec('ALTER TABLE items ADD COLUMN enclosure_type TEXT');
-}
-
-function version_20(PDO $pdo)
-{
- $pdo->exec('ALTER TABLE config ADD COLUMN redirect_nothing_to_read TEXT DEFAULT "feeds"');
-}
-
-function version_19(PDO $pdo)
-{
- $rq = $pdo->prepare('SELECT autoflush FROM config');
- $rq->execute();
- $value = (int) $rq->fetchColumn();
-
- // Change default value of autoflush to 15 days to avoid very large database
- if ($value <= 0) {
- $rq = $pdo->prepare('UPDATE config SET autoflush=?');
- $rq->execute(array(15));
- }
-}
-
-function version_18(PDO $pdo)
-{
- $pdo->exec('ALTER TABLE feeds ADD COLUMN parsing_error INTEGER DEFAULT 0');
-}
-
-function version_17(PDO $pdo)
-{
- $pdo->exec('ALTER TABLE config ADD COLUMN items_sorting_direction TEXT DEFAULT "desc"');
-}
-
-function version_16(PDO $pdo)
-{
- $pdo->exec('ALTER TABLE config ADD COLUMN auth_google_token TEXT DEFAULT ""');
- $pdo->exec('ALTER TABLE config ADD COLUMN auth_mozilla_token TEXT DEFAULT ""');
-}
-
-function version_15(PDO $pdo)
-{
- $pdo->exec('ALTER TABLE feeds ADD COLUMN download_content INTEGER DEFAULT 0');
-}
-
-function version_14(PDO $pdo)
-{
- $pdo->exec('ALTER TABLE config ADD COLUMN feed_token TEXT DEFAULT "'.Helper\generate_token().'"');
-}
-
-function version_13(PDO $pdo)
-{
- $pdo->exec('ALTER TABLE feeds ADD COLUMN enabled INTEGER DEFAULT 1');
-}
-
-function version_12(PDO $pdo)
-{
- $pdo->exec('ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT "'.Helper\generate_token().'"');
-}
-
-function version_11(PDO $pdo)
-{
- $rq = $pdo->prepare('
- SELECT
- items.id, items.url AS item_url, feeds.site_url
- FROM items
- LEFT JOIN feeds ON feeds.id=items.feed_id
- ');
-
- $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');
-}
+const VERSION = 1;
function version_1(PDO $pdo)
{
- $pdo->exec("
- CREATE TABLE config (
- username TEXT DEFAULT 'admin',
- password TEXT
- )
- ");
+ $pdo->exec('CREATE TABLE users (
+ id INTEGER PRIMARY KEY,
+ 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("
- INSERT INTO config
- (password)
- VALUES ('".\password_hash('admin', PASSWORD_BCRYPT)."')
- ");
+ $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,
- site_url TEXT,
- feed_url TEXT UNIQUE,
- title TEXT COLLATE NOCASE
- )
+ $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,
+ 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)
+ )');
+
+ $pdo->exec('CREATE TABLE "groups" (
+ id INTEGER PRIMARY KEY,
+ 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" (
+ 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
+ )');
+
+ $pdo->exec('CREATE TABLE favicons (
+ id INTEGER PRIMARY KEY,
+ 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,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+
+ $fever_token = Helper\generate_token();
+ $rq = $pdo->prepare('
+ INSERT INTO users
+ (username, password, is_admin, api_token, bookmarklet_token, cronjob_token, feed_token, fever_token, fever_api_key)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
');
- $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),
+ ));
}
diff --git a/app/templates/about.php b/app/templates/about.php
index dd84381..9263f8c 100644
--- a/app/templates/about.php
+++ b/app/templates/about.php
@@ -3,9 +3,12 @@