diff --git a/.gitignore b/.gitignore index 7806308..9d8b304 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ update* +make-archive.sh +*.sqlite +*.db \ No newline at end of file diff --git a/README.markdown b/README.markdown index 0570f57..a0e2548 100644 --- a/README.markdown +++ b/README.markdown @@ -14,26 +14,33 @@ Features - Import/Export OPML feeds - Feeds update by a cronjob or with the user interface in one click - Protected by a login/password (only one possible user) -- Use secure headers (only external images are allowed) +- Use secure headers (only external images and Youtube/Vimeo videos are allowed) - Open external links inside a new tab with a `rel="noreferrer"` attribute - Mobile CSS (responsive design) +- Keyboard shortcuts -Todo ----- +Todo and known bugs +------------------- -- Remove older items from the database +- See Issues: License ------- - AGPL: +Authors +------- + +- Original author: [Frédéric Guillot](http://fredericguillot.com/) +- Contributors: [Pull requesters](https://github.com/fguillot/miniflux/pulls?direction=desc&page=1&sort=created&state=closed) and [Bug reporters](https://github.com/fguillot/miniflux/issues?page=1&state=closed) + Requirements ------------ - PHP >= 5.3.7 - PHP XML extensions (SimpleXML, DOM...) -- PHP Sqlite extensions +- PHP Sqlite extension Libraries used -------------- @@ -81,6 +88,11 @@ Your life is cluttered. Miniflux is a minimalist software. Less is more. +### Why there is no favourites? + +Use the right tool for the right job. +Your browser already have bookmarks, if you don't like it there is many online tools for that. + ### I found a bug, what next? Report the bug to the [issues tracker](https://github.com/fguillot/miniflux/issues) and I will fix it. diff --git a/miniflux/assets/css/app.css b/miniflux/assets/css/app.css index 6f9848a..040a890 100644 --- a/miniflux/assets/css/app.css +++ b/miniflux/assets/css/app.css @@ -24,13 +24,13 @@ body { a { color: #3366CC; + border: 1px solid rgba(255, 255, 255, 0); } a:focus { outline: 0; color: red; text-decoration: none; - padding: 3px; border: 1px dotted #aaa; } @@ -63,6 +63,12 @@ blockquote { font-family: Georgia, serif; } +q { + color: purple; + font-family: Georgia, serif; + font-style: italic; +} + blockquote + p { color: #555; font-style: italic; @@ -127,8 +133,6 @@ form { label { cursor: pointer; display: block; - float: left; - width: 10em; } input { @@ -144,7 +148,8 @@ input[type="text"] { line-height: 15px; width: 250px; font-size: 99%; - margin-bottom: 15px; + margin-bottom: 10px; + margin-top: 5px; } input[type="email"]:focus, @@ -196,14 +201,14 @@ textarea.form-error { .form-errors { color: #b94a48; - margin-left: 10em; + margin-left: 15px; list-style-type: none; } .form-help { font-size: 0.9em; color: brown; - display: inline; + margin-bottom: 15px; } /* alerts */ @@ -234,6 +239,12 @@ textarea.form-error { border-color: #bce8f1; } +.alert-normal { + color: #333; + background-color: #f0f0f0; + border-color: #ddd; +} + /* buttons */ .btn { @@ -340,8 +351,7 @@ nav .active a { .page-header li { font-size: 90%; display: inline; - padding-left: 10px; - padding-right: 10px; + padding-right: 5px; border-right: 1px dotted #ccc; } @@ -389,17 +399,25 @@ nav .active a { font-family: Georgia, serif; } +.items #current-item { + border: 2px dashed #d14; + padding: 5px; +} + /* item */ .item { - color: #333; + padding-left: 5px; + padding-right: 5px; padding-bottom: 50px; + color: #555; } .item p, .item li { font-family: Georgia, serif; line-height: 1.6em; + font-size: 1.1em; } .item h2, @@ -443,6 +461,44 @@ nav .active a { color: purple; } +.item nav span, +.item nav a, +.item nav a:visited { + color: #3366CC; +} + +.item nav { + border-top: 1px dotted #ddd; + padding-top: 8px; + margin-top: 50px; + width: 100%; +} + +.nav-left { + width: 30%; + display: block; + float: left; +} + +.nav-middle { + text-align: center; + float: left; + width: 40%; +} + +.nav-right { + text-align: right; + margin-left: 70%; + width: 30%; + display: block; +} + +/* other pages */ +section li { + margin-left: 15px; + list-style-type: square; +} + /* mobile design */ @media only screen and (max-width: 480px) { @@ -460,6 +516,7 @@ nav .active a { .logo { display: block; float: none; + border-bottom: 1px dotted #ddd; } header { @@ -479,11 +536,19 @@ nav .active a { line-height: 25px; } + nav ul { + padding-top: 5px; + } + .page { clear: both; padding-top: 20px; } + .page li { + margin-left: 5px; + } + .item { font-size: 0.8em; } @@ -496,4 +561,4 @@ nav .active a { font-size: 0.9em; padding: 0; } -} \ No newline at end of file +} diff --git a/miniflux/assets/img/favicon.png b/miniflux/assets/img/favicon.png new file mode 100644 index 0000000..7f96f55 Binary files /dev/null and b/miniflux/assets/img/favicon.png differ diff --git a/miniflux/assets/img/touch-icon-ipad-retina.png b/miniflux/assets/img/touch-icon-ipad-retina.png new file mode 100644 index 0000000..92da239 Binary files /dev/null and b/miniflux/assets/img/touch-icon-ipad-retina.png differ diff --git a/miniflux/assets/img/touch-icon-ipad.png b/miniflux/assets/img/touch-icon-ipad.png new file mode 100644 index 0000000..4de6120 Binary files /dev/null and b/miniflux/assets/img/touch-icon-ipad.png differ diff --git a/miniflux/assets/img/touch-icon-iphone-retina.png b/miniflux/assets/img/touch-icon-iphone-retina.png new file mode 100644 index 0000000..71de36e Binary files /dev/null and b/miniflux/assets/img/touch-icon-iphone-retina.png differ diff --git a/miniflux/assets/img/touch-icon-iphone.png b/miniflux/assets/img/touch-icon-iphone.png new file mode 100644 index 0000000..1a46c5e Binary files /dev/null and b/miniflux/assets/img/touch-icon-iphone.png differ diff --git a/miniflux/assets/js/app.js b/miniflux/assets/js/app.js index 9250382..3cca31c 100644 --- a/miniflux/assets/js/app.js +++ b/miniflux/assets/js/app.js @@ -5,21 +5,48 @@ var queue_length = 5; + function switch_status(item_id) + { + var request = new XMLHttpRequest(); + + request.onreadystatechange = function() { + + if (request.readyState === 4 && is_listing()) { + + var response = JSON.parse(request.responseText); + + if (response.status == "read" || response.status == "unread") { + + find_next_item(); + remove_item(response.item_id); + } + } + } + + request.open("POST", "?action=change-item-status&id=" + item_id, true); + request.send(); + } + + function mark_as_read(item_id) { var request = new XMLHttpRequest(); request.onload = function() { - var article = document.getElementById("item-" + item_id); - - if (article) { - - article.style.display = "none"; - } + remove_item(item_id); }; - request.open("POST", "?action=read&id=" + item_id, true); + request.open("POST", "?action=mark-item-read&id=" + item_id, true); + request.send(); + } + + + function mark_as_unread(item_id) + { + var request = new XMLHttpRequest(); + + request.open("POST", "?action=mark-item-unread&id=" + item_id, true); request.send(); } @@ -60,28 +87,31 @@ var request = new XMLHttpRequest(); - request.onload = function() { + request.onreadystatechange = function() { - hide_refresh_icon(feed_id); + if (request.readyState === 4) { - try { + hide_refresh_icon(feed_id); - var response = JSON.parse(this.responseText); + try { - if (callback) { + var response = JSON.parse(this.responseText); - callback(response); - } - - if (! response.result) { - - //window.alert('Unable to refresh this feed: ' + feed_id); + if (callback) { + + callback(response); + } + + if (! response.result) { + + //window.alert('Unable to refresh this feed: ' + feed_id); + } } + catch (e) {} } - catch (e) {} }; - request.open("GET", "?action=ajax-refresh-feed&feed_id=" + feed_id, true); + request.open("POST", "?action=refresh-feed&feed_id=" + feed_id, true); request.send(); return true; @@ -137,6 +167,161 @@ } + function remove_item(item_id) + { + var item = document.getElementById("item-" + item_id); + if (item) item.parentNode.removeChild(item); + } + + + function open_original_item() + { + var link = document.getElementById("original-item"); + + if (link) { + + if (link.getAttribute("data-action") == "mark-read") { + + mark_as_read(link.getAttribute("data-item-id")); + find_next_item(); + } + + link.click(); + } + } + + + function open_item() + { + var link = document.getElementById("open-item"); + if (link) link.click(); + } + + + function open_next_item() + { + var link = document.getElementById("next-item"); + + if (link) { + + link.click(); + } + else if (is_listing()) { + + find_next_item(); + } + } + + + function open_previous_item() + { + var link = document.getElementById("previous-item"); + + if (link) { + + link.click(); + } + else if (is_listing()) { + + find_previous_item(); + } + } + + + function change_item_status() + { + var item = document.getElementById("current-item"); + if (item) switch_status(item.getAttribute("data-item-id")); + } + + + function set_links_item(item_id) + { + var link = document.getElementById("original-item"); + if (link) link.id = "original-" + link.getAttribute("data-item-id"); + + var link = document.getElementById("open-item"); + if (link) link.id = "open-" + link.getAttribute("data-item-id"); + + var link = document.getElementById("original-" + item_id); + if (link) link.id = "original-item"; + + var link = document.getElementById("open-" + item_id); + if (link) link.id = "open-item"; + } + + + function find_next_item() + { + var items = document.getElementsByTagName("article"); + + if (! document.getElementById("current-item")) { + + items[0].id = "current-item"; + set_links_item(items[0].getAttribute("data-item-id")); + } + else { + + for (var i = 0, ilen = items.length; i < ilen; i++) { + + if (items[i].id == "current-item") { + + items[i].id = "item-" + items[i].getAttribute("data-item-id"); + + if (i + 1 < ilen) { + + items[i + 1].id = "current-item"; + set_links_item(items[i + 1].getAttribute("data-item-id")); + } + + break; + } + } + } + } + + + function find_previous_item() + { + var items = document.getElementsByTagName("article"); + + if (! document.getElementById("current-item")) { + + items[items.length - 1].id = "current-item"; + set_links_item(items[items.length - 1].getAttribute("data-item-id")); + } + else { + + for (var i = items.length - 1; i >= 0; i--) { + + if (items[i].id == "current-item") { + + items[i].id = "item-" + items[i].getAttribute("data-item-id"); + + if (i - 1 >= 0) { + + items[i - 1].id = "current-item"; + set_links_item(items[i - 1].getAttribute("data-item-id")); + } + + break; + } + } + } + } + + + function is_listing() + { + if (document.getElementById("listing")) { + + return true; + } + + return false; + } + + document.onclick = function(e) { var action = e.target.getAttribute("data-action"); @@ -161,4 +346,25 @@ } }; + document.onkeypress = function(e) { + + switch (e.keyCode || e.which) { + case 112: + open_previous_item(); + break; + case 110: + open_next_item(); + break; + case 118: + open_original_item(); + break; + case 111: + open_item(); + break; + case 109: + change_item_status(); + break; + } + }; + })(); \ No newline at end of file diff --git a/miniflux/common.php b/miniflux/common.php index 605af0d..7c0e222 100644 --- a/miniflux/common.php +++ b/miniflux/common.php @@ -3,20 +3,33 @@ require 'check_setup.php'; require 'vendor/password.php'; require 'vendor/PicoTools/Dependency_Injection.php'; +require 'vendor/PicoTools/Translator.php'; require 'vendor/PicoDb/Database.php'; require 'vendor/PicoDb/Table.php'; require 'schema.php'; require 'model.php'; +const DB_VERSION = 4; +const APP_VERSION = 'master'; +const APP_USERAGENT = 'Miniflux - http://miniflux.net'; +const HTTP_TIMEOUT = 5; + + +function get_db_filename() +{ + return 'data/db.sqlite'; +} + + PicoTools\container('db', function() { $db = new PicoDb\Database(array( 'driver' => 'sqlite', - 'filename' => 'data/db.sqlite' + 'filename' => get_db_filename() )); - if ($db->schema()->check(1)) { + if ($db->schema()->check(DB_VERSION)) { return $db; } diff --git a/miniflux/favicon.ico b/miniflux/favicon.ico new file mode 100644 index 0000000..77af6f9 Binary files /dev/null and b/miniflux/favicon.ico differ diff --git a/miniflux/index.php b/miniflux/index.php index d198445..23f2fa3 100644 --- a/miniflux/index.php +++ b/miniflux/index.php @@ -22,11 +22,30 @@ Router\before(function($action) { if ($action !== 'login' && ! isset($_SESSION['user'])) { - PicoFarad\Response\redirect('?action=login'); + Response\redirect('?action=login'); } + $language = 'en_US'; + + if (isset($_SESSION['user']['language'])) { + + $language = $_SESSION['user']['language']; + } + else if (isset($_COOKIE['language'])) { + + $language = $_COOKIE['language']; + } + + if ($language !== 'en_US') { + + PicoTools\Translator\load($language); + } + + setcookie('language', $language, time()+365*24*3600, dirname($_SERVER['PHP_SELF'])); + Response\csp(array( - 'img-src' => '*' + 'img-src' => '*', + 'frame-src' => 'http://www.youtube.com https://www.youtube.com http://player.vimeo.com https://player.vimeo.com' )); Response\xframe(); @@ -74,31 +93,85 @@ Router\post_action('login', function() { }); -Router\get_action('read', function() { +Router\get_action('show', function() { $id = Request\param('id'); - Model\set_item_read($id); - Response\html(Template\layout('read_item', array( 'item' => Model\get_item($id) ))); }); -Router\post_action('read', function() { +Router\get_action('read', function() { $id = Request\param('id'); + $item = Model\get_item($id); + $nav = Model\get_nav_item($item); // must be placed before set_item_read() Model\set_item_read($id); + Response\html(Template\layout('read_item', array( + 'item' => $item, + 'item_nav' => $nav + ))); +}); + + +Router\get_action('mark-item-read', function() { + + $id = Request\param('id'); + Model\set_item_read($id); + Response\Redirect('?action=default'); +}); + + +Router\get_action('mark-item-unread', function() { + + $id = Request\param('id'); + Model\set_item_unread($id); + Response\Redirect('?action=history'); +}); + + +Router\get_action('mark-item-removed', function() { + + $id = Request\param('id'); + Model\set_item_removed($id); + Response\Redirect('?action=history'); +}); + + +Router\post_action('mark-item-read', function() { + + $id = Request\param('id'); + Model\set_item_read($id); Response\json(array('Ok')); }); +Router\post_action('mark-item-unread', function() { + + $id = Request\param('id'); + Model\set_item_unread($id); + Response\json(array('Ok')); +}); + + +Router\post_action('change-item-status', function() { + + $id = Request\param('id'); + + Response\json(array( + 'item_id' => urlencode($id), + 'status' => Model\switch_item_status($id) + )); +}); + + Router\get_action('history', function() { - Response\html(Template\layout('read_items', array( + Response\html(Template\layout('history', array( 'items' => Model\get_read_items(), 'menu' => 'history' ))); @@ -122,11 +195,11 @@ Router\get_action('remove', function() { if ($id && Model\remove_feed($id)) { - Session\flash('This subscription has been removed successfully'); + Session\flash(t('This subscription has been removed successfully.')); } else { - Session\flash_error('Unable to remove this subscription'); + Session\flash_error(t('Unable to remove this subscription.')); } Response\redirect('?action=feeds'); @@ -146,7 +219,7 @@ Router\get_action('refresh-feed', function() { }); -Router\get_action('ajax-refresh-feed', function() { +Router\post_action('refresh-feed', function() { $id = Request\int_param('feed_id'); @@ -159,13 +232,21 @@ Router\get_action('ajax-refresh-feed', function() { }); -Router\get_action('flush-unread', function() { +Router\get_action('mark-as-read', function() { - Model\flush_unread(); + Model\mark_as_read(); Response\redirect('?action=unread'); }); +Router\get_action('confirm-flush-history', function() { + + Response\html(Template\layout('confirm_flush', array( + 'menu' => 'history' + ))); +}); + + Router\get_action('flush-history', function() { Model\flush_read(); @@ -176,7 +257,7 @@ Router\get_action('flush-history', function() { Router\get_action('refresh-all', function() { Model\update_feeds(); - Session\flash('Your subscriptions are updated'); + Session\flash(t('Your subscriptions are updated')); Response\redirect('?action=unread'); }); @@ -205,26 +286,32 @@ Router\post_action('add', function() { if (Model\import_feed($_POST['url'])) { - Session\flash('Subscription added successfully.'); + Session\flash(t('Subscription added successfully.')); Response\redirect('?action=feeds'); } else { - Session\flash_error('Unable to find a subscription.'); + Session\flash_error(t('Unable to find a subscription.')); } Response\html(Template\layout('add', array( 'values' => array('url' => $_POST['url']), - 'errors' => array('url' => 'Unable to find a news feed.'), 'menu' => 'feeds' ))); }); +Router\get_action('optimize-db', function() { + + \PicoTools\singleton('db')->getConnection()->exec('VACUUM'); + Response\redirect('?action=config'); +}); + + Router\get_action('download-db', function() { Response\force_download('db.sqlite.gz'); - Response\binary(gzencode(file_get_contents('data/db.sqlite'))); + Response\binary(gzencode(file_get_contents(get_db_filename()))); }); @@ -248,14 +335,14 @@ Router\post_action('import', function() { if (Model\import_feeds(Request\file_content('file'))) { - Session\flash('Your feeds are imported.'); + Session\flash(t('Your feeds have been imported.')); } else { - Session\flash_error('Unable to import your OPML file.'); + Session\flash_error(t('Unable to import your OPML file.')); } - Response\redirect('?action=feeds'); + Response\redirect('?action=import'); }); @@ -264,6 +351,8 @@ Router\get_action('config', function() { Response\html(Template\layout('config', array( 'errors' => array(), 'values' => Model\get_config(), + 'db_size' => filesize(get_db_filename()), + 'languages' => Model\get_languages(), 'menu' => 'config' ))); }); @@ -278,11 +367,11 @@ Router\post_action('config', function() { if (Model\save_config($values)) { - Session\flash('Your preferences are updated.'); + Session\flash(t('Your preferences are updated.')); } else { - Session\flash_error('Unable to update your preferences.'); + Session\flash_error(t('Unable to update your preferences.')); } Response\redirect('?action=config'); @@ -291,6 +380,8 @@ Router\post_action('config', function() { Response\html(Template\layout('config', array( 'errors' => $errors, 'values' => $values, + 'db_size' => filesize(get_db_filename()), + 'languages' => Model\get_languages(), 'menu' => 'config' ))); }); diff --git a/miniflux/locales/fr_FR/translations.php b/miniflux/locales/fr_FR/translations.php new file mode 100644 index 0000000..c944a0d --- /dev/null +++ b/miniflux/locales/fr_FR/translations.php @@ -0,0 +1,87 @@ + 'Français', + 'English' => 'Anglais', + 'unread' => 'non lus', + 'history' => 'historique', + 'subscriptions' => 'abonnements', + 'Subscriptions' => 'Abonnements', + 'preferences' => 'préférences', + 'Preferences' => 'Préférences', + 'logout' => 'déconnexion', + 'Username' => 'Utilisateur', + 'Password' => 'Mot de passe', + 'Confirmation' => 'Confirmation', + 'Language' => 'Langue', + 'Update' => 'Mettre à jour', + 'More informations' => 'Plus d\'informations', + 'Database' => 'Base de données', + 'Database size:' => 'Taille de la base de données :', + 'Optimize the database' => 'Optimiser la base de données', + '(VACUUM command)' => '(commande SQL VACUUM)', + 'Download the entire database' => 'Télécharger la base de données complète', + '(Gzip compressed Sqlite file)' => '(Fichier Sqlite compressé en Gzip)', + 'Keyboard shortcuts' => 'Raccourcis clavier', + 'Previous item' => 'Élément précédent', + 'Next item' => 'Élément suivant', + 'Mark as read or unread' => 'Marquer comme lu ou non lu', + 'Open original link' => 'Ouvrir le lien original', + 'Open item' => 'Ouvrir un élément', + 'About' => 'A propos', + 'Miniflux version:' => 'Version de Miniflux :', + 'Nothing to read' => 'Rien à lire', + 'Unread items' => 'Éléments non lus', + 'mark all as read' => 'tout marquer comme lu', + 'original link' => 'lien original', + 'mark as read' => 'marquer comme lu', + 'No history' => 'Aucun historique', + 'mark as unread' => 'marquer comme non lu', + 'History' => 'Historique', + 'flush these items' => 'supprimer ces éléments', + 'Item not found' => 'Élément introuvable', + 'Unread items' => 'Éléments non lus', + 'Next' => 'Suivant', + 'Previous' => 'Précédent', + 'Sign in' => 'Connexion', + 'feeds' => 'abonnements', + 'add' => 'ajouter', + 'import' => 'importer', + 'export' => 'exporter', + 'OPML Import' => 'Importation OPML', + 'OPML file' => 'Fichier OPML', + 'Import' => 'Importer', + 'refresh all' => 'actualiser', + 'No subscription' => 'Aucun abonnement', + 'remove' => 'supprimer', + 'refresh' => 'actualiser', + 'feed link' => 'lien du flux', + 'New subscription' => 'Nouvel abonnement', + 'Website or Feed URL' => 'URL du site ou du flux', + 'Add' => 'Ajouter', + 'http://website/' => 'http://siteweb/', + 'Yes' => 'Oui', + 'cancel' => 'annuler', + 'or' => 'ou', + 'Official website:' => 'Site officiel :', + 'Bad username or password' => 'Mauvais utilisateur ou mot de passe', + 'Unable to update your preferences.' => 'Impossible de mettre à jour vos préférences.', + 'Your preferences are updated.' => 'Vos préférences ont été mises à jour.', + 'Unable to import your OPML file.' => 'Impossible d\'importer votre fichier OPML', + 'Your feeds have been imported.' => 'Vos abonnements ont été importés avec succès.', + 'Unable to find a subscription.' => 'Impossible de trouver un abonnement.', + 'Subscription added successfully.' => 'Abonnement ajouté avec succès.', + 'Your subscriptions are updated' => 'Vos abonnements ont été mis à jour', + 'Unable to remove this subscription.' => 'Impossible de supprimer cet abonnement.', + 'This subscription has been removed successfully.' => 'L\'abonnement a été supprimé avec succès.', + 'The user name is required' => 'Le nom d\'utilisateur est obligatoire', + 'The maximum length is 50 characters' => 'La longueur maximale est de 50 caractères', + 'The password is required' => 'Le mot de passe est obligatoire', + 'The minimum length is 6 characters' => 'La longueur minimale est de 6 caractères', + 'The confirmation is required' => 'La confirmation est obligatoire', + 'Passwords doesn\'t match' => 'Les mots de passe ne sont pas identique', + 'Do you really want to remove these items from your history?' => 'Voulez-vous vraiment supprimer les éléments de votre historique ?', + 'Do you really want to remove this subscription: "%s"?' => 'Voulez-vous vraiment supprimer cet abonnement : "%s" ?', + 'Nothing to read, do you want to update your subscriptions?' => + 'Il n\'y a rien à lire, voulez-vous mettre à jour vos abonnements ?' +); \ No newline at end of file diff --git a/miniflux/model.php b/miniflux/model.php index c03a03a..2bcf339 100644 --- a/miniflux/model.php +++ b/miniflux/model.php @@ -4,7 +4,6 @@ namespace Model; require_once 'vendor/PicoFeed/Export.php'; require_once 'vendor/PicoFeed/Import.php'; -require_once 'vendor/PicoFeed/Parser.php'; require_once 'vendor/PicoFeed/Reader.php'; require_once 'vendor/SimpleValidator/Validator.php'; require_once 'vendor/SimpleValidator/Base.php'; @@ -22,6 +21,15 @@ use PicoFeed\Reader; use PicoFeed\Export; +function get_languages() +{ + return array( + 'en_US' => t('English'), + 'fr_FR' => t('French') + ); +} + + function export_feeds() { $opml = new Export(get_feeds()); @@ -64,7 +72,7 @@ function import_feeds($content) function import_feed($url) { $reader = new Reader; - $reader->download($url); + $resource = $reader->download($url, '', '', HTTP_TIMEOUT, APP_USERAGENT); $parser = $reader->getParser(); @@ -72,10 +80,14 @@ function import_feed($url) $feed = $parser->execute(); + if ($feed === false) return false; + if (! $feed->title || ! $feed->url) 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, @@ -96,6 +108,65 @@ function import_feed($url) } +function update_feeds() +{ + foreach (get_feeds_id() as $feed_id) { + + update_feed($feed_id); + } + + // Auto-vacuum for people using the cronjob + \PicoTools\singleton('db')->getConnection()->exec('VACUUM'); +} + + +function update_feed($feed_id) +{ + $feed = get_feed($feed_id); + + $reader = new Reader; + + $resource = $reader->download( + $feed['feed_url'], + $feed['last_modified'], + $feed['etag'], + HTTP_TIMEOUT, + APP_USERAGENT + ); + + if (! $resource->isModified()) { + + return true; + } + + $parser = $reader->getParser(); + + if ($parser !== false) { + + $feed = $parser->execute(); + + if ($feed !== false) { + + update_feed_cache_infos($feed_id, $resource->getLastModified(), $resource->getEtag()); + update_items($feed_id, $feed->items); + + return true; + } + } + + return false; +} + + +function get_feeds_id() +{ + return \PicoTools\singleton('db') + ->table('feeds') + ->asc('updated') + ->listing('id', 'id'); +} + + function get_feeds() { return \PicoTools\singleton('db') @@ -114,10 +185,21 @@ function get_feed($feed_id) } +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 remove_feed($feed_id) { $db = \PicoTools\singleton('db'); - $db->table('items')->eq('feed_id', $feed_id)->remove(); return $db->table('feeds')->eq('id', $feed_id)->remove(); @@ -157,6 +239,44 @@ function get_item($id) } +function get_nav_item($item) +{ + $unread_items = \PicoTools\singleton('db') + ->table('items') + ->columns('items.id') + ->eq('status', 'unread') + ->desc('updated') + ->findAll(); + + $next_item = null; + $previous_item = null; + + for ($i = 0, $ilen = count($unread_items); $i < $ilen; $i++) { + + if ($unread_items[$i]['id'] == $item['id']) { + + if ($i > 0) $previous_item = $unread_items[$i - 1]; + if ($i < ($ilen - 1)) $next_item = $unread_items[$i + 1]; + break; + } + } + + return array( + 'next' => $next_item, + 'previous' => $previous_item + ); +} + + +function set_item_removed($id) +{ + \PicoTools\singleton('db') + ->table('items') + ->eq('id', $id) + ->save(array('status' => 'removed')); +} + + function set_item_read($id) { \PicoTools\singleton('db') @@ -166,6 +286,55 @@ function set_item_read($id) } +function set_item_unread($id) +{ + \PicoTools\singleton('db') + ->table('items') + ->eq('id', $id) + ->save(array('status' => 'unread')); +} + + +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 ''; +} + + +function mark_as_read() +{ + \PicoTools\singleton('db') + ->table('items') + ->eq('status', 'unread') + ->save(array('status' => 'read')); +} + + function flush_unread() { \PicoTools\singleton('db') @@ -184,63 +353,50 @@ function flush_read() } -function update_feeds() -{ - foreach (get_feeds() as $feed) { - - $reader = new Reader; - $reader->download($feed['feed_url']); - $parser = $reader->getParser(); - - if ($parser !== false) { - - update_items($feed['id'], $parser->execute()->items); - } - } -} - - -function update_feed($feed_id) -{ - $feed = get_feed($feed_id); - - $reader = new Reader; - $reader->download($feed['feed_url']); - $parser = $reader->getParser(); - - if ($parser !== false) { - - update_items($feed['id'], $parser->execute()->items); - return true; - } - - return false; -} - - function update_items($feed_id, array $items) { + $items_in_feed = array(); $db = \PicoTools\singleton('db'); $db->startTransaction(); foreach ($items as $item) { - if ($item->id && ! $db->table('items')->eq('id', $item->id)->count()) { + // Item parsed correctly? + if ($item->id) { - $db->table('items')->save(array( - 'id' => $item->id, - 'title' => $item->title, - 'url' => $item->url, - 'updated' => $item->updated, - 'author' => $item->author, - 'content' => $item->content, - 'status' => 'unread', - 'feed_id' => $feed_id - )); + // Insert only new item + if ($db->table('items')->eq('id', $item->id)->count() !== 1) { + + $db->table('items')->save(array( + 'id' => $item->id, + 'title' => $item->title, + 'url' => $item->url, + 'updated' => $item->updated, + 'author' => $item->author, + 'content' => $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)) { + + \PicoTools\singleton('db') + ->table('items') + ->notin('id', $items_in_feed) + ->eq('status', 'removed') + ->eq('feed_id', $feed_id) + ->remove(); + } + $db->closeTransaction(); } @@ -249,7 +405,7 @@ function get_config() { return \PicoTools\singleton('db') ->table('config') - ->columns('username', 'history') + ->columns('username', 'language') ->findOne(); } @@ -258,7 +414,7 @@ function get_user() { return \PicoTools\singleton('db') ->table('config') - ->columns('username', 'password') + ->columns('username', 'password', 'language') ->findOne(); } @@ -266,9 +422,9 @@ function get_user() function validate_login(array $values) { $v = new Validator($values, array( - new Validators\Required('username', 'The user name is required'), - new Validators\MaxLength('username', 'The maximum length is 50 characters', 50), - new Validators\Required('password', 'The password is required') + new Validators\Required('username', t('The user name is required')), + new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50), + new Validators\Required('password', t('The password is required')) )); $result = $v->execute(); @@ -280,12 +436,13 @@ function validate_login(array $values) if ($user && \password_verify($values['password'], $user['password'])) { + unset($user['password']); $_SESSION['user'] = $user; } else { $result = false; - $errors['login'] = 'Bad username or password'; + $errors['login'] = t('Bad username or password'); } } @@ -301,19 +458,19 @@ function validate_config_update(array $values) if (! empty($values['password'])) { $v = new Validator($values, array( - new Validators\Required('username', 'The user name is required'), - new Validators\MaxLength('username', 'The maximum length is 50 characters', 50), - new Validators\Required('password', 'The password is required'), - new Validators\MinLength('password', 'The minimum length is 6 characters', 6), - new Validators\Required('confirmation', 'The confirmation is required'), - new Validators\Equals('password', 'confirmation', 'Passwords doesn\'t match') + 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')) )); } else { $v = new Validator($values, array( - new Validators\Required('username', 'The user name is required'), - new Validators\MaxLength('username', 'The maximum length is 50 characters', 50) + new Validators\Required('username', t('The user name is required')), + new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50) )); } @@ -337,5 +494,10 @@ function save_config(array $values) unset($values['confirmation']); + $_SESSION['user']['language'] = $values['language']; + unset($_COOKIE['language']); + + \PicoTools\Translator\load($values['language']); + return \PicoTools\singleton('db')->table('config')->update($values); } diff --git a/miniflux/schema.php b/miniflux/schema.php index c47f16d..4007577 100644 --- a/miniflux/schema.php +++ b/miniflux/schema.php @@ -2,13 +2,32 @@ namespace Schema; + +function version_4($pdo) +{ + $pdo->exec("CREATE INDEX idx_status ON items(status)"); +} + + +function version_3($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN language TEXT DEFAULT 'en_US'"); +} + + +function version_2($pdo) +{ + $pdo->exec('ALTER TABLE feeds ADD COLUMN last_modified TEXT'); + $pdo->exec('ALTER TABLE feeds ADD COLUMN etag TEXT'); +} + + function version_1($pdo) { $pdo->exec(" CREATE TABLE config ( username TEXT DEFAULT 'admin', - password TEXT, - history INTEGER DEFAULT '15' + password TEXT ) "); diff --git a/miniflux/templates/add.php b/miniflux/templates/add.php index 7d4c227..abd1e4f 100644 --- a/miniflux/templates/add.php +++ b/miniflux/templates/add.php @@ -1,16 +1,16 @@
- - + +
- +
\ No newline at end of file diff --git a/miniflux/templates/app_header.php b/miniflux/templates/app_header.php index 2c03dec..2ccee69 100644 --- a/miniflux/templates/app_header.php +++ b/miniflux/templates/app_header.php @@ -2,21 +2,27 @@ - + + + + + + + miniflux - - + +
diff --git a/miniflux/templates/config.php b/miniflux/templates/config.php index 83c50a0..048da99 100644 --- a/miniflux/templates/config.php +++ b/miniflux/templates/config.php @@ -1,26 +1,54 @@ - +
- +
- - - Don't use the same password everywhere!
+ +
- +
+ +
+
- +
+
-

My data

+

- -

Download the entire database (Gzip compressed Sqlite file).

\ No newline at end of file +
+
+

+
    +
  • +
  • +
  • +
+
+
+

+
    +
  • = p
  • +
  • = n
  • +
  • = m
  • +
  • = v
  • +
  • = o
  • +
+
+
+

+ +
+
\ No newline at end of file diff --git a/miniflux/templates/confirm_flush.php b/miniflux/templates/confirm_flush.php new file mode 100644 index 0000000..44938cc --- /dev/null +++ b/miniflux/templates/confirm_flush.php @@ -0,0 +1,10 @@ + + +

+ +
+ + +
\ No newline at end of file diff --git a/miniflux/templates/confirm_remove.php b/miniflux/templates/confirm_remove.php index 4c48a75..ec40551 100644 --- a/miniflux/templates/confirm_remove.php +++ b/miniflux/templates/confirm_remove.php @@ -1,10 +1,10 @@ -

Do you really want to remove this subscription: ""?

+

- Yes - or cancel + +
\ No newline at end of file diff --git a/miniflux/templates/feeds.php b/miniflux/templates/feeds.php index c39d961..156b354 100644 --- a/miniflux/templates/feeds.php +++ b/miniflux/templates/feeds.php @@ -1,21 +1,21 @@ -

No subscription.

+

-

Nothing to read, do you want to update your subscriptions?

+

update your subscriptions?') ?>

@@ -26,10 +26,10 @@

- | - feed link | - remove | - refresh + | + | + | +

diff --git a/miniflux/templates/history.php b/miniflux/templates/history.php new file mode 100644 index 0000000..41637b5 --- /dev/null +++ b/miniflux/templates/history.php @@ -0,0 +1,44 @@ + + +

+ + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/miniflux/templates/import.php b/miniflux/templates/import.php index a81baf3..eca99a7 100644 --- a/miniflux/templates/import.php +++ b/miniflux/templates/import.php @@ -1,16 +1,16 @@
- +
- +
\ No newline at end of file diff --git a/miniflux/templates/login.php b/miniflux/templates/login.php index 66b8fed..2a7ea80 100644 --- a/miniflux/templates/login.php +++ b/miniflux/templates/login.php @@ -3,13 +3,19 @@ + + + + + + miniflux - + @@ -20,14 +26,14 @@
- +
- +
- +
diff --git a/miniflux/templates/read_item.php b/miniflux/templates/read_item.php index 099d097..e097335 100644 --- a/miniflux/templates/read_item.php +++ b/miniflux/templates/read_item.php @@ -1,20 +1,51 @@ -

Article not found.

+

- -
+

- +

| - +

+ + + +
\ No newline at end of file diff --git a/miniflux/templates/read_items.php b/miniflux/templates/read_items.php deleted file mode 100644 index 12accb3..0000000 --- a/miniflux/templates/read_items.php +++ /dev/null @@ -1,27 +0,0 @@ - - -

No history.

- - - - - -
- - - -
- - \ No newline at end of file diff --git a/miniflux/templates/unread_items.php b/miniflux/templates/unread_items.php index 5c79283..dc50a83 100644 --- a/miniflux/templates/unread_items.php +++ b/miniflux/templates/unread_items.php @@ -1,28 +1,43 @@ -

Nothing to read.

+

-
+
-
-

- mark read + diff --git a/miniflux/vendor/PicoDb/Table.php b/miniflux/vendor/PicoDb/Table.php index f543fd8..f05532b 100644 --- a/miniflux/vendor/PicoDb/Table.php +++ b/miniflux/vendor/PicoDb/Table.php @@ -63,7 +63,14 @@ class Table $this->conditions() ); - return false !== $this->db->execute($sql, $values); + $result = $this->db->execute($sql, $values); + + if ($result !== false && $result->rowCount() > 0) { + + return true; + } + + return false; } @@ -290,6 +297,17 @@ class Table } break; + case 'notin': + if (is_array($arguments[1])) { + + $sql = sprintf( + '%s NOT IN (%s)', + $this->db->escapeIdentifier($column), + implode(', ', array_fill(0, count($arguments[1]), '?')) + ); + } + break; + case 'like': $sql = sprintf('%s LIKE ?', $this->db->escapeIdentifier($column)); break; diff --git a/miniflux/vendor/PicoFeed/Export.php b/miniflux/vendor/PicoFeed/Export.php index 95efae2..4601f0a 100644 --- a/miniflux/vendor/PicoFeed/Export.php +++ b/miniflux/vendor/PicoFeed/Export.php @@ -6,6 +6,12 @@ class Export { private $content = array(); + public $required_fields = array( + 'title', + 'site_url', + 'feed_url' + ); + public function __construct(array $content) { @@ -24,6 +30,19 @@ class Export foreach ($this->content as $feed) { + $valid = true; + + foreach ($this->required_fields as $field) { + + if (! isset($feed[$field])) { + + $valid = false; + break; + } + } + + if (! $valid) continue; + $outline = $body->addChild('outline'); $outline->addAttribute('xmlUrl', $feed['feed_url']); $outline->addAttribute('htmlUrl', $feed['site_url']); diff --git a/miniflux/vendor/PicoFeed/Filter.php b/miniflux/vendor/PicoFeed/Filter.php index 9c7dd24..f57d5e8 100644 --- a/miniflux/vendor/PicoFeed/Filter.php +++ b/miniflux/vendor/PicoFeed/Filter.php @@ -7,11 +7,9 @@ class Filter private $data = ''; private $url = ''; private $input = ''; - private $empty_tag = false; + private $empty_tags = array(); private $strip_content = false; - public $ignored_tags = array(); - public $allowed_tags = array( 'dt' => array(), 'dd' => array(), @@ -45,7 +43,9 @@ class Filter 'figcaption' => array(), 'cite' => array(), 'time' => array('datetime'), - 'abbr' => array('title') + 'abbr' => array('title'), + 'iframe' => array('width', 'height', 'frameborder', 'src'), + 'q' => array('cite') ); public $strip_tags_content = array( @@ -56,8 +56,11 @@ class Filter 'http://', 'https://', 'ftp://', - 'mailto://', - '//' + 'mailto:', + '//', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpg;base64,' ); public $protocol_attributes = array( @@ -67,25 +70,46 @@ class Filter public $blacklist_media = array( 'feeds.feedburner.com', - 'feedsportal.com', + 'da.feedsportal.com', + 'rss.feedsportal.com', + 'res.feedsportal.com', + 'pi.feedsportal.com', 'rss.nytimes.com', 'feeds.wordpress.com', - 'stats.wordpress.com' + 'stats.wordpress.com', + 'rss.cnn.com', + 'twitter.com/home?status=', + 'twitter.com/share', + 'twitter_icon_large.png', + 'www.facebook.com/sharer.php', + 'facebook_icon_large.png', + 'plus.google.com/share', + 'www.gstatic.com/images/icons/gplus-16.png', + 'www.gstatic.com/images/icons/gplus-32.png', + 'www.gstatic.com/images/icons/gplus-64.png' ); public $required_attributes = array( 'a' => array('href'), - 'img' => array('src') + 'img' => array('src'), + 'iframe' => array('src') ); public $add_attributes = array( 'a' => 'rel="noreferrer" target="_blank"' ); + public $iframe_allowed_resources = array( + 'http://www.youtube.com/', + 'https://www.youtube.com/', + 'http://player.vimeo.com/', + 'https://player.vimeo.com/' + ); - public function __construct($data, $url) + + public function __construct($data, $site_url) { - $this->url = $url; + $this->url = $site_url; // Convert bad formatted documents to XML $dom = new \DOMDocument; @@ -104,7 +128,7 @@ class Filter if (! xml_parse($parser, $this->input, true)) { - var_dump($this->input); + //var_dump($this->input); die(xml_get_current_line_number($parser).'|'.xml_error_string(xml_get_error_code($parser))); } @@ -116,12 +140,12 @@ class Filter public function startTag($parser, $name, $attributes) { - $this->empty_tag = false; + $empty_tag = false; $this->strip_content = false; if ($this->isPixelTracker($name, $attributes)) { - $this->empty_tag = true; + $empty_tag = true; } else if ($this->isAllowedTag($name)) { @@ -130,17 +154,33 @@ class Filter foreach ($attributes as $attribute => $value) { - if ($this->isAllowedAttribute($name, $attribute)) { + if ($value != '' && $this->isAllowedAttribute($name, $attribute)) { if ($this->isResource($attribute)) { - if ($this->isRelativePath($value)) { + if ($name === 'iframe') { + + if ($this->isAllowedIframeResource($value)) { + + $attr_data .= ' '.$attribute.'="'.$value.'"'; + $used_attributes[] = $attribute; + } + } + else if ($this->isRelativePath($value)) { $attr_data .= ' '.$attribute.'="'.$this->getAbsoluteUrl($value, $this->url).'"'; $used_attributes[] = $attribute; } else if ($this->isAllowedProtocol($value) && ! $this->isBlacklistMedia($value)) { + if ($attribute == 'src' && + isset($attributes['data-src']) && + $this->isAllowedProtocol($attributes['data-src']) && + ! $this->isBlacklistMedia($attributes['data-src'])) { + + $value = $attributes['data-src']; + } + $attr_data .= ' '.$attribute.'="'.$value.'"'; $used_attributes[] = $attribute; } @@ -153,45 +193,46 @@ class Filter } } + // Check for required attributes if (isset($this->required_attributes[$name])) { foreach ($this->required_attributes[$name] as $required_attribute) { if (! in_array($required_attribute, $used_attributes)) { - $this->empty_tag = true; + $empty_tag = true; break; } } } - if (! $this->empty_tag) { + if (! $empty_tag) { $this->data .= '<'.$name.$attr_data; + // Add custom attributes if (isset($this->add_attributes[$name])) { $this->data .= ' '.$this->add_attributes[$name].' '; } + // If img or br, we don't close it here if ($name !== 'img' && $name !== 'br') $this->data .= '>'; } } - else { - - $this->ignored_tags[] = $name; - } if (in_array($name, $this->strip_tags_content)) { $this->strip_content = true; } + + $this->empty_tags[] = $empty_tag; } public function endTag($parser, $name) { - if (! $this->empty_tag && $this->isAllowedTag($name)) { + if (! array_pop($this->empty_tags) && $this->isAllowedTag($name)) { $this->data .= $name !== 'img' && $name !== 'br' ? '' : '/>'; } @@ -216,7 +257,6 @@ class Filter else { // Relative path - $url_path = $components['path']; if ($url_path{strlen($url_path) - 1} !== '/') { @@ -236,6 +276,8 @@ class Filter public function isRelativePath($value) { + if (strpos($value, 'data:') === 0) return false; + return strpos($value, '://') === false && strpos($value, '//') !== 0; } @@ -258,6 +300,20 @@ class Filter } + public function isAllowedIframeResource($value) + { + foreach ($this->iframe_allowed_resources as $url) { + + if (strpos($value, $url) === 0) { + + return true; + } + } + + return false; + } + + public function isAllowedProtocol($value) { foreach ($this->allowed_protocols as $protocol) { diff --git a/miniflux/vendor/PicoFeed/Parser.php b/miniflux/vendor/PicoFeed/Parser.php index 6612ebf..a494494 100644 --- a/miniflux/vendor/PicoFeed/Parser.php +++ b/miniflux/vendor/PicoFeed/Parser.php @@ -14,6 +14,7 @@ abstract class Parser public $title = ''; public $updated = ''; public $items = array(); + public $debug = false; abstract public function execute(); @@ -37,253 +38,25 @@ abstract class Parser return $content; } -} -class Atom extends Parser -{ - public function execute() + public function displayXmlErrors() { - try { + foreach(\libxml_get_errors() as $error) { - \libxml_use_internal_errors(true); - - $xml = new \SimpleXMLElement($this->content); - - $this->url = $this->getUrl($xml); - $this->title = (string) $xml->title; - $this->id = (string) $xml->id; - $this->updated = strtotime((string) $xml->updated); - $author = (string) $xml->author->name; - - foreach ($xml->entry as $entry) { - - if (isset($entry->author->name)) { - - $author = $entry->author->name; - } - - $item = new \StdClass; - $item->id = (string) $entry->id; - $item->title = (string) $entry->title; - $item->url = $this->getUrl($entry); - $item->updated = strtotime((string) $entry->updated); - $item->author = $author; - $item->content = $this->filterHtml($this->getContent($entry), $item->url); - - $this->items[] = $item; - } + printf("Message: %s\nLine: %d\nColumn: %d\nCode: %d\n", + $error->message, + $error->line, + $error->column, + $error->code + ); } - catch (\Exception $e) { - - } - - return $this; } - public function getContent($entry) + // Dirty quickfix before XML parsing + public function normalizeData($data) { - if (isset($entry->content) && ! empty($entry->content)) { - - if (count($entry->content->children())) { - - return (string) $entry->content->asXML(); - } - else { - - return (string) $entry->content; - } - } - else if (isset($entry->summary) && ! empty($entry->summary)) { - - return (string) $entry->summary; - } - - return ''; - } - - - public function getUrl($xml) - { - foreach ($xml->link as $link) { - - if ((string) $link['type'] === 'text/html') { - - return (string) $link['href']; - } - } - - return (string) $xml->link['href']; + return str_replace("\xc3\x20", '', $data); } } - - -class Rss20 extends Parser -{ - public function execute() - { - try { - - \libxml_use_internal_errors(true); - - $xml = new \SimpleXMLElement($this->content); - $ns = $xml->getNamespaces(true); - - $this->title = (string) $xml->channel->title; - $this->url = (string) $xml->channel->link; - $this->id = $this->url; - $this->updated = isset($xml->channel->pubDate) ? (string) $xml->channel->pubDate : (string) $xml->channel->lastBuildDate; - - if ($this->updated) { - - $this->updated = strtotime($this->updated); - } - else { - - $this->updated = time(); - } - - foreach ($xml->channel->item as $entry) { - - $author = ''; - $content = ''; - $pubdate = ''; - $link = ''; - - if (isset($ns['feedburner'])) { - - $ns_fb = $entry->children($ns['feedburner']); - $link = $ns_fb->origLink; - } - - if (isset($ns['dc'])) { - - $ns_dc = $entry->children($ns['dc']); - $author = (string) $ns_dc->creator; - $pubdate = (string) $ns_dc->date; - } - - if (isset($ns['content'])) { - - $ns_content = $entry->children($ns['content']); - $content = (string) $ns_content->encoded; - } - - if ($content === '' && isset($entry->description)) { - - $content = (string) $entry->description; - } - - if ($author === '') { - - if (isset($entry->author)) { - - $author = (string) $entry->author; - } - else if (isset($xml->channel->webMaster)) { - - $author = (string) $xml->channel->webMaster; - } - } - - $item = new \StdClass; - $item->title = (string) $entry->title; - $item->url = $link ?: (string) $entry->link; - $item->id = isset($entry->guid) ? (string) $entry->guid : $item->url; - $item->updated = strtotime($pubdate ?: (string) $entry->pubDate) ?: $this->updated; - $item->content = $this->filterHtml($content, $item->url); - $item->author = $author; - - $this->items[] = $item; - } - } - catch (\Exception $e) { - - } - - return $this; - } -} - - -class Rss10 extends Parser -{ - public function execute() - { - try { - - \libxml_use_internal_errors(true); - - $xml = new \SimpleXMLElement($this->content); - $ns = $xml->getNamespaces(true); - - $this->title = (string) $xml->channel->title; - $this->url = (string) $xml->channel->link; - $this->id = $this->url; - - if (isset($ns['dc'])) { - - $ns_dc = $xml->channel->children($ns['dc']); - $this->updated = isset($ns_dc->date) ? strtotime($ns_dc->date) : time(); - } - else { - - $this->updated = time(); - } - - foreach ($xml->item as $entry) { - - $author = ''; - $content = ''; - $pubdate = ''; - $link = ''; - - if (isset($ns['feedburner'])) { - - $ns_fb = $entry->children($ns['feedburner']); - $link = $ns_fb->origLink; - } - - if (isset($ns['dc'])) { - - $ns_dc = $entry->children($ns['dc']); - $author = (string) $ns_dc->creator; - $pubdate = (string) $ns_dc->date; - } - - if (isset($ns['content'])) { - - $ns_content = $entry->children($ns['content']); - $content = (string) $ns_content->encoded; - } - - if ($content === '' && isset($entry->description)) { - - $content = (string) $entry->description; - } - - $item = new \StdClass; - $item->title = (string) $entry->title; - $item->url = $link ?: (string) $entry->link; - $item->id = $item->url; - $item->updated = $pubdate ? strtotime($pubdate) : time(); - $item->content = $this->filterHtml($content, $item->url); - $item->author = $author ?: (string) $xml->channel->webMaster; - - $this->items[] = $item; - } - } - catch (\Exception $e) { - - } - - return $this; - } -} - - -class Rss92 extends Rss20 {} - - -class Rss91 extends Rss20 {} diff --git a/miniflux/vendor/PicoFeed/Parsers/Atom.php b/miniflux/vendor/PicoFeed/Parsers/Atom.php new file mode 100644 index 0000000..6e925d0 --- /dev/null +++ b/miniflux/vendor/PicoFeed/Parsers/Atom.php @@ -0,0 +1,85 @@ +content = $this->normalizeData($this->content); + + \libxml_use_internal_errors(true); + + $xml = \simplexml_load_string($this->content); + + if ($xml === false) { + + if ($this->debug) $this->displayXmlErrors(); + return false; + } + + $this->url = $this->getUrl($xml); + $this->title = (string) $xml->title; + $this->id = (string) $xml->id; + $this->updated = strtotime((string) $xml->updated); + $author = (string) $xml->author->name; + + foreach ($xml->entry as $entry) { + + if (isset($entry->author->name)) { + + $author = $entry->author->name; + } + + $item = new \StdClass; + $item->id = (string) $entry->id; + $item->title = (string) $entry->title; + $item->url = $this->getUrl($entry); + $item->updated = strtotime((string) $entry->updated); + $item->author = $author; + $item->content = $this->filterHtml($this->getContent($entry), $item->url); + + if (empty($item->title)) $item->title = $item->url; + + $this->items[] = $item; + } + + return $this; + } + + + public function getContent($entry) + { + if (isset($entry->content) && ! empty($entry->content)) { + + if (count($entry->content->children())) { + + return (string) $entry->content->asXML(); + } + else { + + return (string) $entry->content; + } + } + else if (isset($entry->summary) && ! empty($entry->summary)) { + + return (string) $entry->summary; + } + + return ''; + } + + + public function getUrl($xml) + { + foreach ($xml->link as $link) { + + if ((string) $link['type'] === 'text/html' || (string) $link['type'] === 'application/xhtml+xml') { + + return (string) $link['href']; + } + } + + return (string) $xml->link['href']; + } +} \ No newline at end of file diff --git a/miniflux/vendor/PicoFeed/Parsers/Rss10.php b/miniflux/vendor/PicoFeed/Parsers/Rss10.php new file mode 100644 index 0000000..d04bdd5 --- /dev/null +++ b/miniflux/vendor/PicoFeed/Parsers/Rss10.php @@ -0,0 +1,85 @@ +content = $this->normalizeData($this->content); + + \libxml_use_internal_errors(true); + $xml = \simplexml_load_string($this->content); + + if ($xml === false) { + + if ($this->debug) $this->displayXmlErrors(); + return false; + } + + $namespaces = $xml->getNamespaces(true); + + $this->title = (string) $xml->channel->title; + $this->url = (string) $xml->channel->link; + $this->id = $this->url; + + if (isset($namespaces['dc'])) { + + $ns_dc = $xml->channel->children($namespaces['dc']); + $this->updated = isset($ns_dc->date) ? strtotime($ns_dc->date) : time(); + } + else { + + $this->updated = time(); + } + + foreach ($xml->item as $entry) { + + $item = new \StdClass; + $item->title = (string) $entry->title; + $item->url = ''; + $item->author= ''; + $item->updated = ''; + $item->content = ''; + + foreach ($namespaces as $name => $url) { + + $namespace = $entry->children($namespaces[$name]); + + if (! $item->url && ! empty($namespace->origLink)) $item->url = (string) $namespace->origLink; + if (! $item->author && ! empty($namespace->creator)) $item->author = (string) $namespace->creator; + if (! $item->updated && ! empty($namespace->date)) $item->updated = strtotime((string) $namespace->date); + if (! $item->updated && ! empty($namespace->updated)) $item->updated = strtotime((string) $namespace->updated); + if (! $item->content && ! empty($namespace->encoded)) $item->content = (string) $namespace->encoded; + } + + if (empty($item->url)) $item->url = (string) $entry->link; + if (empty($item->updated)) $item->updated = $this->updated; + + if (empty($item->content)) { + + $item->content = isset($entry->description) ? (string) $entry->description : ''; + } + + if (empty($item->author)) { + + if (isset($entry->author)) { + + $item->author = (string) $entry->author; + } + else if (isset($xml->channel->webMaster)) { + + $item->author = (string) $xml->channel->webMaster; + } + } + + if (empty($item->title)) $item->title = $item->url; + + $item->id = $item->url; + $item->content = $this->filterHtml($item->content, $item->url); + $this->items[] = $item; + } + + return $this; + } +} \ No newline at end of file diff --git a/miniflux/vendor/PicoFeed/Parsers/Rss20.php b/miniflux/vendor/PicoFeed/Parsers/Rss20.php new file mode 100644 index 0000000..919b968 --- /dev/null +++ b/miniflux/vendor/PicoFeed/Parsers/Rss20.php @@ -0,0 +1,102 @@ +content = $this->normalizeData($this->content); + + \libxml_use_internal_errors(true); + $xml = \simplexml_load_string($this->content); + + if ($xml === false) { + + if ($this->debug) $this->displayXmlErrors(); + return false; + } + + $namespaces = $xml->getNamespaces(true); + + if ($xml->channel->link->count() > 1) { + + foreach ($xml->channel->link as $xml_link) { + + $link = (string) $xml_link; + + if ($link !== '') { + + $this->url = (string) $link; + break; + } + } + } + else { + + $this->url = (string) $xml->channel->link; + } + + $this->title = (string) $xml->channel->title; + $this->id = $this->url; + $this->updated = isset($xml->channel->pubDate) ? (string) $xml->channel->pubDate : (string) $xml->channel->lastBuildDate; + $this->updated = $this->updated ? strtotime($this->updated) : time(); + + foreach ($xml->channel->item as $entry) { + + $item = new \StdClass; + $item->title = (string) $entry->title; + $item->url = ''; + $item->author= ''; + $item->updated = ''; + $item->content = ''; + + foreach ($namespaces as $name => $url) { + + $namespace = $entry->children($namespaces[$name]); + + if (! $item->url && ! empty($namespace->origLink)) $item->url = (string) $namespace->origLink; + if (! $item->author && ! empty($namespace->creator)) $item->author = (string) $namespace->creator; + if (! $item->updated && ! empty($namespace->date)) $item->updated = strtotime((string) $namespace->date); + if (! $item->updated && ! empty($namespace->updated)) $item->updated = strtotime((string) $namespace->updated); + if (! $item->content && ! empty($namespace->encoded)) $item->content = (string) $namespace->encoded; + } + + if (empty($item->url)) $item->url = (string) $entry->link; + if (empty($item->updated)) $item->updated = strtotime((string) $entry->pubDate) ?: $this->updated; + + if (empty($item->content)) { + + $item->content = isset($entry->description) ? (string) $entry->description : ''; + } + + if (empty($item->author)) { + + if (isset($entry->author)) { + + $item->author = (string) $entry->author; + } + else if (isset($xml->channel->webMaster)) { + + $item->author = (string) $xml->channel->webMaster; + } + } + + if (isset($entry->guid) && isset($entry->guid['isPermaLink']) && (string) $entry->guid['isPermaLink'] != 'false') { + + $item->id = (string) $entry->guid; + } + else { + + $item->id = $item->url; + } + + if (empty($item->title)) $item->title = $item->url; + + $item->content = $this->filterHtml($item->content, $item->url); + $this->items[] = $item; + } + + return $this; + } +} \ No newline at end of file diff --git a/miniflux/vendor/PicoFeed/Parsers/Rss91.php b/miniflux/vendor/PicoFeed/Parsers/Rss91.php new file mode 100644 index 0000000..08716e9 --- /dev/null +++ b/miniflux/vendor/PicoFeed/Parsers/Rss91.php @@ -0,0 +1,7 @@ +url = $url; - $this->content = @file_get_contents($this->url); + $resource = new RemoteResource($url, $timeout, $user_agent); + $resource->setLastModified($last_modified); + $resource->setEtag($etag); + $resource->execute(); - return $this; + $this->content = $resource->getContent(); + $this->url = $resource->getUrl(); + + return $resource; } @@ -67,24 +75,32 @@ class Reader { $first_tag = $this->getFirstTag($this->content); - if (strpos($first_tag, 'content); } - else if (strpos($first_tag, 'content); } - else if (strpos($first_tag, 'content); } - else if (strpos($first_tag, 'content); } else if (strpos($first_tag, 'content); } else if ($discover === true) { diff --git a/miniflux/vendor/PicoFeed/RemoteResource.php b/miniflux/vendor/PicoFeed/RemoteResource.php new file mode 100644 index 0000000..f5b1c0a --- /dev/null +++ b/miniflux/vendor/PicoFeed/RemoteResource.php @@ -0,0 +1,166 @@ +url = $url; + $this->timeout = $timeout; + $this->user_agent = $user_agent; + + return $this; + } + + + public function setLastModified($last_modified) + { + $this->last_modified = $last_modified; + return $this; + } + + + public function getLastModified() + { + return $this->last_modified; + } + + + public function setEtag($etag) + { + $this->etag = $etag; + return $this; + } + + + public function getEtag() + { + return $this->etag; + } + + + public function getUrl() + { + return $this->url; + } + + + public function getContent() + { + return $this->content; + } + + + public function isModified() + { + return $this->is_modified; + } + + + public function execute() + { + $response = $this->makeRequest(); + + $this->etag = isset($response['headers']['ETag']) ? $response['headers']['ETag'] : ''; + $this->last_modified = isset($response['headers']['Last-Modified']) ? $response['headers']['Last-Modified'] : ''; + + if ($response['status'] == 304) { + + $this->is_modified = false; + } + else if ($response['status'] == 301 || $response['status'] == 302) { + + if (isset($response['headers']['Location'])) { + + $this->url = $response['headers']['Location']; + } + else if (isset($response['headers']['location'])) { + + $this->url = $response['headers']['location']; + } + + $this->execute(); + } + else { + + $this->content = $response['body']; + } + } + + + public function makeRequest() + { + $http_code = 200; + $http_body = ''; + $http_headers = array(); + + if (! function_exists('curl_init')) { + + $http_body = @file_get_contents($this->url); + } + else { + + $headers = array('Connection: close'); + + if ($this->etag) $headers[] = 'If-None-Match: '.$this->etag; + if ($this->last_modified) $headers[] = 'If-Modified-Since: '.$this->last_modified; + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $this->url); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); + curl_setopt($ch, CURLOPT_USERAGENT, $this->user_agent); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + + $http_response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $http_body = ''; + $http_headers = array(); + + curl_close($ch); + + $lines = explode("\r\n", $http_response); + $body_start = 0; + $i = 0; + + foreach ($lines as $line) { + + if ($line === '') { + + $body_start = $i; + break; + } + else if (($p = strpos($line, ':')) !== false) { + + $key = substr($line, 0, $p); + $value = substr($line, $p + 1); + + $http_headers[trim($key)] = trim($value); + } + + $i++; + } + + $http_body = implode("\r\n", array_splice($lines, $i + 1)); + } + + return array( + 'status' => $http_code, + 'body' => $http_body, + 'headers' => $http_headers + ); + } +} \ No newline at end of file diff --git a/miniflux/vendor/PicoTools/Chrono.php b/miniflux/vendor/PicoTools/Chrono.php deleted file mode 100644 index 8a65505..0000000 --- a/miniflux/vendor/PicoTools/Chrono.php +++ /dev/null @@ -1,100 +0,0 @@ - microtime(true), - 'finish' => 0 - ); - } - - - /** - * Stop a chrono - * - * @access public - * @static - * @param string $name Chrono name - */ - public static function stop($name) - { - if (! isset(self::$chronos[$name])) { - - throw new \RuntimeException('Chrono not started!'); - } - - self::$chronos[$name]['finish'] = microtime(true); - } - - - /** - * Get a duration of a chrono - * - * @access public - * @static - * @return float - */ - public static function duration($name) - { - if (! isset(self::$chronos[$name])) { - - throw new \RuntimeException('Chrono not started!'); - } - - return self::$chronos[$name]['finish'] - self::$chronos[$name]['start']; - } - - - /** - * Show all durations - * - * @access public - * @static - */ - public static function show() - { - foreach (self::$chronos as $name => $values) { - - echo $name.' = '; - echo round($values['finish'] - $values['start'], 2).'s'; - echo PHP_EOL; - } - } -} \ No newline at end of file diff --git a/miniflux/vendor/PicoTools/Crypto.php b/miniflux/vendor/PicoTools/Crypto.php deleted file mode 100644 index aa48295..0000000 --- a/miniflux/vendor/PicoTools/Crypto.php +++ /dev/null @@ -1,36 +0,0 @@ -'; + $html .= error_list($errors, $name); + + return $html; +} + + function form_textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') { $class .= error_class($errors, $name); diff --git a/miniflux/vendor/PicoTools/Pixtag.php b/miniflux/vendor/PicoTools/Pixtag.php deleted file mode 100644 index a5e59d8..0000000 --- a/miniflux/vendor/PicoTools/Pixtag.php +++ /dev/null @@ -1,243 +0,0 @@ -filename = $filename; - } - - - /** - * Read metadata from the picture - * - * @access public - */ - public function read() - { - $c = new Command('exiv2 -PIEXkt '.$this->filename); - $c->execute(); - $this->parse($c->getStdout()); - } - - - /** - * Parse metadata bloc from exiv2 output command - * - * @access public - * @param string $data Raw command output of exiv2 - */ - public function parse($data) - { - $lines = explode("\n", trim($data)); - - foreach ($lines as $line) { - - $results = preg_split('/ /', $line, -1, PREG_SPLIT_OFFSET_CAPTURE); - - if (isset($results[0][0])) { - - $key = $results[0][0]; - $value = ''; - - for ($i = 1, $ilen = count($results); $i < $ilen; ++$i) { - - if ($results[$i][0] !== '') { - - $value = substr($line, $results[$i][1]); - break; - } - } - - if ($value === '(Binary value suppressed)') { - - $value = ''; - } - - $this->container[$key] = $value; - } - } - } - - - /** - * Write metadata to the picture - * This method erase all keys and then add them to the picture - * - * @access public - */ - public function write() - { - $commands = array(); - - foreach ($this->container as $key => $value) { - - $commands[] = sprintf('-M "del %s"', $key); - $commands[] = sprintf('-M "add %s %s"', $key, $value); - } - - $c = new Command(sprintf( - 'exiv2 %s %s', - implode(' ', $commands), - $this->filename - )); - - $c->execute(); - - if ($c->getReturnValue() !== 0) { - - throw new \RuntimeException('Unable to write metadata'); - } - } - - - /** - * Set a metadata - * - * @access public - * @param string $offset Key name, see exiv2 documentation for keys list - * @param string $value Key value - */ - public function offsetSet($offset, $value) - { - $this->container[$offset] = $value; - } - - - /** - * Check if a key exists - * - * @access public - * @param string $offset Key name, see exiv2 documentation for keys list - * @return boolean True if the key exists - */ - public function offsetExists($offset) - { - return isset($this->container[$offset]); - } - - - /** - * Remove a metadata - * - * @access public - * @param string $offset Key name, see exiv2 documentation for keys list - */ - public function offsetUnset($offset) - { - unset($this->container[$offset]); - } - - - /** - * Get a metadata - * - * @access public - * @param string $offset Key name, see exiv2 documentation for keys list - * @return string Key value - */ - public function offsetGet($offset) - { - return isset($this->container[$offset]) ? $this->container[$offset] : null; - } - - - /** - * Reset the position of the container - * - * @access public - */ - public function rewind() - { - reset($this->container); - } - - - /** - * Current - * - * @access public - * @return string Current value - */ - public function current() - { - return current($this->container); - } - - - /** - * Key - * - * @access public - * @return string Current key - */ - public function key() - { - return key($this->container); - } - - - /** - * Next - * - * @access public - */ - public function next() - { - next($this->container); - } - - - /** - * Valid - * - * @access public - * @return boolean True if the current key is valid - */ - public function valid() - { - return isset($this->container[key($this->container)]); - } -} \ No newline at end of file diff --git a/miniflux/vendor/PicoTools/Translator.php b/miniflux/vendor/PicoTools/Translator.php index 40bd653..2e4dffb 100644 --- a/miniflux/vendor/PicoTools/Translator.php +++ b/miniflux/vendor/PicoTools/Translator.php @@ -19,7 +19,7 @@ namespace PicoTools\Translator { $args = \func_get_args(); \array_shift($args); - \array_unshift($args, get($identifier)); + \array_unshift($args, get($identifier, $identifier)); return \call_user_func_array( 'sprintf', @@ -61,6 +61,12 @@ namespace PicoTools\Translator { } + function datetime($format, $timestamp) + { + return strftime($format, $timestamp); + } + + function get($identifier, $default = '') { $locales = container(); @@ -78,6 +84,8 @@ namespace PicoTools\Translator { function load($language) { + setlocale(LC_TIME, $language.'.UTF-8'); + $path = PATH.$language; $locales = array(); @@ -102,7 +110,7 @@ namespace PicoTools\Translator { { static $values = array(); - if ($locales) { + if ($locales !== null) { $values = $locales; } @@ -118,4 +126,10 @@ namespace { return call_user_func_array('\PicoTools\Translator\translate', func_get_args()); } + + + function dt() { + + return call_user_func_array('\PicoTools\Translator\datetime', func_get_args()); + } } \ No newline at end of file