diff --git a/common.php b/common.php index 28b3299..defdb5f 100644 --- a/common.php +++ b/common.php @@ -1,14 +1,16 @@ DB_FILENAME )); - if ($db->schema()->check(Model\DB_VERSION)) { + if ($db->schema()->check(Model\Config\DB_VERSION)) { return $db; } else { diff --git a/controllers/bookmark.php b/controllers/bookmark.php new file mode 100644 index 0000000..8d9db91 --- /dev/null +++ b/controllers/bookmark.php @@ -0,0 +1,54 @@ + $id, 'value' => $value)); +}); + +// Add new bookmark +Router\get_action('bookmark', function() { + + $id = Request\param('id'); + $menu = Request\param('menu', 'unread'); + $source = Request\param('source', 'unread'); + $offset = Request\int_param('offset', 0); + $feed_id = Request\int_param('feed_id', 0); + + Model\Item\set_bookmark_value($id, Request\int_param('value')); + + if ($source === 'show') { + Response\Redirect('?action=show&menu='.$menu.'&id='.$id); + } + + Response\Redirect('?action='.$menu.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id); +}); + +// Display bookmarks page +Router\get_action('bookmarks', function() { + + $offset = Request\int_param('offset', 0); + $nb_items = Model\Item\count_bookmarks(); + + Response\html(Template\layout('bookmarks', array( + 'order' => '', + 'direction' => '', + 'items' => Model\Item\get_bookmarks($offset, Model\Config\get('items_per_page')), + 'nb_items' => $nb_items, + 'offset' => $offset, + 'items_per_page' => Model\Config\get('items_per_page'), + 'menu' => 'bookmarks', + 'title' => t('Bookmarks').' ('.$nb_items.')' + ))); +}); diff --git a/controllers/common.php b/controllers/common.php new file mode 100644 index 0000000..6e64d89 --- /dev/null +++ b/controllers/common.php @@ -0,0 +1,68 @@ + '*', + 'img-src' => '*', + 'frame-src' => $frame_src + )); + + Response\xframe(); + Response\xss(); + Response\nosniff(); +}); + +// Javascript assets +Router\get_action('js', function() { + + $data = file_get_contents('assets/js/app.js'); + $data .= file_get_contents('assets/js/feed.js'); + $data .= file_get_contents('assets/js/item.js'); + $data .= file_get_contents('assets/js/event.js'); + $data .= file_get_contents('assets/js/nav.js'); + $data .= 'Miniflux.App.Run();'; + + Response\js($data); +}); + +// Show help +Router\get_action('show-help', function() { + + Response\html(Template\load('show_help')); +}); \ No newline at end of file diff --git a/controllers/console.php b/controllers/console.php new file mode 100644 index 0000000..9dbdbf6 --- /dev/null +++ b/controllers/console.php @@ -0,0 +1,24 @@ + @file_get_contents(DEBUG_FILENAME), + 'title' => t('Console') + ))); +}); diff --git a/controllers/feed.php b/controllers/feed.php new file mode 100644 index 0000000..5d1955f --- /dev/null +++ b/controllers/feed.php @@ -0,0 +1,276 @@ + Model\Feed\get($id), + 'errors' => array(), + 'menu' => 'feeds', + 'title' => t('Edit subscription') + ))); +}); + +// Submit edit feed form +Router\post_action('edit-feed', function() { + + $values = Request\values(); + list($valid, $errors) = Model\Feed\validate_modification($values); + + if ($valid) { + + if (Model\Feed\update($values)) { + Session\flash(t('Your subscription has been updated.')); + } + else { + Session\flash_error(t('Unable to edit your subscription.')); + } + + Response\redirect('?action=feeds'); + } + + Response\html(Template\layout('edit_feed', array( + 'values' => $values, + 'errors' => $errors, + 'menu' => 'feeds', + 'title' => t('Edit subscription') + ))); +}); + +// Disable content grabber for a feed +Router\get_action('disable-grabber-feed', function() { + + $id = Request\int_param('feed_id'); + + if ($id && Model\Feed\disable_grabber($id)) { + Session\flash(t('The content grabber is disabled successfully.')); + } + else { + Session\flash_error(t('Unable to disable the content grabber for this subscription.')); + } + + Response\redirect('?action=feeds'); +}); + +// Enable content grabber for a feed +Router\get_action('enable-grabber-feed', function() { + + $id = Request\int_param('feed_id'); + + if ($id && Model\Feed\enable_grabber($id)) { + Session\flash(t('The content grabber is enabled successfully.')); + } + else { + Session\flash_error(t('Unable to activate the content grabber for this subscription.')); + } + + Response\redirect('?action=feeds'); +}); + +// Confirmation box to disable a feed +Router\get_action('confirm-disable-feed', function() { + + $id = Request\int_param('feed_id'); + + Response\html(Template\layout('confirm_disable_feed', array( + 'feed' => Model\Feed\get($id), + 'menu' => 'feeds', + 'title' => t('Confirmation') + ))); +}); + +// Disable a feed +Router\get_action('disable-feed', function() { + + $id = Request\int_param('feed_id'); + + if ($id && Model\Feed\disable($id)) { + Session\flash(t('This subscription has been disabled successfully.')); + } + else { + Session\flash_error(t('Unable to disable this subscription.')); + } + + Response\redirect('?action=feeds'); +}); + +// Enable a feed +Router\get_action('enable-feed', function() { + + $id = Request\int_param('feed_id'); + + if ($id && Model\Feed\enable($id)) { + Session\flash(t('This subscription has been enabled successfully.')); + } + else { + Session\flash_error(t('Unable to enable this subscription.')); + } + + Response\redirect('?action=feeds'); +}); + +// Confirmation box to remove a feed +Router\get_action('confirm-remove-feed', function() { + + $id = Request\int_param('feed_id'); + + Response\html(Template\layout('confirm_remove_feed', array( + 'feed' => Model\Feed\get($id), + 'menu' => 'feeds', + 'title' => t('Confirmation') + ))); +}); + +// Remove a feed +Router\get_action('remove-feed', function() { + + $id = Request\int_param('feed_id'); + + if ($id && Model\Feed\remove($id)) { + Session\flash(t('This subscription has been removed successfully.')); + } + else { + Session\flash_error(t('Unable to remove this subscription.')); + } + + Response\redirect('?action=feeds'); +}); + +// Refresh one feed and redirect to unread items +Router\get_action('refresh-feed', function() { + + Model\Feed\refresh(Request\int_param('feed_id')); + Response\redirect('?action=unread'); +}); + +// Ajax call to refresh one feed +Router\post_action('refresh-feed', function() { + + $id = Request\int_param('feed_id', 0); + $result = Model\Feed\refresh($id); + + Response\json(array('feed_id' => $id, 'result' => $result)); +}); + +// Display all feeds +Router\get_action('feeds', function() { + + if (! Request\int_param('disable_empty_feeds_check')) { + + $empty_feeds = Model\Feed\get_all_empty(); + + if (! empty($empty_feeds)) { + + $listing = array(); + + foreach ($empty_feeds as &$feed) { + $listing[] = '"'.$feed['title'].'"'; + } + + $message = t( + 'There is %d empty feeds, there is maybe an error: %s...', + count($empty_feeds), + implode(', ', array_slice($listing, 0, 5)) + ); + + Session\flash_error($message); + } + } + + Response\html(Template\layout('feeds', array( + 'feeds' => Model\Feed\get_all(), + 'nothing_to_read' => Request\int_param('nothing_to_read'), + 'menu' => 'feeds', + 'title' => t('Subscriptions') + ))); +}); + +// Display form to add one feed +Router\get_action('add', function() { + + Response\html(Template\layout('add', array( + 'values' => array(), + 'errors' => array(), + 'menu' => 'feeds', + 'title' => t('New subscription') + ))); +}); + +// Add a feed with the form or directly from the url, it can be used by a bookmarklet by example +Router\action('subscribe', function() { + + if (Request\param('url')) { + $values = array(); + $url = Request\param('url'); + } + else { + $values = Request\values(); + $url = isset($values['url']) ? $values['url'] : ''; + } + + $values += array('download_content' => 0); + $url = trim($url); + $result = Model\Feed\create($url, $values['download_content']); + + if ($result) { + Session\flash(t('Subscription added successfully.')); + Response\redirect('?action=feeds'); + } + else { + Session\flash_error(t('Unable to find a subscription.')); + } + + Response\html(Template\layout('add', array( + 'values' => array('url' => $url), + 'menu' => 'feeds', + 'title' => t('Subscriptions') + ))); +}); + +// OPML export +Router\get_action('export', function() { + + Response\force_download('feeds.opml'); + Response\xml(Model\Feed\export_opml()); +}); + +// 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() { + + if (Model\Feed\import_opml(Request\file_content('file'))) { + + Session\flash(t('Your feeds have been imported.')); + Response\redirect('?action=feeds&disable_empty_feeds_check=1'); + } + else { + + Session\flash_error(t('Unable to import your OPML file.')); + Response\redirect('?action=import'); + } +}); diff --git a/controllers/history.php b/controllers/history.php new file mode 100644 index 0000000..205adf2 --- /dev/null +++ b/controllers/history.php @@ -0,0 +1,47 @@ + Model\Item\get_all( + 'read', + $offset, + Model\Config\get('items_per_page'), + 'updated', + Model\Config\get('items_sorting_direction') + ), + 'order' => '', + 'direction' => '', + 'nb_items' => $nb_items, + 'offset' => $offset, + 'items_per_page' => Model\Config\get('items_per_page'), + 'menu' => 'history', + 'title' => t('History').' ('.$nb_items.')' + ))); +}); + +// Confirmation box to flush history +Router\get_action('confirm-flush-history', function() { + + Response\html(Template\layout('confirm_flush_items', array( + 'menu' => 'history', + 'title' => t('Confirmation') + ))); +}); + +// Flush history +Router\get_action('flush-history', function() { + + Model\Item\mark_all_as_removed(); + Response\redirect('?action=history'); +}); diff --git a/controllers/item.php b/controllers/item.php new file mode 100644 index 0000000..c80cf5e --- /dev/null +++ b/controllers/item.php @@ -0,0 +1,158 @@ + isset($nb_unread_items) ? $nb_unread_items : null, + 'item' => $item, + 'feed' => $feed, + 'item_nav' => isset($nav) ? $nav : null, + 'menu' => $menu, + 'title' => $item['title'] + ))); +}); + +// Display feed items page +Router\get_action('feed-items', function() { + + $feed_id = Request\int_param('feed_id', 0); + $offset = Request\int_param('offset', 0); + $nb_items = Model\Item\count_by_feed($feed_id); + $feed = Model\Feed\get($feed_id); + $order = Request\param('order', 'updated'); + $direction = Request\param('direction', Model\Config\get('items_sorting_direction')); + $items = Model\Item\get_all_by_feed($feed_id, $offset, Model\Config\get('items_per_page'), $order, $direction); + + Response\html(Template\layout('feed_items', array( + 'order' => $order, + 'direction' => $direction, + 'feed' => $feed, + 'items' => $items, + 'nb_items' => $nb_items, + 'offset' => $offset, + 'items_per_page' => Model\Config\get('items_per_page'), + 'menu' => 'feed-items', + 'title' => '('.$nb_items.') '.$feed['title'] + ))); +}); + +// Ajax call to download an item (fetch the full content from the original website) +Router\post_action('download-item', function() { + + Response\json(Model\Item\download_content_id(Request\param('id'))); +}); + +// Ajax call change item status +Router\post_action('change-item-status', function() { + + $id = Request\param('id'); + + Response\json(array( + 'item_id' => $id, + 'status' => Model\Item\switch_status($id) + )); +}); + +// Ajax call to mark item read +Router\post_action('mark-item-read', function() { + + Model\Item\set_read(Request\param('id')); + Response\json(array('Ok')); +}); + +// Ajax call to mark item unread +Router\post_action('mark-item-unread', function() { + + Model\Item\set_unread(Request\param('id')); + Response\json(array('Ok')); +}); + +// Mark all unread items as read +Router\get_action('mark-as-read', function() { + + Model\Item\mark_all_as_read(); + Response\redirect('?action=unread'); +}); + +// Mark all unread items as read for a specific feed +Router\get_action('mark-feed-as-read', function() { + + Model\Item\mark_feed_as_read(Request\int_param('feed_id')); + Response\redirect('?action=unread'); +}); + +// Mark sent items id as read (Ajax request) +Router\post_action('mark-items-as-read', function(){ + + Model\Item\mark_items_as_read(Request\values()); + Response\json(array('OK')); +}); + +// Mark item as read and redirect to the listing page +Router\get_action('mark-item-read', function() { + + $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); +}); + +// Mark item as unread and redirect to the listing page +Router\get_action('mark-item-unread', function() { + + $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); +}); + +// Mark item as removed and redirect to the listing page +Router\get_action('mark-item-removed', function() { + + $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); + + Response\Redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id); +}); diff --git a/controllers/unread.php b/controllers/unread.php new file mode 100644 index 0000000..753401e --- /dev/null +++ b/controllers/unread.php @@ -0,0 +1,33 @@ + $order, + 'direction' => $direction, + 'items' => $items, + 'nb_items' => $nb_items, + 'nb_unread_items' => $nb_items, + 'offset' => $offset, + 'items_per_page' => Model\Config\get('items_per_page'), + 'title' => 'Miniflux ('.$nb_items.')', + 'menu' => 'unread' + ))); +}); diff --git a/controllers/user.php b/controllers/user.php new file mode 100644 index 0000000..d161bf2 --- /dev/null +++ b/controllers/user.php @@ -0,0 +1,147 @@ + Model\Config\get('auth_google_token') !== '', + 'mozilla_auth_enable' => Model\Config\get('auth_mozilla_token') !== '', + 'errors' => array(), + 'values' => array() + ))); +}); + +// Check credentials and redirect to unread items +Router\post_action('login', function() { + + $values = Request\values(); + list($valid, $errors) = Model\User\validate_login($values); + + if ($valid) Response\redirect('?action=unread'); + + Response\html(Template\load('login', array( + 'google_auth_enable' => Model\Config\get('auth_google_token') !== '', + 'mozilla_auth_enable' => Model\Config\get('auth_mozilla_token') !== '', + 'errors' => $errors, + 'values' => $values + ))); +}); + +// Link to a Google Account (redirect) +Router\get_action('google-redirect-link', function() { + + require 'vendor/PicoTools/AuthProvider.php'; + Response\Redirect(AuthProvider\google_get_url(Helper\get_current_base_url(), '?action=google-link')); +}); + +// Link to a Google Account (association) +Router\get_action('google-link', function() { + + require 'vendor/PicoTools/AuthProvider.php'; + + list($valid, $token) = AuthProvider\google_validate(); + + if ($valid) { + Model\Config\save_auth_token('google', $token); + Session\flash(t('Your Google Account is linked to Miniflux.')); + } + else { + Session\flash_error(t('Unable to link Miniflux to your Google Account.')); + } + + Response\redirect('?action=config'); +}); + +// Authenticate with a Google Account (redirect) +Router\get_action('google-redirect-auth', function() { + + require 'vendor/PicoTools/AuthProvider.php'; + Response\Redirect(AuthProvider\google_get_url(Helper\get_current_base_url(), '?action=google-auth')); +}); + +// Authenticate with a Google Account (callback url) +Router\get_action('google-auth', function() { + + require 'vendor/PicoTools/AuthProvider.php'; + + list($valid, $token) = AuthProvider\google_validate(); + + if ($valid && $token === Model\Config\get('auth_google_token')) { + + $_SESSION['user'] = array( + 'username' => Model\Config\get('username'), + 'language' => Model\Config\get('language'), + ); + + Response\redirect('?action=unread'); + } + else { + + Response\html(Template\load('login', array( + 'google_auth_enable' => Model\Config\get('auth_google_token') !== '', + 'mozilla_auth_enable' => Model\Config\get('auth_mozilla_token') !== '', + 'errors' => array('login' => t('Unable to authenticate with Google')), + 'values' => array() + ))); + } +}); + +// Authenticate with a Mozilla Persona (ajax check) +Router\post_action('mozilla-auth', function() { + + require 'vendor/PicoTools/AuthProvider.php'; + + list($valid, $token) = AuthProvider\mozilla_validate(Request\value('token')); + + if ($valid && $token === Model\Config\get('auth_mozilla_token')) { + + $_SESSION['user'] = array( + 'username' => Model\Config\get('username'), + 'language' => Model\Config\get('language'), + ); + + Response\text('?action=unread'); + } + else { + Response\text("?action=login"); + } +}); + +// Link Miniflux to a Mozilla Account (ajax check) +Router\post_action('mozilla-link', function() { + + require 'vendor/PicoTools/AuthProvider.php'; + + list($valid, $token) = AuthProvider\mozilla_validate(Request\value('token')); + + if ($valid) { + Model\Config\save_auth_token('mozilla', $token); + Session\flash(t('Your Mozilla Persona Account is linked to Miniflux.')); + } + else { + Session\flash_error(t('Unable to link Miniflux to your Mozilla Persona Account.')); + } + + Response\text("?action=config"); +}); + +// Remove account link +Router\get_action('unlink-account-provider', function() { + Model\Config\remove_auth_token(Request\param('type')); + Response\redirect('?action=config'); +}); diff --git a/cronjob.php b/cronjob.php index 1c1632a..86bed17 100644 --- a/cronjob.php +++ b/cronjob.php @@ -15,15 +15,15 @@ else { $options = $_GET; } -$limit = ! empty($options['limit']) && ctype_digit($options['limit']) ? (int) $options['limit'] : Model\LIMIT_ALL; +$limit = ! empty($options['limit']) && ctype_digit($options['limit']) ? (int) $options['limit'] : Model\Feed\LIMIT_ALL; $update_interval = ! empty($options['update-interval']) && ctype_digit($options['update-interval']) ? (int) $options['update-interval'] : null; $call_interval = ! empty($options['call-interval']) && ctype_digit($options['call-interval']) ? (int) $options['call-interval'] : null; -if ($update_interval !== null && $call_interval !== null && $limit === Model\LIMIT_ALL && $update_interval >= $call_interval) { +if ($update_interval !== null && $call_interval !== null && $limit === Model\Feed\LIMIT_ALL && $update_interval >= $call_interval) { $feeds_count = \PicoTools\singleton('db')->table('feeds')->count(); $limit = ceil($feeds_count / ($update_interval / $call_interval)); } -Model\update_feeds($limit); -Model\write_debug(); +Model\Feed\refresh_all($limit); +Model\Config\write_debug(); diff --git a/examples/api_client.php b/examples/api_client.php index f8e1ec0..9ce12e8 100644 --- a/examples/api_client.php +++ b/examples/api_client.php @@ -4,8 +4,8 @@ require '../vendor/JsonRPC/Client.php'; use JsonRPC\Client; -$client = new Client('http://webapps/miniflux/jsonrpc.php'); -$client->authentication('admin', 'd4i/Tanb55426mi'); +$client = new Client('http://127.0.0.1:8000/jsonrpc.php'); +$client->authentication('admin', 'EF8goVO/8YUxcyt'); $result = $client->execute('feed.create', array('url' => 'http://bbc.co.uk/news')); var_dump($result); diff --git a/feed.php b/feed.php index 8526aec..c467b0c 100644 --- a/feed.php +++ b/feed.php @@ -11,7 +11,7 @@ use PicoFarad\Request; use PicoFeed\Writers\Atom; // Check token -$feed_token = Model\get_config_value('feed_token'); +$feed_token = Model\Config\get('feed_token'); $request_token = Request\param('token'); if ($feed_token !== $request_token) { @@ -19,7 +19,7 @@ if ($feed_token !== $request_token) { } // Load translations -$language = Model\get_config_value('language') ?: 'en_US'; +$language = Model\Config\get('language') ?: 'en_US'; if ($language !== 'en_US') PicoTools\Translator\load($language); // Build Feed @@ -28,11 +28,11 @@ $writer->title = t('Bookmarks').' - Miniflux'; $writer->site_url = Helper\get_current_base_url(); $writer->feed_url = $writer->site_url.'feed.php?token='.urlencode($feed_token); -$bookmarks = Model\get_bookmarks(); +$bookmarks = Model\Item\get_bookmarks(); foreach ($bookmarks as $bookmark) { - $article = Model\get_item($bookmark['id']); + $article = Model\Item\get($bookmark['id']); $writer->items[] = array( 'id' => $article['id'], diff --git a/helpers.php b/helpers.php index a0a0229..3ef7495 100644 --- a/helpers.php +++ b/helpers.php @@ -2,10 +2,9 @@ namespace Helper; - function css() { - $theme = \Model\get_config_value('theme'); + $theme = \Model\Config\get('theme'); if ($theme !== 'original') { @@ -17,4 +16,4 @@ function css() } return 'assets/css/app.css?version='.filemtime('assets/css/app.css'); -} \ No newline at end of file +} diff --git a/index.php b/index.php index 85e5418..75f9efc 100644 --- a/index.php +++ b/index.php @@ -1,892 +1,12 @@ '*', - 'img-src' => '*', - 'frame-src' => $frame_src - )); - - Response\xframe(); - Response\xss(); - Response\nosniff(); -}); - - -// Javascript assets -Router\get_action('js', function() { - - $data = file_get_contents('assets/js/app.js'); - $data .= file_get_contents('assets/js/feed.js'); - $data .= file_get_contents('assets/js/item.js'); - $data .= file_get_contents('assets/js/event.js'); - $data .= file_get_contents('assets/js/nav.js'); - $data .= 'Miniflux.App.Run();'; - - Response\js($data); -}); - - -// Logout and destroy session -Router\get_action('logout', function() { - - Session\close(); - Response\redirect('?action=login'); -}); - - -// Display form login -Router\get_action('login', function() { - - if (isset($_SESSION['user'])) Response\redirect('?action=unread'); - - Response\html(Template\load('login', array( - 'google_auth_enable' => Model\get_config_value('auth_google_token') !== '', - 'mozilla_auth_enable' => Model\get_config_value('auth_mozilla_token') !== '', - 'errors' => array(), - 'values' => array() - ))); -}); - - -// Check credentials and redirect to unread items -Router\post_action('login', function() { - - $values = Request\values(); - list($valid, $errors) = Model\validate_login($values); - - if ($valid) Response\redirect('?action=unread'); - - Response\html(Template\load('login', array( - 'google_auth_enable' => Model\get_config_value('auth_google_token') !== '', - 'mozilla_auth_enable' => Model\get_config_value('auth_mozilla_token') !== '', - 'errors' => $errors, - 'values' => $values - ))); -}); - - -// Show help -Router\get_action('show-help', function() { - - Response\html(Template\load('show_help')); -}); - - -// Show item -Router\get_action('show', function() { - - $id = Request\param('id'); - $menu = Request\param('menu'); - $item = Model\get_item($id); - $feed = Model\get_feed($item['feed_id']); - - Model\set_item_read($id); - - switch ($menu) { - case 'unread': - $nav = Model\get_nav_item($item); - $nb_unread_items = Model\count_items('unread'); - break; - case 'history': - $nav = Model\get_nav_item($item, array('read')); - break; - case 'feed-items': - $nav = Model\get_nav_item($item, array('unread', 'read'), array(1, 0), $item['feed_id']); - break; - case 'bookmarks': - $nav = Model\get_nav_item($item, array('unread', 'read'), array(1)); - break; - } - - Response\html(Template\layout('show_item', array( - 'nb_unread_items' => isset($nb_unread_items) ? $nb_unread_items : null, - 'item' => $item, - 'feed' => $feed, - 'item_nav' => isset($nav) ? $nav : null, - 'menu' => $menu, - 'title' => $item['title'] - ))); -}); - - -// Mark item as read and redirect to the listing page -Router\get_action('mark-item-read', function() { - - $id = Request\param('id'); - $redirect = Request\param('redirect', 'unread'); - $offset = Request\int_param('offset', 0); - $feed_id = Request\int_param('feed_id', 0); - - Model\set_item_read($id); - - Response\Redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id); -}); - - -// Mark item as unread and redirect to the listing page -Router\get_action('mark-item-unread', function() { - - $id = Request\param('id'); - $redirect = Request\param('redirect', 'history'); - $offset = Request\int_param('offset', 0); - $feed_id = Request\int_param('feed_id', 0); - - Model\set_item_unread($id); - - Response\Redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id); -}); - - -// Mark item as removed and redirect to the listing page -Router\get_action('mark-item-removed', function() { - - $id = Request\param('id'); - $redirect = Request\param('redirect', 'history'); - $offset = Request\int_param('offset', 0); - $feed_id = Request\int_param('feed_id', 0); - - Model\set_item_removed($id); - - Response\Redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id); -}); - - -// Ajax call to download an item (fetch the full content from the original website) -Router\post_action('download-item', function() { - - Response\json(Model\download_item(Request\param('id'))); -}); - - -// Ajax call to mark item read -Router\post_action('mark-item-read', function() { - - $id = Request\param('id'); - Model\set_item_read($id); - Response\json(array('Ok')); -}); - - -// Ajax call to mark item unread -Router\post_action('mark-item-unread', function() { - - $id = Request\param('id'); - Model\set_item_unread($id); - Response\json(array('Ok')); -}); - - -// Ajax call change item status -Router\post_action('change-item-status', function() { - - $id = Request\param('id'); - - Response\json(array( - 'item_id' => $id, - 'status' => Model\switch_item_status($id) - )); -}); - - -// Ajax call to add or remove a bookmark -Router\post_action('bookmark', function() { - - $id = Request\param('id'); - $value = Request\int_param('value'); - - Model\set_bookmark_value($id, $value); - - Response\json(array('id' => $id, 'value' => $value)); -}); - - -// Add new bookmark -Router\get_action('bookmark', function() { - - $id = Request\param('id'); - $menu = Request\param('menu', 'unread'); - $source = Request\param('source', 'unread'); - $offset = Request\int_param('offset', 0); - $feed_id = Request\int_param('feed_id', 0); - - Model\set_bookmark_value($id, Request\int_param('value')); - - if ($source === 'show') { - Response\Redirect('?action=show&menu='.$menu.'&id='.$id); - } - - Response\Redirect('?action='.$menu.'&offset='.$offset.'&feed_id='.$feed_id.'#item-'.$id); -}); - - -// Display history page -Router\get_action('history', function() { - - $offset = Request\int_param('offset', 0); - $nb_items = Model\count_items('read'); - - Response\html(Template\layout('history', array( - 'items' => Model\get_items( - 'read', - $offset, - Model\get_config_value('items_per_page'), - 'updated', - Model\get_config_value('items_sorting_direction') - ), - 'order' => '', - 'direction' => '', - 'nb_items' => $nb_items, - 'offset' => $offset, - 'items_per_page' => Model\get_config_value('items_per_page'), - 'menu' => 'history', - 'title' => t('History').' ('.$nb_items.')' - ))); -}); - - -// Display feed items page -Router\get_action('feed-items', function() { - - $feed_id = Request\int_param('feed_id', 0); - $offset = Request\int_param('offset', 0); - $nb_items = Model\count_feed_items($feed_id); - $feed = Model\get_feed($feed_id); - $order = Request\param('order', 'updated'); - $direction = Request\param('direction', Model\get_config_value('items_sorting_direction')); - $items = Model\get_feed_items($feed_id, $offset, Model\get_config_value('items_per_page'), $order, $direction); - - Response\html(Template\layout('feed_items', array( - 'order' => $order, - 'direction' => $direction, - 'feed' => $feed, - 'items' => $items, - 'nb_items' => $nb_items, - 'offset' => $offset, - 'items_per_page' => Model\get_config_value('items_per_page'), - 'menu' => 'feed-items', - 'title' => '('.$nb_items.') '.$feed['title'] - ))); -}); - - -// Display bookmarks page -Router\get_action('bookmarks', function() { - - $offset = Request\int_param('offset', 0); - $nb_items = Model\count_bookmarks(); - - Response\html(Template\layout('bookmarks', array( - 'order' => '', - 'direction' => '', - 'items' => Model\get_bookmarks($offset, Model\get_config_value('items_per_page')), - 'nb_items' => $nb_items, - 'offset' => $offset, - 'items_per_page' => Model\get_config_value('items_per_page'), - 'menu' => 'bookmarks', - 'title' => t('Bookmarks').' ('.$nb_items.')' - ))); -}); - - -// Mark all unread items as read -Router\get_action('mark-as-read', function() { - - Model\mark_as_read(); - Response\redirect('?action=unread'); -}); - - -// Mark all unread items as read for a specific feed -Router\get_action('mark-feed-as-read', function() { - - $feed_id = Request\int_param('feed_id'); - Model\mark_feed_as_read($feed_id); - Response\redirect('?action=unread'); -}); - - -// Mark sent items id as read (Ajax request) -Router\post_action('mark-items-as-read', function(){ - - Model\mark_items_as_read(Request\values()); - Response\json(array('OK')); -}); - - -// Confirmation box to flush history -Router\get_action('confirm-flush-history', function() { - - Response\html(Template\layout('confirm_flush_items', array( - 'menu' => 'history', - 'title' => t('Confirmation') - ))); -}); - - -// Flush history -Router\get_action('flush-history', function() { - - Model\mark_as_removed(); - Response\redirect('?action=history'); -}); - - -// Refresh all feeds, used when Javascript is disabled -Router\get_action('refresh-all', function() { - - Model\update_feeds(); - Session\flash(t('Your subscriptions are updated')); - Response\redirect('?action=unread'); -}); - - -// Edit feed form -Router\get_action('edit-feed', function() { - - $id = Request\int_param('feed_id'); - - Response\html(Template\layout('edit_feed', array( - 'values' => Model\get_feed($id), - 'errors' => array(), - 'menu' => 'feeds', - 'title' => t('Edit subscription') - ))); -}); - - -// Submit edit feed form -Router\post_action('edit-feed', function() { - - $values = Request\values(); - list($valid, $errors) = Model\validate_feed_modification($values); - - if ($valid) { - - if (Model\save_feed($values)) { - Session\flash(t('Your subscription has been updated.')); - } - else { - Session\flash_error(t('Unable to edit your subscription.')); - } - - Response\redirect('?action=feeds'); - } - - Response\html(Template\layout('edit_feed', array( - 'values' => $values, - 'errors' => $errors, - 'menu' => 'feeds', - 'title' => t('Edit subscription') - ))); -}); - - -// Disable content grabber for a feed -Router\get_action('disable-grabber-feed', function() { - - $id = Request\int_param('feed_id'); - - if ($id && Model\disable_grabber_feed($id)) { - Session\flash(t('The content grabber is disabled successfully.')); - } - else { - Session\flash_error(t('Unable to disable the content grabber for this subscription.')); - } - - Response\redirect('?action=feeds'); -}); - - -// Enable content grabber for a feed -Router\get_action('enable-grabber-feed', function() { - - $id = Request\int_param('feed_id'); - - if ($id && Model\enable_grabber_feed($id)) { - Session\flash(t('The content grabber is enabled successfully.')); - } - else { - Session\flash_error(t('Unable to activate the content grabber for this subscription.')); - } - - Response\redirect('?action=feeds'); -}); - - -// Confirmation box to disable a feed -Router\get_action('confirm-disable-feed', function() { - - $id = Request\int_param('feed_id'); - - Response\html(Template\layout('confirm_disable_feed', array( - 'feed' => Model\get_feed($id), - 'menu' => 'feeds', - 'title' => t('Confirmation') - ))); -}); - - -// Disable a feed -Router\get_action('disable-feed', function() { - - $id = Request\int_param('feed_id'); - - if ($id && Model\disable_feed($id)) { - Session\flash(t('This subscription has been disabled successfully.')); - } - else { - Session\flash_error(t('Unable to disable this subscription.')); - } - - Response\redirect('?action=feeds'); -}); - - -// Enable a feed -Router\get_action('enable-feed', function() { - - $id = Request\int_param('feed_id'); - - if ($id && Model\enable_feed($id)) { - Session\flash(t('This subscription has been enabled successfully.')); - } - else { - Session\flash_error(t('Unable to enable this subscription.')); - } - - Response\redirect('?action=feeds'); -}); - - -// Confirmation box to remove a feed -Router\get_action('confirm-remove-feed', function() { - - $id = Request\int_param('feed_id'); - - Response\html(Template\layout('confirm_remove_feed', array( - 'feed' => Model\get_feed($id), - 'menu' => 'feeds', - 'title' => t('Confirmation') - ))); -}); - - -// Remove a feed -Router\get_action('remove-feed', function() { - - $id = Request\int_param('feed_id'); - - if ($id && Model\remove_feed($id)) { - Session\flash(t('This subscription has been removed successfully.')); - } - else { - Session\flash_error(t('Unable to remove this subscription.')); - } - - Response\redirect('?action=feeds'); -}); - - -// Refresh one feed and redirect to unread items -Router\get_action('refresh-feed', function() { - - $id = Request\int_param('feed_id'); - if ($id) Model\update_feed($id); - Response\redirect('?action=unread'); -}); - - -// Ajax call to refresh one feed -Router\post_action('refresh-feed', function() { - - $id = Request\int_param('feed_id', 0); - - if ($id) { - $result = Model\update_feed($id); - } - - Response\json(array('feed_id' => $id, 'result' => $result)); -}); - - -// Display all feeds -Router\get_action('feeds', function() { - - if (! Request\int_param('disable_empty_feeds_check')) { - - $empty_feeds = Model\get_empty_feeds(); - - if (! empty($empty_feeds)) { - - $listing = array(); - - foreach ($empty_feeds as &$feed) { - $listing[] = '"'.$feed['title'].'"'; - } - - $message = t( - 'There is %d empty feeds, there is maybe an error: %s...', - count($empty_feeds), - implode(', ', array_slice($listing, 0, 5)) - ); - - Session\flash_error($message); - } - } - - Response\html(Template\layout('feeds', array( - 'feeds' => Model\get_feeds(), - 'nothing_to_read' => Request\int_param('nothing_to_read'), - 'menu' => 'feeds', - 'title' => t('Subscriptions') - ))); -}); - - -// Display form to add one feed -Router\get_action('add', function() { - - Response\html(Template\layout('add', array( - 'values' => array(), - 'errors' => array(), - 'menu' => 'feeds', - 'title' => t('New subscription') - ))); -}); - - -// Add a feed with the form or directly from the url, it can be used by a bookmarklet by example -Router\action('subscribe', function() { - - if (Request\param('url')) { - $values = array(); - $url = Request\param('url'); - } - else { - $values = Request\values(); - $url = isset($values['url']) ? $values['url'] : ''; - } - - $values += array('download_content' => 0); - $url = trim($url); - $result = Model\import_feed($url, $values['download_content']); - - if ($result) { - - Session\flash(t('Subscription added successfully.')); - Response\redirect('?action=feeds'); - } - else { - - Session\flash_error(t('Unable to find a subscription.')); - } - - Response\html(Template\layout('add', array( - 'values' => array('url' => $url), - 'menu' => 'feeds', - 'title' => t('Subscriptions') - ))); -}); - - -// OPML export -Router\get_action('export', function() { - - Response\force_download('feeds.opml'); - Response\xml(Model\export_feeds()); -}); - - -// 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() { - - if (Model\import_feeds(Request\file_content('file'))) { - - Session\flash(t('Your feeds have been imported.')); - Response\redirect('?action=feeds&disable_empty_feeds_check=1'); - } - else { - - Session\flash_error(t('Unable to import your OPML file.')); - Response\redirect('?action=import'); - } -}); - - -// Re-generate tokens -Router\get_action('generate-tokens', function() { - - Model\new_tokens(); - Response\redirect('?action=config#api'); -}); - - -// Optimize the database manually -Router\get_action('optimize-db', function() { - - \PicoTools\singleton('db')->getConnection()->exec('VACUUM'); - Response\redirect('?action=config'); -}); - - -// Download the compressed database -Router\get_action('download-db', function() { - - Response\force_download('db.sqlite.gz'); - Response\binary(gzencode(file_get_contents(DB_FILENAME))); -}); - - -// Flush console messages -Router\get_action('flush-console', function() { - - @unlink(DEBUG_FILENAME); - Response\redirect('?action=console'); -}); - - -// Display console -Router\get_action('console', function() { - - Response\html(Template\layout('console', array( - 'content' => @file_get_contents(DEBUG_FILENAME), - 'title' => t('Console') - ))); -}); - - -// Display preferences page -Router\get_action('config', function() { - - Response\html(Template\layout('config', array( - 'errors' => array(), - 'values' => Model\get_config(), - 'db_size' => filesize(DB_FILENAME), - 'languages' => Model\get_languages(), - 'autoflush_options' => Model\get_autoflush_options(), - 'paging_options' => Model\get_paging_options(), - 'theme_options' => Model\get_themes(), - 'sorting_options' => Model\get_sorting_directions(), - 'menu' => 'config', - 'title' => t('Preferences') - ))); -}); - - -// Update preferences -Router\post_action('config', function() { - - $values = Request\values() + array('nocontent' => 0); - list($valid, $errors) = Model\validate_config_update($values); - - if ($valid) { - - if (Model\save_config($values)) { - Session\flash(t('Your preferences are updated.')); - } - else { - Session\flash_error(t('Unable to update your preferences.')); - } - - Response\redirect('?action=config'); - } - - Response\html(Template\layout('config', array( - 'errors' => $errors, - 'values' => $values, - 'db_size' => filesize(DB_FILENAME), - 'languages' => Model\get_languages(), - 'autoflush_options' => Model\get_autoflush_options(), - 'paging_options' => Model\get_paging_options(), - 'theme_options' => Model\get_themes(), - 'sorting_options' => Model\get_sorting_directions(), - 'menu' => 'config', - 'title' => t('Preferences') - ))); -}); - - -// Link to a Google Account (redirect) -Router\get_action('google-redirect-link', function() { - - require 'vendor/PicoTools/AuthProvider.php'; - Response\Redirect(AuthProvider\google_get_url(Helper\get_current_base_url(), '?action=google-link')); -}); - - -// Link to a Google Account (association) -Router\get_action('google-link', function() { - - require 'vendor/PicoTools/AuthProvider.php'; - - list($valid, $token) = AuthProvider\google_validate(); - - if ($valid) { - Model\save_auth_token('google', $token); - Session\flash(t('Your Google Account is linked to Miniflux.')); - } - else { - Session\flash_error(t('Unable to link Miniflux to your Google Account.')); - } - - Response\redirect('?action=config'); -}); - - -// Authenticate with a Google Account (redirect) -Router\get_action('google-redirect-auth', function() { - - require 'vendor/PicoTools/AuthProvider.php'; - Response\Redirect(AuthProvider\google_get_url(Helper\get_current_base_url(), '?action=google-auth')); -}); - - -// Authenticate with a Google Account (callback url) -Router\get_action('google-auth', function() { - - require 'vendor/PicoTools/AuthProvider.php'; - - list($valid, $token) = AuthProvider\google_validate(); - - if ($valid && $token === Model\get_config_value('auth_google_token')) { - - $_SESSION['user'] = array( - 'username' => Model\get_config_value('username'), - 'language' => Model\get_config_value('language'), - ); - - Response\redirect('?action=unread'); - } - else { - - Response\html(Template\load('login', array( - 'google_auth_enable' => Model\get_config_value('auth_google_token') !== '', - 'mozilla_auth_enable' => Model\get_config_value('auth_mozilla_token') !== '', - 'errors' => array('login' => t('Unable to authenticate with Google')), - 'values' => array() - ))); - } -}); - - -// Authenticate with a Mozilla Persona (ajax check) -Router\post_action('mozilla-auth', function() { - - require 'vendor/PicoTools/AuthProvider.php'; - - list($valid, $token) = AuthProvider\mozilla_validate(Request\value('token')); - - if ($valid && $token === Model\get_config_value('auth_mozilla_token')) { - - $_SESSION['user'] = array( - 'username' => Model\get_config_value('username'), - 'language' => Model\get_config_value('language'), - ); - - Response\text('?action=unread'); - } - else { - Response\text("?action=login"); - } -}); - - -// Link Miniflux to a Mozilla Account (ajax check) -Router\post_action('mozilla-link', function() { - - require 'vendor/PicoTools/AuthProvider.php'; - - list($valid, $token) = AuthProvider\mozilla_validate(Request\value('token')); - - if ($valid) { - Model\save_auth_token('mozilla', $token); - Session\flash(t('Your Mozilla Persona Account is linked to Miniflux.')); - } - else { - Session\flash_error(t('Unable to link Miniflux to your Mozilla Persona Account.')); - } - - Response\text("?action=config"); -}); - - -// Remove account link -Router\get_action('unlink-account-provider', function() { - Model\remove_auth_token(Request\param('type')); - Response\redirect('?action=config'); -}); - - -// Display unread items -Router\notfound(function() { - - Model\autoflush(); - - $order = Request\param('order', 'updated'); - $direction = Request\param('direction', Model\get_config_value('items_sorting_direction')); - $offset = Request\int_param('offset', 0); - $items = Model\get_items('unread', $offset, Model\get_config_value('items_per_page'), $order, $direction); - $nb_items = Model\count_items('unread'); - - if ($nb_items === 0) Response\redirect('?action=feeds¬hing_to_read=1'); - - Response\html(Template\layout('unread_items', array( - 'order' => $order, - 'direction' => $direction, - 'items' => $items, - 'nb_items' => $nb_items, - 'nb_unread_items' => $nb_items, - 'offset' => $offset, - 'items_per_page' => Model\get_config_value('items_per_page'), - 'title' => 'Miniflux ('.$nb_items.')', - 'menu' => 'unread' - ))); -}); +require 'check_setup.php'; +require 'controllers/common.php'; +require 'controllers/console.php'; +require 'controllers/user.php'; +require 'controllers/config.php'; +require 'controllers/item.php'; +require 'controllers/history.php'; +require 'controllers/bookmark.php'; +require 'controllers/feed.php'; +require 'controllers/unread.php'; diff --git a/jsonrpc.php b/jsonrpc.php index d600481..b36f4e3 100644 --- a/jsonrpc.php +++ b/jsonrpc.php @@ -7,26 +7,26 @@ use JsonRPC\Server; $server = new Server; $server->authentication(array( - Model\get_config_value('username') => Model\get_config_value('api_token') + \Model\Config\get('username') => \Model\Config\get('api_token') )); // Get all feeds $server->register('feed.list', function () { - return Model\get_feeds(); + return Model\Feed\get_all(); }); // Get one feed $server->register('feed.info', function ($feed_id) { - return Model\get_feed($feed_id); + return Model\Feed\get($feed_id); }); // Add a new feed $server->register('feed.create', function($url) { - $result = Model\import_feed($url); - Model\write_debug(); + $result = Model\Feed\create($url); + Model\Config\write_debug(); return $result; }); @@ -34,133 +34,133 @@ $server->register('feed.create', function($url) { // Delete a feed $server->register('feed.delete', function($feed_id) { - return Model\remove_feed($feed_id); + return Model\Feed\remove($feed_id); }); // Delete all feeds $server->register('feed.delete_all', function() { - return Model\remove_feeds(); + return Model\Feed\remove_all(); }); // Enable a feed $server->register('feed.enable', function($feed_id) { - return Model\enable_feed($feed_id); + return Model\Feed\enable($feed_id); }); // Disable a feed $server->register('feed.disable', function($feed_id) { - return Model\disable_feed($feed_id); + return Model\Feed\disable($feed_id); }); // Update a feed $server->register('feed.update', function($feed_id) { - return Model\update_feed($feed_id); + return Model\Feed\refresh($feed_id); }); // Get all items for a specific feed $server->register('item.feed.list', function ($feed_id, $offset = null, $limit = null) { - return Model\get_feed_items($feed_id, $offset, $limit); + return Model\Item\get_all_by_feed($feed_id, $offset, $limit); }); // Count all feed items $server->register('item.feed.count', function ($feed_id) { - return Model\count_feed_items($feed_id); + return Model\Item\count_by_feed($feed_id); }); // Get all bookmark items $server->register('item.bookmark.list', function ($offset = null, $limit = null) { - return Model\get_bookmarks($offset, $limit); + return Model\Item\get_bookmarks($offset, $limit); }); // Count bookmarks $server->register('item.bookmark.count', function () { - return Model\count_bookmarks(); + return Model\Item\count_bookmarks(); }); // Add a bookmark $server->register('item.bookmark.create', function ($item_id) { - return Model\set_bookmark_value($item_id, 1); + return Model\Item\set_bookmark_value($item_id, 1); }); // Remove a bookmark $server->register('item.bookmark.delete', function ($item_id) { - return Model\set_bookmark_value($item_id, 0); + return Model\Item\set_bookmark_value($item_id, 0); }); // Get all unread items $server->register('item.list_unread', function ($offset = null, $limit = null) { - return Model\get_items('unread', $offset, $limit); + return Model\Item\get_all('unread', $offset, $limit); }); // Count all unread items $server->register('item.count_unread', function () { - return Model\count_items('unread'); + return Model\Item\count_by_status('unread'); }); // Get all read items $server->register('item.list_read', function ($offset = null, $limit = null) { - return Model\get_items('read', $offset, $limit); + return Model\Item\get_all('read', $offset, $limit); }); // Count all read items $server->register('item.count_read', function () { - return Model\count_items('read'); + return Model\Item\count_by_status('read'); }); // Get one item $server->register('item.info', function ($item_id) { - return Model\get_item($item_id); + return Model\Item\get($item_id); }); // Delete an item $server->register('item.delete', function($item_id) { - return Model\set_item_removed($item_id); + return Model\Item\set_removed($item_id); }); // Mark item as read $server->register('item.mark_as_read', function($item_id) { - return Model\set_item_read($item_id); + return Model\Item\set_read($item_id); }); // Mark item as unread $server->register('item.mark_as_unread', function($item_id) { - return Model\set_item_unread($item_id); + return Model\Item\set_unread($item_id); }); // Change the status of list of items $server->register('item.set_list_status', function($status, array $items) { - return Model\set_items_status($status, $items); + return Model\Item\set_status($status, $items); }); // Flush all read items $server->register('item.flush', function() { - return Model\mark_as_removed(); + return Model\Item\mark_all_as_removed(); }); // Mark all unread items as read $server->register('item.mark_all_as_read', function() { - return Model\mark_as_read(); + return Model\Item\mark_all_as_read(); }); -echo $server->execute(); \ No newline at end of file +echo $server->execute(); diff --git a/locales/fr_FR/translations.php b/locales/fr_FR/translations.php index 67e1569..18945dd 100644 --- a/locales/fr_FR/translations.php +++ b/locales/fr_FR/translations.php @@ -42,6 +42,7 @@ return array( 'Bookmarklet:' => 'Bookmarklet :', 'Subscribe with Miniflux' => 'S\'abonner avec Miniflux', 'Drag and drop this link to your bookmarks' => 'Glisser-déposer ce lien dans vos favoris', + 'The content grabber is disabled successfully.' => 'Le téléchargement de contenu a été désactivé avec succès.', 'The content grabber is enabled successfully.' => 'Le téléchargement de contenu est activé avec succès.', 'Unable to activate the content grabber for this subscription.' => 'Impossible d\'activer le téléchargement de contenu pour cet abonnement.', 'enable full content' => 'télécharger le contenu complet', diff --git a/model.php b/model.php deleted file mode 100644 index 55e57c0..0000000 --- a/model.php +++ /dev/null @@ -1,1113 +0,0 @@ - t('Older items first'), - 'desc' => t('Most recent first'), - ); -} - - -function get_languages() -{ - $languages = array( - 'cs_CZ' => t('Czech'), - 'de_DE' => t('German'), - 'en_US' => t('English'), - 'es_ES' => t('Spanish'), - 'fr_FR' => t('French'), - 'it_IT' => t('Italian'), - 'pt_BR' => t('Portuguese'), - 'zh_CN' => t('Simplified Chinese'), - ); - - asort($languages); - - return $languages; -} - - -function get_themes() -{ - $themes = array( - 'original' => t('Original') - ); - - if (file_exists(THEME_DIRECTORY)) { - - $dir = new \DirectoryIterator(THEME_DIRECTORY); - - foreach ($dir as $fileinfo) { - - if (! $fileinfo->isDot() && $fileinfo->isDir()) { - $themes[$dir->getFilename()] = ucfirst($dir->getFilename()); - } - } - } - - return $themes; -} - - -function get_autoflush_options() -{ - return array( - '0' => t('Never'), - '1' => t('After %d day', 1), - '5' => t('After %d days', 5), - '15' => t('After %d days', 15), - '30' => t('After %d days', 30) - ); -} - - -function get_paging_options() -{ - return array( - 50 => 50, - 100 => 100, - 150 => 150, - 200 => 200, - 250 => 250, - ); -} - - -function write_debug() -{ - if (DEBUG) { - - $data = ''; - - foreach (\PicoFeed\Logging::$messages as $line) { - $data .= $line.PHP_EOL; - } - - file_put_contents(DEBUG_FILENAME, $data); - } -} - - -function generate_token() -{ - if (ini_get('open_basedir') === '') { - return substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15); - } - else { - return substr(base64_encode(uniqid(mt_rand(), true)), 0, 20); - } -} - - -function new_tokens() -{ - $values = array( - 'api_token' => generate_token(), - 'feed_token' => generate_token(), - ); - - return \PicoTools\singleton('db')->table('config')->update($values); -} - - -function save_auth_token($type, $value) -{ - return \PicoTools\singleton('db') - ->table('config') - ->update(array( - 'auth_'.$type.'_token' => $value - )); -} - - -function remove_auth_token($type) -{ - \PicoTools\singleton('db') - ->table('config') - ->update(array( - 'auth_'.$type.'_token' => '' - )); - - $_SESSION['config'] = get_config(); -} - - -function export_feeds() -{ - $opml = new Export(get_feeds()); - return $opml->execute(); -} - - -function save_feed(array $values) -{ - return \PicoTools\singleton('db') - ->table('feeds') - ->eq('id', $values['id']) - ->save(array( - 'title' => $values['title'], - 'site_url' => $values['site_url'], - 'feed_url' => $values['feed_url'] - )); -} - - -function import_feeds($content) -{ - $import = new Import($content); - $feeds = $import->execute(); - - if ($feeds) { - - $db = \PicoTools\singleton('db'); - - $db->startTransaction(); - - foreach ($feeds as $feed) { - - if (! $db->table('feeds')->eq('feed_url', $feed->feed_url)->count()) { - - $db->table('feeds')->save(array( - 'title' => $feed->title, - 'site_url' => $feed->site_url, - 'feed_url' => $feed->feed_url - )); - } - } - - $db->closeTransaction(); - - write_debug(); - - return true; - } - - write_debug(); - - return false; -} - - -function import_feed($url, $grabber = false) -{ - $reader = new Reader; - $resource = $reader->download($url, '', '', HTTP_TIMEOUT, HTTP_USERAGENT); - - $parser = $reader->getParser(); - - if ($parser !== false) { - - $parser->grabber = $grabber; - $feed = $parser->execute(); - - if ($feed === false) { - write_debug(); - return false; - } - - if (! $feed->url) $feed->url = $reader->getUrl(); - - if (! $feed->title) { - write_debug(); - return false; - } - - $db = \PicoTools\singleton('db'); - - if (! $db->table('feeds')->eq('feed_url', $reader->getUrl())->count()) { - - // Etag and LastModified are added the next update - $rs = $db->table('feeds')->save(array( - 'title' => $feed->title, - 'site_url' => $feed->url, - 'feed_url' => $reader->getUrl(), - 'download_content' => $grabber ? 1 : 0 - )); - - if ($rs) { - - $feed_id = $db->getConnection()->getLastId(); - update_items($feed_id, $feed->items, $grabber); - write_debug(); - - return (int) $feed_id; - } - } - } - - write_debug(); - - return false; -} - - -function update_feeds($limit = LIMIT_ALL) -{ - $feeds_id = get_feeds_id($limit); - - foreach ($feeds_id as $feed_id) { - update_feed($feed_id); - } - - // Auto-vacuum for people using the cronjob - \PicoTools\singleton('db')->getConnection()->exec('VACUUM'); - - return true; -} - - -function update_feed($feed_id) -{ - $feed = get_feed($feed_id); - if (empty($feed)) return false; - - $reader = new Reader; - - $resource = $reader->download( - $feed['feed_url'], - $feed['last_modified'], - $feed['etag'], - HTTP_TIMEOUT, - HTTP_USERAGENT - ); - - // Update the `last_checked` column each time, HTTP cache or not - update_feed_last_checked($feed_id); - - if (! $resource->isModified()) { - write_debug(); - return true; - } - - $parser = $reader->getParser(); - - if ($parser !== false) { - - if ($feed['download_content']) { - - // Don't fetch previous items, only new one - $parser->grabber_ignore_urls = \PicoTools\singleton('db') - ->table('items') - ->eq('feed_id', $feed_id) - ->findAllByColumn('url'); - - $parser->grabber = true; - $parser->grabber_timeout = HTTP_TIMEOUT; - $parser->grabber_user_agent = HTTP_FAKE_USERAGENT; - } - - $result = $parser->execute(); - - if ($result !== false) { - - update_feed_parsing_error($feed_id, 0); - update_feed_cache_infos($feed_id, $resource->getLastModified(), $resource->getEtag()); - update_items($feed_id, $result->items, $parser->grabber); - write_debug(); - - return true; - } - } - - update_feed_parsing_error($feed_id, 1); - write_debug(); - - return false; -} - - -function get_feeds_id($limit = LIMIT_ALL) -{ - $table_feeds = \PicoTools\singleton('db')->table('feeds') - ->eq('enabled', 1) - ->asc('last_checked'); - - if ($limit !== LIMIT_ALL) { - $table_feeds->limit((int)$limit); - } - - return $table_feeds->listing('id', 'id'); -} - - -function get_feeds() -{ - return \PicoTools\singleton('db') - ->table('feeds') - ->asc('title') - ->findAll(); -} - - -function get_feed($feed_id) -{ - return \PicoTools\singleton('db') - ->table('feeds') - ->eq('id', $feed_id) - ->findOne(); -} - - -function get_empty_feeds() -{ - $feeds = \PicoTools\singleton('db') - ->table('feeds') - ->columns('feeds.id', 'feeds.title', 'COUNT(items.id) AS nb_items') - ->join('items', 'feed_id', 'id') - ->isNull('feeds.last_checked') - ->groupBy('feeds.id') - ->findAll(); - - foreach ($feeds as $key => &$feed) { - - if ($feed['nb_items'] > 0) { - unset($feeds[$key]); - } - } - - return $feeds; -} - - -function update_feed_parsing_error($feed_id, $value) -{ - \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save(array('parsing_error' => $value)); -} - - -function update_feed_last_checked($feed_id) -{ - \PicoTools\singleton('db') - ->table('feeds') - ->eq('id', $feed_id) - ->save(array( - 'last_checked' => time() - )); -} - - -function update_feed_cache_infos($feed_id, $last_modified, $etag) -{ - \PicoTools\singleton('db') - ->table('feeds') - ->eq('id', $feed_id) - ->save(array( - 'last_modified' => $last_modified, - 'etag' => $etag - )); -} - - -function parse_content_with_readability($content, $url) -{ - require_once 'vendor/Readability/Readability.php'; - - if (! empty($content)) { - - $readability = new \Readability($content, $url); - - if ($readability->init()) { - return $readability->getContent()->innerHTML; - } - } - - return ''; -} - - -function download_content($url) -{ - require_once 'vendor/PicoFeed/Grabber.php'; - - $client = \PicoFeed\Client::create(); - $client->url = $url; - $client->timeout = HTTP_TIMEOUT; - $client->user_agent = HTTP_FAKE_USERAGENT; - $client->execute(); - - $html = $client->getContent(); - - if (! empty($html)) { - - // Try first with PicoFeed grabber and with Readability after - $grabber = new \PicoFeed\Grabber($url, $html, $client->getEncoding()); - $content = ''; - - if ($grabber->parse()) { - $content = $grabber->content; - } - - if (empty($content)) { - $content = parse_content_with_readability($grabber->html, $url); - } - - // Filter content - $filter = new \PicoFeed\Filter($content, $url); - return $filter->execute(); - } - - return ''; -} - - -function download_item($item_id) -{ - $item = get_item($item_id); - $content = download_content($item['url']); - - if (! empty($content)) { - - if (! get_config_value('nocontent')) { - - // Save content - \PicoTools\singleton('db') - ->table('items') - ->eq('id', $item['id']) - ->save(array('content' => $content)); - } - - write_debug(); - - return array( - 'result' => true, - 'content' => $content - ); - } - - write_debug(); - - return array( - 'result' => false, - 'content' => '' - ); -} - - -function remove_feed($feed_id) -{ - // Items are removed by a sql constraint - return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->remove(); -} - - -function remove_feeds() -{ - return \PicoTools\singleton('db')->table('feeds')->remove(); -} - - -function enable_feed($feed_id) -{ - return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 1))); -} - - -function disable_feed($feed_id) -{ - return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 0))); -} - - -function enable_grabber_feed($feed_id) -{ - return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save((array('download_content' => 1))); -} - - -function disable_grabber_feed($feed_id) -{ - return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save((array('download_content' => 0))); -} - - -function validate_feed_modification(array $values) -{ - $v = new Validator($values, array( - new Validators\Required('id', t('The feed id is required')), - new Validators\Required('title', t('The title is required')), - new Validators\Required('site_url', t('The site url is required')), - new Validators\Required('feed_url', t('The feed url is required')), - )); - - $result = $v->execute(); - $errors = $v->getErrors(); - - return array( - $result, - $errors - ); -} - - -function get_items($status, $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc') -{ - return \PicoTools\singleton('db') - ->table('items') - ->columns( - 'items.id', - 'items.title', - 'items.updated', - 'items.url', - 'items.bookmark', - 'items.feed_id', - 'items.status', - 'items.content', - 'feeds.site_url', - 'feeds.title AS feed_title' - ) - ->join('feeds', 'id', 'feed_id') - ->eq('status', $status) - ->orderBy($order_column, $order_direction) - ->offset($offset) - ->limit($limit) - ->findAll(); -} - - -function count_items($status) -{ - return \PicoTools\singleton('db') - ->table('items') - ->eq('status', $status) - ->count(); -} - - -function count_bookmarks() -{ - return \PicoTools\singleton('db') - ->table('items') - ->eq('bookmark', 1) - ->in('status', array('read', 'unread')) - ->count(); -} - - -function get_bookmarks($offset = null, $limit = null) -{ - return \PicoTools\singleton('db') - ->table('items') - ->columns( - 'items.id', - 'items.title', - 'items.updated', - 'items.url', - 'items.bookmark', - 'items.status', - 'items.content', - 'items.feed_id', - 'feeds.site_url', - 'feeds.title AS feed_title' - ) - ->join('feeds', 'id', 'feed_id') - ->in('status', array('read', 'unread')) - ->eq('bookmark', 1) - ->orderBy('updated', get_config_value('items_sorting_direction')) - ->offset($offset) - ->limit($limit) - ->findAll(); -} - - -function count_feed_items($feed_id) -{ - return \PicoTools\singleton('db') - ->table('items') - ->eq('feed_id', $feed_id) - ->in('status', array('unread', 'read')) - ->count(); -} - - -function get_feed_items($feed_id, $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc') -{ - return \PicoTools\singleton('db') - ->table('items') - ->columns( - 'items.id', - 'items.title', - 'items.updated', - 'items.url', - 'items.feed_id', - 'items.status', - 'items.content', - 'items.bookmark', - 'feeds.site_url' - ) - ->join('feeds', 'id', 'feed_id') - ->in('status', array('unread', 'read')) - ->eq('feed_id', $feed_id) - ->orderBy($order_column, $order_direction) - ->offset($offset) - ->limit($limit) - ->findAll(); -} - - -function get_item($id) -{ - return \PicoTools\singleton('db') - ->table('items') - ->eq('id', $id) - ->findOne(); -} - - -function get_nav_item($item, $status = array('unread'), $bookmark = array(1, 0), $feed_id = null) -{ - $query = \PicoTools\singleton('db') - ->table('items') - ->columns('id', 'status', 'title', 'bookmark') - ->neq('status', 'removed') - ->orderBy('updated', get_config_value('items_sorting_direction')); - - if ($feed_id) $query->eq('feed_id', $feed_id); - - $items = $query->findAll(); - - $next_item = null; - $previous_item = null; - - for ($i = 0, $ilen = count($items); $i < $ilen; $i++) { - - if ($items[$i]['id'] == $item['id']) { - - if ($i > 0) { - - $j = $i - 1; - - while ($j >= 0) { - - if (in_array($items[$j]['status'], $status) && in_array($items[$j]['bookmark'], $bookmark)) { - $previous_item = $items[$j]; - break; - } - - $j--; - } - } - - if ($i < ($ilen - 1)) { - - $j = $i + 1; - - while ($j < $ilen) { - - if (in_array($items[$j]['status'], $status) && in_array($items[$j]['bookmark'], $bookmark)) { - $next_item = $items[$j]; - break; - } - - $j++; - } - } - - break; - } - } - - return array( - 'next' => $next_item, - 'previous' => $previous_item - ); -} - - -function set_item_removed($id) -{ - return \PicoTools\singleton('db') - ->table('items') - ->eq('id', $id) - ->save(array('status' => 'removed', 'content' => '')); -} - - -function set_item_read($id) -{ - return \PicoTools\singleton('db') - ->table('items') - ->eq('id', $id) - ->save(array('status' => 'read')); -} - - -function set_item_unread($id) -{ - return \PicoTools\singleton('db') - ->table('items') - ->eq('id', $id) - ->save(array('status' => 'unread')); -} - - -function set_items_status($status, array $items) -{ - if (! in_array($status, array('read', 'unread', 'removed'))) return false; - - return \PicoTools\singleton('db') - ->table('items') - ->in('id', $items) - ->save(array('status' => $status)); -} - - -function set_bookmark_value($id, $value) -{ - return \PicoTools\singleton('db') - ->table('items') - ->eq('id', $id) - ->save(array('bookmark' => $value)); -} - - -function switch_item_status($id) -{ - $item = \PicoTools\singleton('db') - ->table('items') - ->columns('status') - ->eq('id', $id) - ->findOne(); - - if ($item['status'] == 'unread') { - - \PicoTools\singleton('db') - ->table('items') - ->eq('id', $id) - ->save(array('status' => 'read')); - - return 'read'; - } - else { - - \PicoTools\singleton('db') - ->table('items') - ->eq('id', $id) - ->save(array('status' => 'unread')); - - return 'unread'; - } - - return ''; -} - - -// Mark all items as read -function mark_as_read() -{ - return \PicoTools\singleton('db') - ->table('items') - ->eq('status', 'unread') - ->save(array('status' => 'read')); -} - - -// Mark only specified items as read -function mark_items_as_read(array $items_id) -{ - \PicoTools\singleton('db')->startTransaction(); - - foreach ($items_id as $id) { - set_item_read($id); - } - - \PicoTools\singleton('db')->closeTransaction(); -} - - -// Mark all items of a feed as read -function mark_feed_as_read($feed_id) -{ - \PicoTools\singleton('db')->startTransaction(); - - $items_id = \PicoTools\singleton('db') - ->table('items') - ->columns('items.id') - ->eq('status', 'unread') - ->eq('feed_id', $feed_id) - ->listing('id', 'id'); - - foreach ($items_id as $id) { - set_item_read($id); - } - - \PicoTools\singleton('db')->closeTransaction(); -} - - -function mark_as_removed() -{ - return \PicoTools\singleton('db') - ->table('items') - ->eq('status', 'read') - ->eq('bookmark', 0) - ->save(array('status' => 'removed', 'content' => '')); -} - - -function autoflush() -{ - $autoflush = get_config_value('autoflush'); - - if ($autoflush) { - - \PicoTools\singleton('db') - ->table('items') - ->eq('bookmark', 0) - ->eq('status', 'read') - ->lt('updated', strtotime('-'.$autoflush.'day')) - ->save(array('status' => 'removed', 'content' => '')); - } -} - - -function update_items($feed_id, array $items, $grabber = false) -{ - $nocontent = (bool) get_config_value('nocontent'); - - $items_in_feed = array(); - $db = \PicoTools\singleton('db'); - - $db->startTransaction(); - - foreach ($items as $item) { - - // Item parsed correctly? - if ($item->id) { - - // Insert only new item - if ($db->table('items')->eq('id', $item->id)->count() !== 1) { - - if (! $item->content && ! $nocontent && $grabber) { - $item->content = download_content($item->url); - } - - $db->table('items')->save(array( - 'id' => $item->id, - 'title' => $item->title, - 'url' => $item->url, - 'updated' => $item->updated, - 'author' => $item->author, - 'content' => $nocontent ? '' : $item->content, - 'status' => 'unread', - 'feed_id' => $feed_id - )); - } - - // Items inside this feed - $items_in_feed[] = $item->id; - } - } - - // Remove from the database items marked as "removed" - // and not present inside the feed - if (! empty($items_in_feed)) { - - $removed_items = \PicoTools\singleton('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)) { - - \PicoTools\singleton('db') - ->table('items') - ->in('id', $items_to_remove) - ->eq('status', 'removed') - ->eq('feed_id', $feed_id) - ->remove(); - } - } - } - - $db->closeTransaction(); -} - - -function get_config_value($name) -{ - if (! isset($_SESSION)) { - - return \PicoTools\singleton('db')->table('config')->findOneColumn($name); - } - else { - - if (! isset($_SESSION['config'])) { - $_SESSION['config'] = get_config(); - } - - if (isset($_SESSION['config'][$name])) { - return $_SESSION['config'][$name]; - } - } - - return null; -} - - -function get_config() -{ - return \PicoTools\singleton('db') - ->table('config') - ->columns( - 'username', - 'language', - 'autoflush', - 'nocontent', - 'items_per_page', - 'theme', - 'api_token', - 'feed_token', - 'auth_google_token', - 'auth_mozilla_token', - 'items_sorting_direction' - ) - ->findOne(); -} - - -function get_user($username) -{ - return \PicoTools\singleton('db') - ->table('config') - ->columns('username', 'password', 'language') - ->eq('username', $username) - ->findOne(); -} - - -function validate_login(array $values) -{ - $v = new Validator($values, array( - new Validators\Required('username', t('The user name is required')), - new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50), - new Validators\Required('password', t('The password is required')) - )); - - $result = $v->execute(); - $errors = $v->getErrors(); - - if ($result) { - - $user = get_user($values['username']); - - if ($user && \password_verify($values['password'], $user['password'])) { - - unset($user['password']); - - $_SESSION['user'] = $user; - $_SESSION['config'] = get_config(); - } - else { - - $result = false; - $errors['login'] = t('Bad username or password'); - } - } - - return array( - $result, - $errors - ); -} - - -function validate_config_update(array $values) -{ - if (! empty($values['password'])) { - - $v = new Validator($values, array( - new Validators\Required('username', t('The user name is required')), - new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50), - new Validators\Required('password', t('The password is required')), - new Validators\MinLength('password', t('The minimum length is 6 characters'), 6), - new Validators\Required('confirmation', t('The confirmation is required')), - new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match')), - new Validators\Required('autoflush', t('Value required')), - new Validators\Required('items_per_page', t('Value required')), - new Validators\Integer('items_per_page', t('Must be an integer')), - new Validators\Required('theme', t('Value required')), - )); - } - else { - - $v = new Validator($values, array( - new Validators\Required('username', t('The user name is required')), - new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50), - new Validators\Required('autoflush', t('Value required')), - new Validators\Required('items_per_page', t('Value required')), - new Validators\Integer('items_per_page', t('Must be an integer')), - new Validators\Required('theme', t('Value required')), - )); - } - - return array( - $v->execute(), - $v->getErrors() - ); -} - - -function save_config(array $values) -{ - // Update the password if needed - if (! empty($values['password'])) { - $values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT); - } else { - unset($values['password']); - } - - unset($values['confirmation']); - - // Reload configuration in session - $_SESSION['config'] = $values; - - // Reload translations for flash session message - \PicoTools\Translator\load($values['language']); - - // If the user does not want content of feeds, remove it in previous ones - if (isset($values['nocontent']) && (bool) $values['nocontent']) { - \PicoTools\singleton('db')->table('items')->update(array('content' => '')); - } - - return \PicoTools\singleton('db')->table('config')->update($values); -} diff --git a/models/feed.php b/models/feed.php new file mode 100644 index 0000000..9e53755 --- /dev/null +++ b/models/feed.php @@ -0,0 +1,338 @@ +table('feeds') + ->eq('id', $values['id']) + ->save(array( + 'title' => $values['title'], + 'site_url' => $values['site_url'], + 'feed_url' => $values['feed_url'] + )); +} + +// Export all feeds +function export_opml() +{ + $opml = new \PicoFeed\Export(get_all()); + return $opml->execute(); +} + +// Import OPML file +function import_opml($content) +{ + $import = new \PicoFeed\Import($content); + $feeds = $import->execute(); + + if ($feeds) { + + $db = \PicoTools\singleton('db'); + $db->startTransaction(); + + foreach ($feeds as $feed) { + + if (! $db->table('feeds')->eq('feed_url', $feed->feed_url)->count()) { + + $db->table('feeds')->save(array( + 'title' => $feed->title, + 'site_url' => $feed->site_url, + 'feed_url' => $feed->feed_url + )); + } + } + + $db->closeTransaction(); + + \Model\Config\write_debug(); + + return true; + } + + \Model\Config\write_debug(); + + return false; +} + +// Add a new feed from an URL +function create($url, $grabber = false) +{ + $reader = new \PicoFeed\Reader; + $resource = $reader->download($url, '', '', HTTP_TIMEOUT, \Model\Config\HTTP_USERAGENT); + + $parser = $reader->getParser(); + + if ($parser !== false) { + + $parser->grabber = $grabber; + $feed = $parser->execute(); + + if ($feed === false) { + \Model\Config\write_debug(); + return false; + } + + if (! $feed->url) $feed->url = $reader->getUrl(); + + if (! $feed->title) { + \Model\Config\write_debug(); + return false; + } + + $db = \PicoTools\singleton('db'); + + if (! $db->table('feeds')->eq('feed_url', $reader->getUrl())->count()) { + + // Etag and LastModified are added the next update + $rs = $db->table('feeds')->save(array( + 'title' => $feed->title, + 'site_url' => $feed->url, + 'feed_url' => $reader->getUrl(), + 'download_content' => $grabber ? 1 : 0 + )); + + if ($rs) { + + $feed_id = $db->getConnection()->getLastId(); + \Model\Item\update_all($feed_id, $feed->items, $grabber); + \Model\Config\write_debug(); + + return (int) $feed_id; + } + } + } + + \Model\Config\write_debug(); + + return false; +} + +// Refresh all feeds +function refresh_all($limit = LIMIT_ALL) +{ + $feeds_id = get_ids($limit); + + foreach ($feeds_id as $feed_id) { + refresh($feed_id); + } + + // Auto-vacuum for people using the cronjob + \PicoTools\singleton('db')->getConnection()->exec('VACUUM'); + + return true; +} + +// Refresh one feed +function refresh($feed_id) +{ + $feed = get($feed_id); + if (empty($feed)) return false; + + $reader = new \PicoFeed\Reader; + + $resource = $reader->download( + $feed['feed_url'], + $feed['last_modified'], + $feed['etag'], + HTTP_TIMEOUT, + \Model\Config\HTTP_USERAGENT + ); + + // Update the `last_checked` column each time, HTTP cache or not + update_last_checked($feed_id); + + if (! $resource->isModified()) { + update_parsing_error($feed_id, 0); + \Model\Config\write_debug(); + return true; + } + + $parser = $reader->getParser(); + + if ($parser !== false) { + + if ($feed['download_content']) { + + // Don't fetch previous items, only new one + $parser->grabber_ignore_urls = \PicoTools\singleton('db') + ->table('items') + ->eq('feed_id', $feed_id) + ->findAllByColumn('url'); + + $parser->grabber = true; + $parser->grabber_timeout = HTTP_TIMEOUT; + $parser->grabber_user_agent = \Model\Config\HTTP_FAKE_USERAGENT; + } + + $result = $parser->execute(); + + if ($result !== false) { + + update_parsing_error($feed_id, 0); + update_cache($feed_id, $resource->getLastModified(), $resource->getEtag()); + \Model\Item\update_all($feed_id, $result->items, $parser->grabber); + \Model\Config\write_debug(); + + return true; + } + } + + update_parsing_error($feed_id, 1); + \Model\Config\write_debug(); + + return false; +} + +// Get the list of feeds ID to refresh +function get_ids($limit = LIMIT_ALL) +{ + $table_feeds = \PicoTools\singleton('db')->table('feeds') + ->eq('enabled', 1) + ->asc('last_checked'); + + if ($limit !== LIMIT_ALL) { + $table_feeds->limit((int) $limit); + } + + return $table_feeds->listing('id', 'id'); +} + +// Get feeds with no item +function get_all_empty() +{ + $feeds = \PicoTools\singleton('db') + ->table('feeds') + ->columns('feeds.id', 'feeds.title', 'COUNT(items.id) AS nb_items') + ->join('items', 'feed_id', 'id') + ->isNull('feeds.last_checked') + ->groupBy('feeds.id') + ->findAll(); + + foreach ($feeds as $key => &$feed) { + + if ($feed['nb_items'] > 0) { + unset($feeds[$key]); + } + } + + return $feeds; +} + +// Get all feeds +function get_all() +{ + return \PicoTools\singleton('db') + ->table('feeds') + ->asc('title') + ->findAll(); +} + +// Get one feed +function get($feed_id) +{ + return \PicoTools\singleton('db') + ->table('feeds') + ->eq('id', $feed_id) + ->findOne(); +} + +// Update parsing error column +function update_parsing_error($feed_id, $value) +{ + \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save(array('parsing_error' => $value)); +} + +// Update last check date +function update_last_checked($feed_id) +{ + \PicoTools\singleton('db') + ->table('feeds') + ->eq('id', $feed_id) + ->save(array( + 'last_checked' => time() + )); +} + +// Update Etag and last Modified columns +function update_cache($feed_id, $last_modified, $etag) +{ + \PicoTools\singleton('db') + ->table('feeds') + ->eq('id', $feed_id) + ->save(array( + 'last_modified' => $last_modified, + 'etag' => $etag + )); +} + +// Remove one feed +function remove($feed_id) +{ + // Items are removed by a sql constraint + return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->remove(); +} + +// Remove all feeds +function remove_all() +{ + return \PicoTools\singleton('db')->table('feeds')->remove(); +} + +// Enable a feed (activate refresh) +function enable($feed_id) +{ + return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 1))); +} + +// Disable feed +function disable($feed_id) +{ + return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 0))); +} + +// Enable content download +function enable_grabber($feed_id) +{ + return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save((array('download_content' => 1))); +} + +// Disable content download +function disable_grabber($feed_id) +{ + return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save((array('download_content' => 0))); +} + +// Validation for edit +function validate_modification(array $values) +{ + $v = new Validator($values, array( + new Validators\Required('id', t('The feed id is required')), + new Validators\Required('title', t('The title is required')), + new Validators\Required('site_url', t('The site url is required')), + new Validators\Required('feed_url', t('The feed url is required')), + )); + + $result = $v->execute(); + $errors = $v->getErrors(); + + return array( + $result, + $errors + ); +} diff --git a/models/item.php b/models/item.php new file mode 100644 index 0000000..c66066c --- /dev/null +++ b/models/item.php @@ -0,0 +1,471 @@ +table('items') + ->columns( + 'items.id', + 'items.title', + 'items.updated', + 'items.url', + 'items.bookmark', + 'items.feed_id', + 'items.status', + 'items.content', + 'feeds.site_url', + 'feeds.title AS feed_title' + ) + ->join('feeds', 'id', 'feed_id') + ->eq('status', $status) + ->orderBy($order_column, $order_direction) + ->offset($offset) + ->limit($limit) + ->findAll(); +} + +// Get the number of items per status +function count_by_status($status) +{ + return \PicoTools\singleton('db') + ->table('items') + ->eq('status', $status) + ->count(); +} + +// Get the number of bookmarks +function count_bookmarks() +{ + return \PicoTools\singleton('db') + ->table('items') + ->eq('bookmark', 1) + ->in('status', array('read', 'unread')) + ->count(); +} + +// Get all bookmarks +function get_bookmarks($offset = null, $limit = null) +{ + return \PicoTools\singleton('db') + ->table('items') + ->columns( + 'items.id', + 'items.title', + 'items.updated', + 'items.url', + 'items.bookmark', + 'items.status', + 'items.content', + 'items.feed_id', + 'feeds.site_url', + 'feeds.title AS feed_title' + ) + ->join('feeds', 'id', 'feed_id') + ->in('status', array('read', 'unread')) + ->eq('bookmark', 1) + ->orderBy('updated', \Model\Config\get('items_sorting_direction')) + ->offset($offset) + ->limit($limit) + ->findAll(); +} + +// Get the number of items per feed +function count_by_feed($feed_id) +{ + return \PicoTools\singleton('db') + ->table('items') + ->eq('feed_id', $feed_id) + ->in('status', array('unread', 'read')) + ->count(); +} + +// Get all items per feed +function get_all_by_feed($feed_id, $offset = null, $limit = null, $order_column = 'updated', $order_direction = 'desc') +{ + return \PicoTools\singleton('db') + ->table('items') + ->columns( + 'items.id', + 'items.title', + 'items.updated', + 'items.url', + 'items.feed_id', + 'items.status', + 'items.content', + 'items.bookmark', + 'feeds.site_url' + ) + ->join('feeds', 'id', 'feed_id') + ->in('status', array('unread', 'read')) + ->eq('feed_id', $feed_id) + ->orderBy($order_column, $order_direction) + ->offset($offset) + ->limit($limit) + ->findAll(); +} + +// Get one item by id +function get($id) +{ + return \PicoTools\singleton('db') + ->table('items') + ->eq('id', $id) + ->findOne(); +} + +// Get item naviguation (next/prev items) +function get_nav($item, $status = array('unread'), $bookmark = array(1, 0), $feed_id = null) +{ + $query = \PicoTools\singleton('db') + ->table('items') + ->columns('id', 'status', 'title', 'bookmark') + ->neq('status', 'removed') + ->orderBy('updated', \Model\Config\get('items_sorting_direction')); + + if ($feed_id) $query->eq('feed_id', $feed_id); + + $items = $query->findAll(); + + $next_item = null; + $previous_item = null; + + for ($i = 0, $ilen = count($items); $i < $ilen; $i++) { + + if ($items[$i]['id'] == $item['id']) { + + if ($i > 0) { + + $j = $i - 1; + + while ($j >= 0) { + + if (in_array($items[$j]['status'], $status) && in_array($items[$j]['bookmark'], $bookmark)) { + $previous_item = $items[$j]; + break; + } + + $j--; + } + } + + if ($i < ($ilen - 1)) { + + $j = $i + 1; + + while ($j < $ilen) { + + if (in_array($items[$j]['status'], $status) && in_array($items[$j]['bookmark'], $bookmark)) { + $next_item = $items[$j]; + break; + } + + $j++; + } + } + + break; + } + } + + return array( + 'next' => $next_item, + 'previous' => $previous_item + ); +} + +// Change item status to removed and clear content +function set_removed($id) +{ + return \PicoTools\singleton('db') + ->table('items') + ->eq('id', $id) + ->save(array('status' => 'removed', 'content' => '')); +} + +// Change item status to read +function set_read($id) +{ + return \PicoTools\singleton('db') + ->table('items') + ->eq('id', $id) + ->save(array('status' => 'read')); +} + +// Change item status to unread +function set_unread($id) +{ + return \PicoTools\singleton('db') + ->table('items') + ->eq('id', $id) + ->save(array('status' => 'unread')); +} + +// Change item status to "read", "unread" or "removed" +function set_status($status, array $items) +{ + if (! in_array($status, array('read', 'unread', 'removed'))) return false; + + return \PicoTools\singleton('db') + ->table('items') + ->in('id', $items) + ->save(array('status' => $status)); +} + +// Enable/disable bookmark flag +function set_bookmark_value($id, $value) +{ + return \PicoTools\singleton('db') + ->table('items') + ->eq('id', $id) + ->save(array('bookmark' => $value)); +} + +// Swap item status read <-> unread +function switch_status($id) +{ + $item = \PicoTools\singleton('db') + ->table('items') + ->columns('status') + ->eq('id', $id) + ->findOne(); + + if ($item['status'] == 'unread') { + + \PicoTools\singleton('db') + ->table('items') + ->eq('id', $id) + ->save(array('status' => 'read')); + + return 'read'; + } + else { + + \PicoTools\singleton('db') + ->table('items') + ->eq('id', $id) + ->save(array('status' => 'unread')); + + return 'unread'; + } + + return ''; +} + +// Mark all unread items as read +function mark_all_as_read() +{ + return \PicoTools\singleton('db') + ->table('items') + ->eq('status', 'unread') + ->save(array('status' => 'read')); +} + +// Mark all read items to removed +function mark_all_as_removed() +{ + return \PicoTools\singleton('db') + ->table('items') + ->eq('status', 'read') + ->eq('bookmark', 0) + ->save(array('status' => 'removed', 'content' => '')); +} + +// Mark only specified items as read +function mark_items_as_read(array $items_id) +{ + \PicoTools\singleton('db')->startTransaction(); + + foreach ($items_id as $id) { + set_read($id); + } + + \PicoTools\singleton('db')->closeTransaction(); +} + +// Mark all items of a feed as read +function mark_feed_as_read($feed_id) +{ + return \PicoTools\singleton('db') + ->table('items') + ->columns('items.id') + ->eq('status', 'unread') + ->eq('feed_id', $feed_id) + ->update(array('status' => 'read')); +} + +// Mark all read items to removed after X days +function autoflush() +{ + $autoflush = \Model\Config\get('autoflush'); + + if ($autoflush) { + + \PicoTools\singleton('db') + ->table('items') + ->eq('bookmark', 0) + ->eq('status', 'read') + ->lt('updated', strtotime('-'.$autoflush.'day')) + ->save(array('status' => 'removed', 'content' => '')); + } +} + +// Update all items +function update_all($feed_id, array $items, $grabber = false) +{ + $nocontent = (bool) \Model\Config\get('nocontent'); + + $items_in_feed = array(); + $db = \PicoTools\singleton('db'); + + $db->startTransaction(); + + foreach ($items as $item) { + + // Item parsed correctly? + if ($item->id) { + + // Insert only new item + if ($db->table('items')->eq('id', $item->id)->count() !== 1) { + + if (! $item->content && ! $nocontent && $grabber) { + $item->content = download_content($item->url); + } + + $db->table('items')->save(array( + 'id' => $item->id, + 'title' => $item->title, + 'url' => $item->url, + 'updated' => $item->updated, + 'author' => $item->author, + 'content' => $nocontent ? '' : $item->content, + 'status' => 'unread', + 'feed_id' => $feed_id + )); + } + + // Items inside this feed + $items_in_feed[] = $item->id; + } + } + + // Remove from the database items marked as "removed" + // and not present inside the feed + if (! empty($items_in_feed)) { + + $removed_items = \PicoTools\singleton('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)) { + + \PicoTools\singleton('db') + ->table('items') + ->in('id', $items_to_remove) + ->eq('status', 'removed') + ->eq('feed_id', $feed_id) + ->remove(); + } + } + } + + $db->closeTransaction(); +} + +// Download content from an URL +function download_content_url($url) +{ + $client = \PicoFeed\Client::create(); + $client->url = $url; + $client->timeout = HTTP_TIMEOUT; + $client->user_agent = \Model\Config\HTTP_FAKE_USERAGENT; + $client->execute(); + + $html = $client->getContent(); + + if (! empty($html)) { + + // Try first with PicoFeed grabber and with Readability after + $grabber = new \PicoFeed\Grabber($url, $html, $client->getEncoding()); + $content = ''; + + if ($grabber->parse()) { + $content = $grabber->content; + } + + if (empty($content)) { + $content = download_content_readability($grabber->html, $url); + } + + // Filter content + $filter = new \PicoFeed\Filter($content, $url); + return $filter->execute(); + } + + return ''; +} + +// Download content from item ID +function download_content_id($item_id) +{ + $item = get($item_id); + $content = download_content_url($item['url']); + + if (! empty($content)) { + + if (! \Model\Config\get('nocontent')) { + + // Save content + \PicoTools\singleton('db') + ->table('items') + ->eq('id', $item['id']) + ->save(array('content' => $content)); + } + + \Model\Config\write_debug(); + + return array( + 'result' => true, + 'content' => $content + ); + } + + \Model\Config\write_debug(); + + return array( + 'result' => false, + 'content' => '' + ); +} + +// Download content with Readability PHP port +function download_content_readability($content, $url) +{ + if (! empty($content)) { + + $readability = new \Readability($content, $url); + + if ($readability->init()) { + return $readability->getContent()->innerHTML; + } + } + + return ''; +} diff --git a/models/user.php b/models/user.php new file mode 100644 index 0000000..ab8c408 --- /dev/null +++ b/models/user.php @@ -0,0 +1,57 @@ +table('config') + ->columns('username', 'password', 'language') + ->eq('username', $username) + ->findOne(); +} + +// Validate authentication +function validate_login(array $values) +{ + $v = new Validator($values, array( + new Validators\Required('username', t('The user name is required')), + new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50), + new Validators\Required('password', t('The password is required')) + )); + + $result = $v->execute(); + $errors = $v->getErrors(); + + if ($result) { + + $user = get($values['username']); + + if ($user && \password_verify($values['password'], $user['password'])) { + + unset($user['password']); + + $_SESSION['user'] = $user; + $_SESSION['config'] = \Model\Config\get_all(); + } + else { + + $result = false; + $errors['login'] = t('Bad username or password'); + } + } + + return array( + $result, + $errors + ); +} diff --git a/schema.php b/schema.php index c466016..20496c5 100644 --- a/schema.php +++ b/schema.php @@ -30,7 +30,7 @@ function version_15($pdo) function version_14($pdo) { - $pdo->exec('ALTER TABLE config ADD COLUMN feed_token TEXT DEFAULT "'.\Model\generate_token().'"'); + $pdo->exec('ALTER TABLE config ADD COLUMN feed_token TEXT DEFAULT "'.\Model\Config\generate_token().'"'); } @@ -42,7 +42,7 @@ function version_13($pdo) function version_12($pdo) { - $pdo->exec('ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT "'.\Model\generate_token().'"'); + $pdo->exec('ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT "'.\Model\Config\generate_token().'"'); }