diff --git a/CHANGELOG b/CHANGELOG index 4db9381d5..d167dd917 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,54 @@ # Journal des modifications +## 2014-06-13 FreshRSS 0.7.2 + +* API compatible with Google Reader API level 2 + * FreshRSS can now be used from e.g.: + * (Android) News+ https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader + * (Android) EasyRSS https://github.com/Alkarex/EasyRSS +* Basic support for audio and video podcasts +* Searching + * New search filters date: and pubdate: accepting ISO 8601 date intervals such as `date:2013-2014` or `pubdate:P1W` + * Possibility to combine search filters, e.g. `date:2014-05 intitle:FreshRSS intitle:Open great reader #Internet` +* Change nav menu with more buttons instead of dropdown menus and add some filters +* New system of import / export + * Support OPML, Json (like Google Reader) and Zip archives + * Can export and import articles (specific option for favorites) +* Refactor "Origine" theme + * Some improvements + * Based on a template file (other themes will use it too) + + +## 2014-02-19 FreshRSS 0.7.1 + +* Mise à jour des flux plus rapide grâce à une meilleure utilisation du cache + * Utilisation d’une signature MD5 du contenu intéressant pour les flux n’implémentant pas les requêtes conditionnelles +* Modification des raccourcis + * "s" partage directement si un seul moyen de partage + * Moyens de partage accessibles par "1", "2", "3", etc. + * Premier article : Home ; Dernier article : End + * Ajout du déplacement au sein des catégories / flux (via modificateurs shift et alt) +* UI + * Séparation des descriptions des raccourcis par groupes + * Revue rapide de la page de connexion + * Amélioration de l'affichage des notifications sur mobile +* Revue du système de rafraîchissement des flux + * Meilleure gestion de la file de flux à rafraîchir en JSON + * Rafraîchissement uniquement pour les flux non rafraîchis récemment + * Possibilité donnée aux anonymes de rafraîchir les flux +* SimplePie + * Mise à jour de la lib + * Corrige fuite de mémoire + * Meilleure tolérance aux flux invalides +* Corrections divers + * Ne déplie plus l'article lors du clic sur l'icône lien externe + * Ne boucle plus à la fin de la navigation dans les articles + * Suppression du champ category.color inutile + * Corrige bug redirection infinie (Persona) + * Amélioration vérification de la requête POST + * Ajout d'un verrou lorsqu'une action mark_read ou mark_favorite est en cours + + ## 2014-01-29 FreshRSS 0.7 * Nouveau mode multi-utilisateur diff --git a/README.md b/README.md index 75346f98e..ce3c8ef32 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,19 @@ Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture an * Site officiel : http://freshrss.org * Démo : http://demo.freshrss.org/ * Développeur : Marien Fressinaud -* Version actuelle : 0.7 -* Date de publication 2014-01-29 +* Version actuelle : 0.8-dev +* Date de publication 2014-0x-xx * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) +# Note sur les branches +**Ce logiciel est encore en développement !** Veuillez vous assurer d'utiliser la branche qui vous correspond : + +* Utilisez [la branche master](https://github.com/marienfressinaud/FreshRSS/tree/master/) si vous visez la stabilité. +* [La branche beta](https://github.com/marienfressinaud/FreshRSS/tree/beta) est celle par défaut : les nouveautés y sont ajoutées environ tous les mois. +* Pour les développeurs et ceux qui savent ce qu'ils font, [la branche dev](https://github.com/marienfressinaud/FreshRSS/tree/dev) vous ouvre les bras ! + # Disclaimer Cette application a été développée pour s’adapter à des besoins personnels et non professionnels. Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement. @@ -25,9 +32,9 @@ Privilégiez pour cela des demandes sur GitHub * Serveur modeste, par exemple sous Linux ou Windows * Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées) * Serveur Web Apache2 ou Nginx (non testé sur les autres) -* PHP 5.2+ (PHP 5.3.7+ recommandé) +* PHP 5.2.1+ (PHP 5.3.7+ recommandé) * Requis : [PDO_MySQL](http://php.net/pdo-mysql), [cURL](http://php.net/curl), [LibXML](http://php.net/xml), [PCRE](http://php.net/pcre), [ctype](http://php.net/ctype) - * Recommandés : [JSON](http://php.net/json), [zlib](http://php.net/zlib), [mbstring](http://php.net/mbstring), [iconv](http://php.net/iconv) + * Recommandés : [JSON](http://php.net/json), [zlib](http://php.net/zlib), [mbstring](http://php.net/mbstring), [iconv](http://php.net/iconv), [Zip](http://php.net/zip) * MySQL 5.0.3+ (ou SQLite 3.7.4+ à venir) * Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+ * Fonctionne aussi sur mobile @@ -42,7 +49,8 @@ Privilégiez pour cela des demandes sur GitHub 5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à me contacter. # Contrôle d’accès -Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS : +Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix : +* En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.3.7+ recommandé – fonctionne avec certaines versions de PHP 5.3.3+) * En utilisant l’identification par [Mozilla Persona](https://login.persona.org/about) incluse dans FreshRSS * En utilisant un contrôle d’accès HTTP défini par votre serveur Web * Voir par exemple la [documentation d’Apache sur l’authentification](http://httpd.apache.org/docs/trunk/howto/auth.html) @@ -80,6 +88,7 @@ mysqldump -u utilisateur -p --databases freshrss > freshrss.sql * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/) * [jQuery](http://jquery.com/) * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/) +* [flotr2](http://www.humblesoftware.com/flotr2) ## Uniquement pour certaines options * [bcrypt.js](https://github.com/dcodeIO/bcrypt.js) diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 645f9eabf..a117e0f9c 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -29,7 +29,6 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $cat = new FreshRSS_Category ($name); $values = array ( 'name' => $cat->name (), - 'color' => $cat->color () ); $catDAO->updateCategory ($ids[$key], $values); } elseif ($ids[$key] != $defaultId) { @@ -43,10 +42,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $values = array ( 'id' => $cat->id (), 'name' => $cat->name (), - 'color' => $cat->color () ); - if ($catDAO->searchByName ($newCat) == false) { + if ($catDAO->searchByName ($newCat) == null) { $catDAO->addCategory ($values); } } @@ -64,7 +62,6 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $this->view->categories = $catDAO->listCategories (false); $this->view->defaultCategory = $catDAO->getDefault (); $this->view->feeds = $feedDAO->listFeeds (); - $this->view->flux = false; Minz_View::prependTitle (Minz_Translate::t ('categories_management') . ' · '); } @@ -116,7 +113,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { if ($feedDAO->updateFeed ($id, $values)) { $this->view->flux->_category ($cat); - + $this->view->flux->faviconPrepare(); $notif = array ( 'type' => 'good', 'content' => Minz_Translate::t ('feed_updated') @@ -143,25 +140,12 @@ class FreshRSS_configure_Controller extends Minz_ActionController { public function displayAction () { if (Minz_Request::isPost()) { $this->view->conf->_language(Minz_Request::param('language', 'en')); - $this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10)); - $this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal')); - $this->view->conf->_default_view (Minz_Request::param('default_view', 'a')); - $this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false)); - $this->view->conf->_display_posts(Minz_Request::param('display_posts', false)); - $this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false)); - $this->view->conf->_lazyload (Minz_Request::param('lazyload', false)); - $this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC')); - $this->view->conf->_mark_when (array( - 'article' => Minz_Request::param('mark_open_article', false), - 'site' => Minz_Request::param('mark_open_site', false), - 'scroll' => Minz_Request::param('mark_scroll', false), - 'reception' => Minz_Request::param('mark_upon_reception', false), - )); $themeId = Minz_Request::param('theme', ''); if ($themeId == '') { $themeId = FreshRSS_Themes::defaultTheme; } $this->view->conf->_theme($themeId); + $this->view->conf->_content_width(Minz_Request::param('content_width', 'thin')); $this->view->conf->_topline_read(Minz_Request::param('topline_read', false)); $this->view->conf->_topline_favorite(Minz_Request::param('topline_favorite', false)); $this->view->conf->_topline_date(Minz_Request::param('topline_date', false)); @@ -189,21 +173,48 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $this->view->themes = FreshRSS_Themes::get(); + Minz_View::prependTitle (Minz_Translate::t ('display_configuration') . ' · '); + } + + public function readingAction () { + if (Minz_Request::isPost()) { + $this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10)); + $this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal')); + $this->view->conf->_default_view (Minz_Request::param('default_view', 'a')); + $this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false)); + $this->view->conf->_display_posts(Minz_Request::param('display_posts', false)); + $this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false)); + $this->view->conf->_lazyload (Minz_Request::param('lazyload', false)); + $this->view->conf->_sticky_post (Minz_Request::param('sticky_post', false)); + $this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC')); + $this->view->conf->_mark_when (array( + 'article' => Minz_Request::param('mark_open_article', false), + 'site' => Minz_Request::param('mark_open_site', false), + 'scroll' => Minz_Request::param('mark_scroll', false), + 'reception' => Minz_Request::param('mark_upon_reception', false), + )); + $this->view->conf->save(); + + Minz_Session::_param ('language', $this->view->conf->language); + Minz_Translate::reset (); + invalidateHttpCache(); + + $notif = array ( + 'type' => 'good', + 'content' => Minz_Translate::t ('configuration_updated') + ); + Minz_Session::_param ('notification', $notif); + + Minz_Request::forward (array ('c' => 'configure', 'a' => 'reading'), true); + } + Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' · '); } public function sharingAction () { if (Minz_Request::isPost ()) { - $this->view->conf->_sharing (array( - 'shaarli' => Minz_Request::param ('shaarli', false), - 'wallabag' => Minz_Request::param ('wallabag', false), - 'diaspora' => Minz_Request::param ('diaspora', false), - 'twitter' => Minz_Request::param ('twitter', false), - 'g+' => Minz_Request::param ('g+', false), - 'facebook' => Minz_Request::param ('facebook', false), - 'email' => Minz_Request::param ('email', false), - 'print' => Minz_Request::param ('print', false), - )); + $params = Minz_Request::params(); + $this->view->conf->_sharing ($params['share']); $this->view->conf->save(); invalidateHttpCache(); @@ -219,74 +230,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController { Minz_View::prependTitle (Minz_Translate::t ('sharing') . ' · '); } - public function importExportAction () { - require_once(LIB_PATH . '/lib_opml.php'); - $catDAO = new FreshRSS_CategoryDAO (); - $this->view->categories = $catDAO->listCategories (); - - $this->view->req = Minz_Request::param ('q'); - - if ($this->view->req == 'export') { - Minz_View::_title ('freshrss_feeds.opml'); - - $this->view->_useLayout (false); - header('Content-Type: application/xml; charset=utf-8'); - header('Content-disposition: attachment; filename=freshrss_feeds.opml'); - - $feedDAO = new FreshRSS_FeedDAO (); - $catDAO = new FreshRSS_CategoryDAO (); - - $list = array (); - foreach ($catDAO->listCategories () as $key => $cat) { - $list[$key]['name'] = $cat->name (); - $list[$key]['feeds'] = $feedDAO->listByCategory ($cat->id ()); - } - - $this->view->categories = $list; - } elseif ($this->view->req == 'import' && Minz_Request::isPost ()) { - if ($_FILES['file']['error'] == 0) { - invalidateHttpCache(); - // on parse le fichier OPML pour récupérer les catégories et les flux associés - try { - list ($categories, $feeds) = opml_import ( - file_get_contents ($_FILES['file']['tmp_name']) - ); - - // On redirige vers le controller feed qui va se charger d'insérer les flux en BDD - // les flux sont mis au préalable dans des variables de Request - Minz_Request::_param ('q', 'null'); - Minz_Request::_param ('categories', $categories); - Minz_Request::_param ('feeds', $feeds); - Minz_Request::forward (array ('c' => 'feed', 'a' => 'massiveImport')); - } catch (FreshRSS_Opml_Exception $e) { - Minz_Log::record ($e->getMessage (), Minz_Log::WARNING); - - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('bad_opml_file') - ); - Minz_Session::_param ('notification', $notif); - - Minz_Request::forward (array ( - 'c' => 'configure', - 'a' => 'importExport' - ), true); - } - } - } - - $feedDAO = new FreshRSS_FeedDAO (); - $this->view->feeds = $feedDAO->listFeeds (); - - // au niveau de la vue, permet de ne pas voir un flux sélectionné dans la liste - $this->view->flux = false; - - Minz_View::prependTitle (Minz_Translate::t ('import_export_opml') . ' · '); - } - public function shortcutAction () { $list_keys = array ('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter', - 'escape', 'f', 'g', 'h', 'i', 'insert', 'j', 'k', 'l', 'left', + 'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left', 'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right', 's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index 1756c91e5..bbcb990f5 100755 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -100,6 +100,9 @@ class FreshRSS_entry_Controller extends Minz_ActionController { $entryDAO = new FreshRSS_EntryDAO(); $entryDAO->optimizeTable(); + $feedDAO = new FreshRSS_FeedDAO(); + $feedDAO->updateCachedValues(); + invalidateHttpCache(); $notif = array ( @@ -137,11 +140,13 @@ class FreshRSS_entry_Controller extends Minz_ActionController { if ($nb > 0) { $nbTotal += $nb; Minz_Log::record($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG); - $feedDAO->updateLastUpdate($feed->id()); + //$feedDAO->updateLastUpdate($feed->id()); } } } + $feedDAO->updateCachedValues(); + invalidateHttpCache(); $notif = array( diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index c40b3c400..fce008399 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -3,28 +3,51 @@ class FreshRSS_feed_Controller extends Minz_ActionController { public function firstAction () { if (!$this->view->loginOk) { - $token = $this->view->conf->token; //TODO: check the token logic again, and if it is still needed + // Token is useful in the case that anonymous refresh is forbidden + // and CRON task cannot be used with php command so the user can + // set a CRON task to refresh his feeds by using token inside url + $token = $this->view->conf->token; $token_param = Minz_Request::param ('token', ''); $token_is_ok = ($token != '' && $token == $token_param); $action = Minz_Request::actionName (); - if (!($token_is_ok && $action === 'actualize')) { + if (!(($token_is_ok || Minz_Configuration::allowAnonymousRefresh()) && + $action === 'actualize') + ) { Minz_Error::error ( 403, array ('error' => array (Minz_Translate::t ('access_denied'))) ); } } - - $this->catDAO = new FreshRSS_CategoryDAO (); - $this->catDAO->checkDefault (); } public function addAction () { - @set_time_limit(300); + $url = Minz_Request::param('url_rss', false); + + if ($url === false) { + Minz_Request::forward(array( + 'c' => 'configure', + 'a' => 'feed' + ), true); + } + + $feedDAO = new FreshRSS_FeedDAO (); + $this->catDAO = new FreshRSS_CategoryDAO (); + $this->catDAO->checkDefault (); + + if (Minz_Request::isPost()) { + @set_time_limit(300); + - if (Minz_Request::isPost ()) { - $url = Minz_Request::param ('url_rss'); $cat = Minz_Request::param ('category', false); + if ($cat === 'nc') { + $new_cat = Minz_Request::param ('new_category'); + if (empty($new_cat['name'])) { + $cat = false; + } else { + $cat = $this->catDAO->addCategory($new_cat); + } + } if ($cat === false) { $def_cat = $this->catDAO->getDefault (); $cat = $def_cat->id (); @@ -47,7 +70,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed->load(true); - $feedDAO = new FreshRSS_FeedDAO (); $values = array ( 'url' => $feed->url (), 'category' => $feed->category (), @@ -123,7 +145,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { Minz_Log::record ($e->getMessage (), Minz_Log::WARNING); $notif = array ( 'type' => 'bad', - 'content' => Minz_Translate::t ('internal_problem_feed') + 'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs'))) ); Minz_Session::_param ('notification', $notif); } catch (Minz_FileNotExistException $e) { @@ -131,7 +153,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { Minz_Log::record ($e->getMessage (), Minz_Log::ERROR); $notif = array ( 'type' => 'bad', - 'content' => Minz_Translate::t ('internal_problem_feed') + 'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs'))) ); Minz_Session::_param ('notification', $notif); } @@ -141,6 +163,38 @@ class FreshRSS_feed_Controller extends Minz_ActionController { Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true); } + + // GET request so we must ask confirmation to user + Minz_View::prependTitle(Minz_Translate::t('add_rss_feed') . ' · '); + $this->view->categories = $this->catDAO->listCategories(); + $this->view->feed = new FreshRSS_Feed($url); + try { + // We try to get some more information about the feed + $this->view->feed->load(true); + $this->view->load_ok = true; + } catch (Exception $e) { + $this->view->load_ok = false; + } + + $feed = $feedDAO->searchByUrl($this->view->feed->url()); + if ($feed) { + // Already subscribe so we redirect to the feed configuration page + $notif = array( + 'type' => 'bad', + 'content' => Minz_Translate::t( + 'already_subscribed', $feed->name() + ) + ); + Minz_Session::_param('notification', $notif); + + Minz_Request::forward(array( + 'c' => 'configure', + 'a' => 'feed', + 'params' => array( + 'id' => $feed->id() + ) + ), true); + } } public function truncateAction () { @@ -189,38 +243,51 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $flux_update = 0; $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { + if (!$feed->lock()) { + Minz_Log::record('Feed already being actualized: ' . $feed->url(), Minz_Log::NOTICE); + continue; + } try { $url = $feed->url(); + $feedHistory = $feed->keepHistory(); + $feed->load(false); $entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order + $hasTransaction = false; - //For this feed, check last n entry GUIDs already in database - $existingGuids = array_fill_keys ($entryDAO->listLastGuidsByFeed ($feed->id (), count($entries) + 10), 1); - $useDeclaredDate = empty($existingGuids); + if (count($entries) > 0) { + //For this feed, check last n entry GUIDs already in database + $existingGuids = array_fill_keys ($entryDAO->listLastGuidsByFeed ($feed->id (), count($entries) + 10), 1); + $useDeclaredDate = empty($existingGuids); - $feedHistory = $feed->keepHistory(); - if ($feedHistory == -2) { //default - $feedHistory = $this->view->conf->keep_history_default; - } + if ($feedHistory == -2) { //default + $feedHistory = $this->view->conf->keep_history_default; + } - // On ne vérifie pas strictement que l'article n'est pas déjà en BDD - // La BDD refusera l'ajout car (id_feed, guid) doit être unique - $feedDAO->beginTransaction (); - foreach ($entries as $entry) { - $eDate = $entry->date (true); - if ((!isset ($existingGuids[$entry->guid ()])) && - (($feedHistory != 0) || ($eDate >= $date_min))) { - $values = $entry->toArray (); - //Use declared date at first import, otherwise use discovery date - $values['id'] = ($useDeclaredDate || $eDate < $date_min) ? - min(time(), $eDate) . uSecString() : - uTimeString(); - $values['is_read'] = $is_read; - $entryDAO->addEntry ($values); + $hasTransaction = true; + $feedDAO->beginTransaction(); + + // On ne vérifie pas strictement que l'article n'est pas déjà en BDD + // La BDD refusera l'ajout car (id_feed, guid) doit être unique + foreach ($entries as $entry) { + $eDate = $entry->date (true); + if ((!isset ($existingGuids[$entry->guid ()])) && + (($feedHistory != 0) || ($eDate >= $date_min))) { + $values = $entry->toArray (); + //Use declared date at first import, otherwise use discovery date + $values['id'] = ($useDeclaredDate || $eDate < $date_min) ? + min(time(), $eDate) . uSecString() : + uTimeString(); + $values['is_read'] = $is_read; + $entryDAO->addEntry ($values); + } } } if (($feedHistory >= 0) && (rand(0, 30) === 1)) { + if (!$hasTransaction) { + $feedDAO->beginTransaction(); + } $nb = $feedDAO->cleanOldEntries ($feed->id (), $date_min, max($feedHistory, count($entries) + 10)); if ($nb > 0) { Minz_Log::record ($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG); @@ -228,18 +295,23 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } // on indique que le flux vient d'être mis à jour en BDD - $feedDAO->updateLastUpdate ($feed->id ()); - $feedDAO->commit (); + $feedDAO->updateLastUpdate ($feed->id (), 0, $hasTransaction); + if ($hasTransaction) { + $feedDAO->commit(); + } $flux_update++; if ($feed->url() !== $url) { //URL has changed (auto-discovery) $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); } - $feed->faviconPrepare(); } catch (FreshRSS_Feed_Exception $e) { Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE); $feedDAO->updateLastUpdate ($feed->id (), 1); } + $feed->faviconPrepare(); + $feed->unlock(); + unset($feed); + // On arrête à 10 flux pour ne pas surcharger le serveur // sauf si le paramètre $force est à vrai $i++; @@ -251,6 +323,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $url = array (); if ($flux_update === 1) { // on a mis un seul flux à jour + $feed = reset ($feeds); $notif = array ( 'type' => 'good', 'content' => Minz_Translate::t ('feed_actualized', $feed->name ()) @@ -264,8 +337,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } else { // aucun flux n'a été mis à jour, oups $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('no_feed_actualized') + 'type' => 'good', + 'content' => Minz_Translate::t ('no_feed_to_refresh') ); } @@ -295,77 +368,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } - public function massiveImportAction () { - @set_time_limit(300); - - $entryDAO = new FreshRSS_EntryDAO (); - $feedDAO = new FreshRSS_FeedDAO (); - - $categories = Minz_Request::param ('categories', array (), true); - $feeds = Minz_Request::param ('feeds', array (), true); - - // on ajoute les catégories en masse dans une fonction à part - $this->addCategories ($categories); - - // on calcule la date des articles les plus anciens qu'on accepte - $nb_month_old = $this->view->conf->old_entries; - $date_min = time () - (3600 * 24 * 30 * $nb_month_old); - - // la variable $error permet de savoir si une erreur est survenue - // Le but est de ne pas arrêter l'import même en cas d'erreur - // L'utilisateur sera mis au courant s'il y a eu des erreurs, mais - // ne connaîtra pas les détails. Ceux-ci seront toutefois logguées - $error = false; - $i = 0; - foreach ($feeds as $feed) { - try { - $values = array ( - 'id' => $feed->id (), - 'url' => $feed->url (), - 'category' => $feed->category (), - 'name' => $feed->name (), - 'website' => $feed->website (), - 'description' => $feed->description (), - 'lastUpdate' => 0, - 'httpAuth' => $feed->httpAuth () - ); - - // ajout du flux que s'il n'est pas déjà en BDD - if (!$feedDAO->searchByUrl ($values['url'])) { - $id = $feedDAO->addFeed ($values); - if ($id) { - $feed->_id ($id); - $feed->faviconPrepare(); - } else { - $error = true; - } - } - } catch (FreshRSS_Feed_Exception $e) { - $error = true; - Minz_Log::record ($e->getMessage (), Minz_Log::WARNING); - } - } - - if ($error) { - $res = Minz_Translate::t ('feeds_imported_with_errors'); - } else { - $res = Minz_Translate::t ('feeds_imported'); - } - - $notif = array ( - 'type' => 'good', - 'content' => $res - ); - Minz_Session::_param ('notification', $notif); - Minz_Session::_param ('actualize_feeds', true); - - // et on redirige vers la page d'accueil - Minz_Request::forward (array ( - 'c' => 'index', - 'a' => 'index' - ), true); - } - public function deleteAction () { if (Minz_Request::isPost ()) { $type = Minz_Request::param ('type', 'feed'); @@ -409,17 +411,4 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } } - - private function addCategories ($categories) { - foreach ($categories as $cat) { - if (!$this->catDAO->searchByName ($cat->name ())) { - $values = array ( - 'id' => $cat->id (), - 'name' => $cat->name (), - 'color' => $cat->color () - ); - $catDAO->addCategory ($values); - } - } - } } diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php new file mode 100644 index 000000000..3cd791781 --- /dev/null +++ b/app/Controllers/importExportController.php @@ -0,0 +1,390 @@ +view->loginOk) { + Minz_Error::error( + 403, + array('error' => array(Minz_Translate::t('access_denied'))) + ); + } + + require_once(LIB_PATH . '/lib_opml.php'); + + $this->catDAO = new FreshRSS_CategoryDAO(); + $this->entryDAO = new FreshRSS_EntryDAO(); + $this->feedDAO = new FreshRSS_FeedDAO(); + } + + public function indexAction() { + $this->view->categories = $this->catDAO->listCategories(); + $this->view->feeds = $this->feedDAO->listFeeds(); + + Minz_View::prependTitle(Minz_Translate::t('import_export') . ' · '); + } + + public function importAction() { + if (Minz_Request::isPost() && $_FILES['file']['error'] == 0) { + @set_time_limit(300); + + $file = $_FILES['file']; + $type_file = $this->guessFileType($file['name']); + + $list_files = array( + 'opml' => array(), + 'json_starred' => array(), + 'json_feed' => array() + ); + + // We try to list all files according to their type + // A zip file is first opened and then its files are listed + $list = array(); + if ($type_file === 'zip') { + $zip = zip_open($file['tmp_name']); + + while (($zipfile = zip_read($zip)) !== false) { + $type_zipfile = $this->guessFileType( + zip_entry_name($zipfile) + ); + + if ($type_file !== 'unknown') { + $list_files[$type_zipfile][] = zip_entry_read( + $zipfile, + zip_entry_filesize($zipfile) + ); + } + } + + zip_close($zip); + } elseif ($type_file !== 'unknown') { + $list_files[$type_file][] = file_get_contents( + $file['tmp_name'] + ); + } + + // Import different files. + // OPML first(so categories and feeds are imported) + // Starred articles then so the "favourite" status is already set + // And finally all other files. + $error = false; + foreach ($list_files['opml'] as $opml_file) { + $error = $this->importOpml($opml_file); + } + foreach ($list_files['json_starred'] as $article_file) { + $error = $this->importArticles($article_file, true); + } + foreach ($list_files['json_feed'] as $article_file) { + $error = $this->importArticles($article_file); + } + + // And finally, we get import status and redirect to the home page + $notif = null; + if ($error === true) { + $content_notif = Minz_Translate::t( + 'feeds_imported_with_errors' + ); + } else { + $content_notif = Minz_Translate::t( + 'feeds_imported' + ); + } + + Minz_Session::_param('notification', array( + 'type' => 'good', + 'content' => $content_notif + )); + Minz_Session::_param('actualize_feeds', true); + + Minz_Request::forward(array( + 'c' => 'index', + 'a' => 'index' + ), true); + } + + // What are you doing? you have to call this controller + // with a POST request! + Minz_Request::forward(array( + 'c' => 'importExport', + 'a' => 'index' + )); + } + + private function guessFileType($filename) { + // A *very* basic guess file type function. Only based on filename + // That's could be improved but should be enough, at least for a first + // implementation. + // TODO: improve this function? + + if (substr_compare($filename, '.zip', -4) === 0) { + return 'zip'; + } elseif (substr_compare($filename, '.opml', -5) === 0 || + substr_compare($filename, '.xml', -4) === 0) { + return 'opml'; + } elseif (strcmp($filename, 'starred.json') === 0) { + return 'json_starred'; + } elseif (substr_compare($filename, '.json', -5) === 0 && + strpos($filename, 'feed_') === 0) { + return 'json_feed'; + } else { + return 'unknown'; + } + } + + private function importOpml($opml_file) { + $opml_array = array(); + try { + $opml_array = libopml_parse_string($opml_file); + } catch (LibOPML_Exception $e) { + Minz_Log::warning($e->getMessage()); + return true; + } + + $this->catDAO->checkDefault(); + + return $this->addOpmlElements($opml_array['body']); + } + + private function addOpmlElements($opml_elements, $parent_cat = null) { + $error = false; + foreach ($opml_elements as $elt) { + $res = false; + if (isset($elt['xmlUrl'])) { + $res = $this->addFeedOpml($elt, $parent_cat); + } else { + $res = $this->addCategoryOpml($elt, $parent_cat); + } + + if (!$error && $res) { + // oops: there is at least one error! + $error = $res; + } + } + + return $error; + } + + private function addFeedOpml($feed_elt, $parent_cat) { + if (is_null($parent_cat)) { + // This feed has no parent category so we get the default one + $parent_cat = $this->catDAO->getDefault()->name(); + } + + $cat = $this->catDAO->searchByName($parent_cat); + + if (!$cat) { + return true; + } + + // We get different useful information + $url = html_chars_utf8($feed_elt['xmlUrl']); + $name = html_chars_utf8($feed_elt['text']); + $website = ''; + if (isset($feed_elt['htmlUrl'])) { + $website = html_chars_utf8($feed_elt['htmlUrl']); + } + $description = ''; + if (isset($feed_elt['description'])) { + $description = html_chars_utf8($feed_elt['description']); + } + + $error = false; + try { + // Create a Feed object and add it in DB + $feed = new FreshRSS_Feed($url); + $feed->_category($cat->id()); + $feed->_name($name); + $feed->_website($website); + $feed->_description($description); + + // addFeedObject checks if feed is already in DB so nothing else to + // check here + $id = $this->feedDAO->addFeedObject($feed); + $error = ($id === false); + } catch (FreshRSS_Feed_Exception $e) { + Minz_Log::warning($e->getMessage()); + $error = true; + } + + return $error; + } + + private function addCategoryOpml($cat_elt, $parent_cat) { + // Create a new Category object + $cat = new FreshRSS_Category(html_chars_utf8($cat_elt['text'])); + + $id = $this->catDAO->addCategoryObject($cat); + $error = ($id === false); + + if (isset($cat_elt['@outlines'])) { + // Our cat_elt contains more categories or more feeds, so we + // add them recursively. + // Note: FreshRSS does not support yet category arborescence + $res = $this->addOpmlElements($cat_elt['@outlines'], $cat->name()); + if (!$error && $res) { + $error = true; + } + } + + return $error; + } + + private function importArticles($article_file, $starred = false) { + $article_object = json_decode($article_file, true); + if (is_null($article_object)) { + Minz_Log::warning('Try to import a non-JSON file'); + return true; + } + + $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0; + + $google_compliant = ( + strpos($article_object['id'], 'com.google') !== false + ); + + $error = false; + foreach ($article_object['items'] as $item) { + $feed = $this->addFeedArticles($item['origin'], $google_compliant); + if (is_null($feed)) { + $error = true; + continue; + } + + $author = isset($item['author']) ? $item['author'] : ''; + $key_content = ($google_compliant && !isset($item['content'])) ? + 'summary' : 'content'; + $tags = $item['categories']; + if ($google_compliant) { + $tags = array_filter($tags, function($var) { + return strpos($var, '/state/com.google') === false; + }); + } + + $entry = new FreshRSS_Entry( + $feed->id(), $item['id'], $item['title'], $author, + $item[$key_content]['content'], $item['alternate'][0]['href'], + $item['published'], $is_read, $starred + ); + $entry->_tags($tags); + + $id = $this->entryDAO->addEntryObject( + $entry, $this->view->conf, $feed->keepHistory() + ); + + if (!$error && ($id === false)) { + $error = true; + } + } + + return $error; + } + + private function addFeedArticles($origin, $google_compliant) { + $default_cat = $this->catDAO->getDefault(); + + $return = null; + $key = $google_compliant ? 'htmlUrl' : 'feedUrl'; + $url = $origin[$key]; + $name = $origin['title']; + $website = $origin['htmlUrl']; + $error = false; + try { + // Create a Feed object and add it in DB + $feed = new FreshRSS_Feed($url); + $feed->_category($default_cat->id()); + $feed->_name($name); + $feed->_website($website); + + // addFeedObject checks if feed is already in DB so nothing else to + // check here + $id = $this->feedDAO->addFeedObject($feed); + + if ($id !== false) { + $feed->_id($id); + $return = $feed; + } + } catch (FreshRSS_Feed_Exception $e) { + Minz_Log::warning($e->getMessage()); + } + + return $return; + } + + public function exportAction() { + if (Minz_Request::isPost()) { + $this->view->_useLayout(false); + + $export_opml = Minz_Request::param('export_opml', false); + $export_starred = Minz_Request::param('export_starred', false); + $export_feeds = Minz_Request::param('export_feeds', false); + + // From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly + $file = tempnam('tmp', 'zip'); + $zip = new ZipArchive(); + $zip->open($file, ZipArchive::OVERWRITE); + + // Stuff with content + if ($export_opml) { + $zip->addFromString( + 'feeds.opml', $this->generateOpml() + ); + } + if ($export_starred) { + $zip->addFromString( + 'starred.json', $this->generateArticles('starred') + ); + } + foreach ($export_feeds as $feed_id) { + $feed = $this->feedDAO->searchById($feed_id); + $zip->addFromString( + 'feed_' . $feed->category() . '_' . $feed->id() . '.json', + $this->generateArticles('feed', $feed) + ); + } + + // Close and send to user + $zip->close(); + header('Content-Type: application/zip'); + header('Content-Length: ' . filesize($file)); + header('Content-Disposition: attachment; filename="freshrss_export.zip"'); + readfile($file); + unlink($file); + } + } + + private function generateOpml() { + $list = array(); + foreach ($this->catDAO->listCategories() as $key => $cat) { + $list[$key]['name'] = $cat->name(); + $list[$key]['feeds'] = $this->feedDAO->listByCategory($cat->id()); + } + + $this->view->categories = $list; + return $this->view->helperToString('export/opml'); + } + + private function generateArticles($type, $feed = NULL) { + $this->view->categories = $this->catDAO->listCategories(); + + if ($type == 'starred') { + $this->view->list_title = Minz_Translate::t('starred_list'); + $this->view->type = 'starred'; + $unread_fav = $this->entryDAO->countUnreadReadFavorites(); + $this->view->entries = $this->entryDAO->listWhere( + 's', '', FreshRSS_Entry::STATE_ALL, 'ASC', + $unread_fav['all'] + ); + } elseif ($type == 'feed' && !is_null($feed)) { + $this->view->list_title = Minz_Translate::t( + 'feed_list', $feed->name() + ); + $this->view->type = 'feed/' . $feed->id(); + $this->view->entries = $this->entryDAO->listWhere( + 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', + $this->view->conf->posts_per_page + ); + $this->view->feed = $feed; + } + + return $this->view->helperToString('export/articles'); + } +} diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index 986a322a1..c843748c3 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -5,27 +5,32 @@ class FreshRSS_index_Controller extends Minz_ActionController { public function indexAction () { $output = Minz_Request::param ('output'); - $token = ''; + $token = $this->view->conf->token; // check if user is logged in - if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) - { - $token = $this->view->conf->token; + if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) { $token_param = Minz_Request::param ('token', ''); $token_is_ok = ($token != '' && $token === $token_param); - if (!($output === 'rss' && $token_is_ok)) { + if ($output === 'rss' && !$token_is_ok) { + Minz_Error::error ( + 403, + array ('error' => array (Minz_Translate::t ('access_denied'))) + ); + return; + } elseif ($output !== 'rss') { + // "hard" redirection is not required, just ask dispatcher to + // forward to the login form without 302 redirection + Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin')); return; } - $params['token'] = $token; } - // construction of RSS url of this feed $params = Minz_Request::params (); - $params['output'] = 'rss'; if (isset ($params['search'])) { $params['search'] = urlencode ($params['search']); } - $this->view->rss_url = array ( + + $this->view->url = array ( 'c' => 'index', 'a' => 'index', 'params' => $params @@ -75,20 +80,22 @@ class FreshRSS_index_Controller extends Minz_ActionController { // On récupère les différents éléments de filtrage $this->view->state = $state = Minz_Request::param ('state', $this->view->conf->default_view); + $state_param = Minz_Request::param ('state', null); $filter = Minz_Request::param ('search', ''); if (!empty($filter)) { - $state = 'all'; //Search always in read and unread articles + $state = FreshRSS_Entry::STATE_ALL; //Search always in read and unread articles } $this->view->order = $order = Minz_Request::param ('order', $this->view->conf->sort_order); $nb = Minz_Request::param ('nb', $this->view->conf->posts_per_page); $first = Minz_Request::param ('next', ''); - if ($state === 'not_read') { //Any unread article in this category at all? + if ($state === FreshRSS_Entry::STATE_NOT_READ) { //Any unread article in this category at all? switch ($getType) { case 'a': $hasUnread = $this->view->nb_not_read > 0; break; case 's': + // This is deprecated. The favorite button does not exist anymore $hasUnread = $this->view->nb_favorites['unread'] > 0; break; case 'c': @@ -102,8 +109,8 @@ class FreshRSS_index_Controller extends Minz_ActionController { $hasUnread = true; break; } - if (!$hasUnread) { - $this->view->state = $state = 'all'; + if (!$hasUnread && ($state_param === null)) { + $this->view->state = $state = FreshRSS_Entry::STATE_ALL; } } @@ -116,14 +123,14 @@ class FreshRSS_index_Controller extends Minz_ActionController { $keepHistoryDefault = $this->view->conf->keep_history_default; try { - $entries = $entryDAO->listWhere($getType, $getId, $state, $order, $nb + 1, $first, $filter, $date_min, $keepHistoryDefault); + $entries = $entryDAO->listWhere($getType, $getId, $state, $order, $nb + 1, $first, $filter, $date_min, true, $keepHistoryDefault); // Si on a récupéré aucun article "non lus" // on essaye de récupérer tous les articles - if ($state === 'not_read' && empty($entries)) { + if ($state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null)) { Minz_Log::record ('Conflicting information about nbNotRead!', Minz_Log::DEBUG); - $this->view->state = 'all'; - $entries = $entryDAO->listWhere($getType, $getId, 'all', $order, $nb, $first, $filter, $date_min, $keepHistoryDefault); + $this->view->state = FreshRSS_Entry::STATE_ALL; + $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter, $date_min, true, $keepHistoryDefault); } if (count($entries) <= $nb) { @@ -342,6 +349,37 @@ class FreshRSS_index_Controller extends Minz_ActionController { } $this->view->_useLayout(false); Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); + } elseif (Minz_Configuration::unsafeAutologinEnabled() && isset($_GET['u']) && isset($_GET['p'])) { + Minz_Session::_param('currentUser'); + Minz_Session::_param('mail'); + Minz_Session::_param('passwordHash'); + $username = ctype_alnum($_GET['u']) ? $_GET['u'] : ''; + $passwordPlain = $_GET['p']; + Minz_Request::_param('p'); //Discard plain-text password ASAP + $_GET['p'] = ''; + if (!function_exists('password_verify')) { + include_once(LIB_PATH . '/password_compat.php'); + } + try { + $conf = new FreshRSS_Configuration($username); + $s = $conf->passwordHash; + $ok = password_verify($passwordPlain, $s); + unset($passwordPlain); + if ($ok) { + Minz_Session::_param('currentUser', $username); + Minz_Session::_param('passwordHash', $s); + } else { + Minz_Log::record('Unsafe password mismatch for user ' . $username, Minz_Log::WARNING); + } + } catch (Minz_Exception $me) { + Minz_Log::record('Unsafe login failure: ' . $me->getMessage(), Minz_Log::WARNING); + } + Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); + } elseif (!Minz_Configuration::canLogIn()) { + Minz_Error::error ( + 403, + array ('error' => array (Minz_Translate::t ('access_denied'))) + ); } invalidateHttpCache(); } diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index b879dcd6d..3d741e298 100755 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -8,7 +8,7 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { public function actualizeAction () { header('Content-Type: text/javascript; charset=UTF-8'); $feedDAO = new FreshRSS_FeedDAO (); - $this->view->feeds = $feedDAO->listFeeds (); + $this->view->feeds = $feedDAO->listFeedsOrderUpdate(); } public function nbUnreadsPerFeedAction() { diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php index 8314b75fc..fa967cedc 100644 --- a/app/Controllers/usersController.php +++ b/app/Controllers/usersController.php @@ -32,6 +32,18 @@ class FreshRSS_users_Controller extends Minz_ActionController { } Minz_Session::_param('passwordHash', $this->view->conf->passwordHash); + $passwordPlain = Minz_Request::param('apiPasswordPlain', false); + if ($passwordPlain != '') { + if (!function_exists('password_hash')) { + include_once(LIB_PATH . '/password_compat.php'); + } + $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST)); + $passwordPlain = ''; + $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js + $ok &= ($passwordHash != ''); + $this->view->conf->_apiPasswordHash($passwordHash); + } + if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { $this->view->conf->_mail_login(Minz_Request::param('mail_login', false)); } @@ -54,11 +66,22 @@ class FreshRSS_users_Controller extends Minz_ActionController { $anon = Minz_Request::param('anon_access', false); $anon = ((bool)$anon) && ($anon !== 'no'); + $anon_refresh = Minz_Request::param('anon_refresh', false); + $anon_refresh = ((bool)$anon_refresh) && ($anon_refresh !== 'no'); $auth_type = Minz_Request::param('auth_type', 'none'); + $unsafe_autologin = Minz_Request::param('unsafe_autologin', false); + $api_enabled = Minz_Request::param('api_enabled', false); if ($anon != Minz_Configuration::allowAnonymous() || - $auth_type != Minz_Configuration::authType()) { + $auth_type != Minz_Configuration::authType() || + $anon_refresh != Minz_Configuration::allowAnonymousRefresh() || + $unsafe_autologin != Minz_Configuration::unsafeAutologinEnabled() || + $api_enabled != Minz_Configuration::apiEnabled()) { + Minz_Configuration::_authType($auth_type); Minz_Configuration::_allowAnonymous($anon); + Minz_Configuration::_allowAnonymousRefresh($anon_refresh); + Minz_Configuration::_enableAutologin($unsafe_autologin); + Minz_Configuration::_enableApi($api_enabled); $ok &= Minz_Configuration::writeFile(); } } diff --git a/app/Exceptions/OpmlException.php b/app/Exceptions/OpmlException.php deleted file mode 100644 index e0ea3e493..000000000 --- a/app/Exceptions/OpmlException.php +++ /dev/null @@ -1,6 +0,0 @@ -_name ($name); - $this->_color ($color); if (isset ($feeds)) { $this->_feeds ($feeds); $this->nbFeed = 0; @@ -28,9 +26,6 @@ class FreshRSS_Category extends Minz_Model { public function name () { return $this->name; } - public function color () { - return $this->color; - } public function nbFeed () { if ($this->nbFeed < 0) { $catDAO = new FreshRSS_CategoryDAO (); @@ -68,13 +63,6 @@ class FreshRSS_Category extends Minz_Model { public function _name ($value) { $this->name = $value; } - public function _color ($value) { - if (preg_match ('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) { - $this->color = $value; - } else { - $this->color = '#0062BE'; - } - } public function _feeds ($values) { if (!is_array ($values)) { $values = array ($values); diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 1cc616ac0..6a9b839b9 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -2,12 +2,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { public function addCategory ($valuesTmp) { - $sql = 'INSERT INTO `' . $this->prefix . 'category` (name, color) VALUES(?, ?)'; + $sql = 'INSERT INTO `' . $this->prefix . 'category` (name) VALUES(?)'; $stm = $this->bd->prepare ($sql); $values = array ( substr($valuesTmp['name'], 0, 255), - substr($valuesTmp['color'], 0, 7), ); if ($stm && $stm->execute ($values)) { @@ -19,13 +18,25 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { } } + public function addCategoryObject($category) { + $cat = $this->searchByName($category->name()); + if (!$cat) { + // Category does not exist yet in DB so we add it before continue + $values = array( + 'name' => $category->name(), + ); + return $this->addCategory($values); + } + + return $cat->id(); + } + public function updateCategory ($id, $valuesTmp) { - $sql = 'UPDATE `' . $this->prefix . 'category` SET name=?, color=? WHERE id=?'; + $sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?'; $stm = $this->bd->prepare ($sql); $values = array ( $valuesTmp['name'], - $valuesTmp['color'], $id ); @@ -66,7 +77,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if (isset ($cat[0])) { return $cat[0]; } else { - return false; + return null; } } public function searchByName ($name) { @@ -82,14 +93,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if (isset ($cat[0])) { return $cat[0]; } else { - return false; + return null; } } public function listCategories ($prePopulateFeeds = true, $details = false) { if ($prePopulateFeeds) { $sql = 'SELECT c.id AS c_id, c.name AS c_name, ' - . ($details ? 'c.color AS c_color, ' : '') . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ') . 'FROM `' . $this->prefix . 'category` c ' . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category = c.id ' @@ -123,14 +133,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { public function checkDefault () { $def_cat = $this->searchById (1); - if ($def_cat === false) { + if ($def_cat == null) { $cat = new FreshRSS_Category (Minz_Translate::t ('default_category')); $cat->_id (1); $values = array ( 'id' => $cat->id (), 'name' => $cat->name (), - 'color' => $cat->color () ); $this->addCategory ($values); @@ -203,7 +212,6 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { // End of the current category, we add it to the $list $cat = new FreshRSS_Category ( $previousLine['c_name'], - isset($previousLine['c_color']) ? $previousLine['c_color'] : '', FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id']) ); $cat->_id ($previousLine['c_id']); @@ -220,7 +228,6 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if ($previousLine != null) { $cat = new FreshRSS_Category ( $previousLine['c_name'], - isset($previousLine['c_color']) ? $previousLine['c_color'] : '', FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id']) ); $cat->_id ($previousLine['c_id']); @@ -239,8 +246,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { foreach ($listDAO as $key => $dao) { $cat = new FreshRSS_Category ( - $dao['name'], - $dao['color'] + $dao['name'] ); $cat->_id ($dao['id']); $list[$key] = $cat; diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 2a7fe95aa..0d6666297 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -10,13 +10,15 @@ class FreshRSS_Configuration { 'mail_login' => '', 'token' => '', 'passwordHash' => '', //CRYPT_BLOWFISH + 'apiPasswordHash' => '', //CRYPT_BLOWFISH 'posts_per_page' => 20, 'view_mode' => 'normal', - 'default_view' => 'not_read', + 'default_view' => FreshRSS_Entry::STATE_NOT_READ, 'auto_load_more' => true, 'display_posts' => false, 'onread_jump_next' => true, 'lazyload' => true, + 'sticky_post' => true, 'sort_order' => 'DESC', 'anon_access' => false, 'mark_when' => array( @@ -26,15 +28,19 @@ class FreshRSS_Configuration { 'reception' => false, ), 'theme' => 'Origine', + 'content_width' => 'thin', 'shortcuts' => array( 'mark_read' => 'r', 'mark_favorite' => 'f', 'go_website' => 'space', 'next_entry' => 'j', 'prev_entry' => 'k', + 'first_entry' => 'home', + 'last_entry' => 'end', 'collapse_entry' => 'c', 'load_more' => 'm', 'auto_share' => 's', + 'focus_search' => 'a', ), 'topline_read' => true, 'topline_favorite' => true, @@ -46,16 +52,7 @@ class FreshRSS_Configuration { 'bottomline_tags' => true, 'bottomline_date' => true, 'bottomline_link' => true, - 'sharing' => array( - 'shaarli' => '', - 'wallabag' => '', - 'diaspora' => '', - 'twitter' => true, - 'g+' => true, - 'facebook' => true, - 'email' => true, - 'print' => true, - ), + 'sharing' => array(), ); private $available_languages = array( @@ -63,8 +60,10 @@ class FreshRSS_Configuration { 'fr' => 'Français', ); - public function __construct ($user) { - $this->filename = DATA_PATH . '/' . $user . '_user.php'; + private $shares; + + public function __construct($user) { + $this->filename = DATA_PATH . DIRECTORY_SEPARATOR . $user . '_user.php'; $data = @include($this->filename); if (!is_array($data)) { @@ -78,10 +77,20 @@ class FreshRSS_Configuration { } } $this->data['user'] = $user; + + $this->shares = DATA_PATH . DIRECTORY_SEPARATOR . 'shares.php'; + + $shares = @include($this->shares); + if (!is_array($shares)) { + throw new Minz_PermissionDeniedException($this->shares); + } + + $this->data['shares'] = $shares; } public function save() { @rename($this->filename, $this->filename . '.bak.php'); + unset($this->data['shares']); // Remove shares because it is not intended to be stored in user configuration if (file_put_contents($this->filename, "data, true) . ';', LOCK_EX) === false) { throw new Minz_PermissionDeniedException($this->filename); } @@ -102,16 +111,6 @@ class FreshRSS_Configuration { } } - public function sharing($key = false) { - if ($key === false) { - return $this->data['sharing']; - } - if (isset($this->data['sharing'][$key])) { - return $this->data['sharing'][$key]; - } - return false; - } - public function availableLanguages() { return $this->available_languages; } @@ -134,7 +133,7 @@ class FreshRSS_Configuration { } } public function _default_view ($value) { - $this->data['default_view'] = $value === 'all' ? 'all' : 'not_read'; + $this->data['default_view'] = $value === FreshRSS_Entry::STATE_ALL ? FreshRSS_Entry::STATE_ALL : FreshRSS_Entry::STATE_NOT_READ; } public function _display_posts ($value) { $this->data['display_posts'] = ((bool)$value) && $value !== 'no'; @@ -145,6 +144,9 @@ class FreshRSS_Configuration { public function _lazyload ($value) { $this->data['lazyload'] = ((bool)$value) && $value !== 'no'; } + public function _sticky_post($value) { + $this->data['sticky_post'] = ((bool)$value) && $value !== 'no'; + } public function _sort_order ($value) { $this->data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC'; } @@ -166,6 +168,9 @@ class FreshRSS_Configuration { public function _passwordHash ($value) { $this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; } + public function _apiPasswordHash ($value) { + $this->data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; + } public function _mail_login ($value) { $value = filter_var($value, FILTER_VALIDATE_EMAIL); if ($value) { @@ -185,29 +190,47 @@ class FreshRSS_Configuration { } } public function _sharing ($values) { - $are_url = array ('shaarli', 'wallabag', 'diaspora'); - foreach ($values as $key => $value) { - if (in_array($key, $are_url)) { + $this->data['sharing'] = array(); + foreach ($values as $value) { + if (!is_array($value)) { + continue; + } + + // Verify URL and add default value when needed + if (isset($value['url'])) { $is_url = ( - filter_var ($value, FILTER_VALIDATE_URL) || + filter_var ($value['url'], FILTER_VALIDATE_URL) || (version_compare(PHP_VERSION, '5.3.3', '<') && (strpos($value, '-') > 0) && ($value === filter_var($value, FILTER_SANITIZE_URL))) ); //PHP bug #51192 - if (!$is_url) { - $value = ''; + continue; } - } elseif (!is_bool($value)) { - $value = true; + } else { + $value['url'] = null; } - $this->data['sharing'][$key] = $value; + // Add a default name + if (empty($value['name'])) { + $value['name'] = $value['type']; + } + + $this->data['sharing'][] = $value; } } public function _theme($value) { $this->data['theme'] = $value; } + public function _content_width($value) { + if ($value === 'medium' || + $value === 'large' || + $value === 'no_limit') { + $this->data['content_width'] = $value; + } else { + $this->data['content_width'] = 'thin'; + } + } public function _token($value) { $this->data['token'] = $value; } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index a6c67221b..fa9066d5b 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -1,6 +1,11 @@ listLastGuidsByFeed($entry->feed(), 20), 1 + ); + + $nb_month_old = max($conf->old_entries, 1); + $date_min = time() - (3600 * 24 * 30 * $nb_month_old); + + $eDate = $entry->date(true); + + if ($feedHistory == -2) { + $feedHistory = $conf->keep_history_default; + } + + if (!isset($existingGuids[$entry->guid()]) && + ($feedHistory != 0 || $eDate >= $date_min)) { + $values = $entry->toArray(); + + $useDeclaredDate = empty($existingGuids); + $values['id'] = ($useDeclaredDate || $eDate < $date_min) ? + min(time(), $eDate) . uSecString() : + uTimeString(); + + return $this->addEntry($values); + } + + // We don't return Entry object to avoid a research in DB + return -1; + } + + public function markFavorite($ids, $is_favorite = true) { + if (!is_array($ids)) { + $ids = array($ids); + } $sql = 'UPDATE `' . $this->prefix . 'entry` e ' . 'SET e.is_favorite = ? ' - . 'WHERE e.id=?'; - $values = array ($is_favorite ? 1 : 0, $id); + . 'WHERE e.id IN (' . str_repeat('?,', count($ids) - 1). '?)'; + $values = array ($is_favorite ? 1 : 0); + $values = array_merge($values, $ids); $stm = $this->bd->prepare ($sql); if ($stm && $stm->execute ($values)) { return $stm->rowCount(); @@ -49,30 +83,79 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return false; } } - public function markRead ($id, $is_read = true) { - $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' - . 'SET e.is_read = ?,' - . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 ' - . 'WHERE e.id=?'; - $values = array ($is_read ? 1 : 0, $id); - $stm = $this->bd->prepare ($sql); - if ($stm && $stm->execute ($values)) { - return $stm->rowCount(); + + public function markRead($ids, $is_read = true) { + if (is_array($ids)) { + if (count($ids) < 6) { //Speed heuristics + $affected = 0; + foreach ($ids as $id) { + $affected += $this->markRead($id, $is_read); + } + return $affected; + } + + $this->bd->beginTransaction(); + $sql = 'UPDATE `' . $this->prefix . 'entry` e ' + . 'SET e.is_read = ? ' + . 'WHERE e.id IN (' . str_repeat('?,', count($ids) - 1). '?)'; + $values = array($is_read ? 1 : 0); + $values = array_merge($values, $ids); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + $affected = $stm->rowCount(); + + if ($affected > 0) { + $sql = 'UPDATE `' . $this->prefix . 'feed` f ' + . 'INNER JOIN (' + . 'SELECT e.id_feed, ' + . 'COUNT(CASE WHEN e.is_read = 0 THEN 1 END) AS nbUnreads, ' + . 'COUNT(e.id) AS nbEntries ' + . 'FROM `' . $this->prefix . 'entry` e ' + . 'GROUP BY e.id_feed' + . ') x ON x.id_feed=f.id ' + . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads'; + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute())) { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + } + + $this->bd->commit(); + return $affected; } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - return false; + $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' + . 'SET e.is_read = ?,' + . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 ' + . 'WHERE e.id=?'; + $values = array($is_read ? 1 : 0, $ids); + $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute($values)) { + return $stm->rowCount(); + } else { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + return false; + } } } - public function markReadEntries ($idMax = 0, $favorites = false) { - if ($idMax === 0) { + + public function markReadEntries ($idMax = 0, $onlyFavorites = false, $priorityMin = 0) { + if ($idMax == 0) { $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' - . 'WHERE e.is_read = 0 AND '; - if ($favorites) { - $sql .= 'e.is_favorite = 1'; - } else { - $sql .= 'f.priority > 0'; + . 'WHERE e.is_read = 0'; + if ($onlyFavorites) { + $sql .= ' AND e.is_favorite = 1'; + } elseif ($priorityMin >= 0) { + $sql .= ' AND f.priority > ' . intval($priorityMin); } $stm = $this->bd->prepare ($sql); if ($stm && $stm->execute ()) { @@ -87,11 +170,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' . 'SET e.is_read = 1 ' - . 'WHERE e.is_read = 0 AND e.id <= ? AND '; - if ($favorites) { - $sql .= 'e.is_favorite = 1'; - } else { - $sql .= 'f.priority > 0'; + . 'WHERE e.is_read = 0 AND e.id <= ?'; + if ($onlyFavorites) { + $sql .= ' AND e.is_favorite = 1'; + } elseif ($priorityMin >= 0) { + $sql .= ' AND f.priority > ' . intval($priorityMin); } $values = array ($idMax); $stm = $this->bd->prepare ($sql); @@ -126,8 +209,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return $affected; } } + public function markReadCat ($id, $idMax = 0) { - if ($idMax === 0) { + if ($idMax == 0) { $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' . 'WHERE f.category = ? AND e.is_read = 0'; @@ -181,8 +265,70 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return $affected; } } + + public function markReadCatName($name, $idMax = 0) { + if ($idMax == 0) { + $sql = 'UPDATE `' . $this->prefix . 'entry` e ' + . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' + . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category ' + . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' + . 'WHERE c.name = ?'; + $values = array($name); + $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute($values)) { + return $stm->rowCount(); + } else { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + return false; + } + } else { + $this->bd->beginTransaction(); + + $sql = 'UPDATE `' . $this->prefix . 'entry` e ' + . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' + . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category ' + . 'SET e.is_read = 1 ' + . 'WHERE c.name = ? AND e.id <= ?'; + $values = array($name, $idMax); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + $affected = $stm->rowCount(); + + if ($affected > 0) { + $sql = 'UPDATE `' . $this->prefix . 'feed` f ' + . 'LEFT OUTER JOIN (' + . 'SELECT e.id_feed, ' + . 'COUNT(*) AS nbUnreads ' + . 'FROM `' . $this->prefix . 'entry` e ' + . 'WHERE e.is_read = 0 ' + . 'GROUP BY e.id_feed' + . ') x ON x.id_feed=f.id ' + . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category ' + . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) ' + . 'WHERE c.name = ?'; + $values = array($name); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + } + + $this->bd->commit(); + return $affected; + } + } + public function markReadFeed ($id, $idMax = 0) { - if ($idMax === 0) { + if ($idMax == 0) { $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' . 'WHERE f.id=? AND e.is_read = 0'; @@ -244,7 +390,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $stm->execute ($values); $res = $stm->fetchAll (PDO::FETCH_ASSOC); $entries = self::daoToEntry ($res); - return isset ($entries[0]) ? $entries[0] : false; + return isset ($entries[0]) ? $entries[0] : null; } public function searchById ($id) { @@ -257,10 +403,13 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $stm->execute ($values); $res = $stm->fetchAll (PDO::FETCH_ASSOC); $entries = self::daoToEntry ($res); - return isset ($entries[0]) ? $entries[0] : false; + return isset ($entries[0]) ? $entries[0] : null; } - public function listWhere($type = 'a', $id = '', $state = 'all', $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $keepHistoryDefault = 0) { + private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { + if (!$state) { + $state = FreshRSS_Entry::STATE_ALL; + } $where = ''; $joinFeed = false; $values = array(); @@ -269,7 +418,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $where .= 'f.priority > 0 '; $joinFeed = true; break; - case 's': + case 's': //Deprecated: use $state instead $where .= 'e1.is_favorite = 1 '; break; case 'c': @@ -281,24 +430,30 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $where .= 'e1.id_feed = ? '; $values[] = intval($id); break; + case 'A': + $where .= '1 '; + break; default: throw new FreshRSS_EntriesGetter_Exception ('Bad type in Entry->listByType: [' . $type . ']!'); } - switch ($state) { - case 'all': - break; - case 'not_read': + + if ($state & FreshRSS_Entry::STATE_NOT_READ) { + if (!($state & FreshRSS_Entry::STATE_READ)) { $where .= 'AND e1.is_read = 0 '; - break; - case 'read': - $where .= 'AND e1.is_read = 1 '; - break; - case 'favorite': - $where .= 'AND e1.is_favorite = 1 '; - break; - default: - throw new FreshRSS_EntriesGetter_Exception ('Bad state in Entry->listByType: [' . $state . ']!'); + } } + elseif ($state & FreshRSS_Entry::STATE_READ) { + $where .= 'AND e1.is_read = 1 '; + } + if ($state & FreshRSS_Entry::STATE_FAVORITE) { + if (!($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) { + $where .= 'AND e1.is_favorite = 1 '; + } + } + elseif ($state & FreshRSS_Entry::STATE_NOT_FAVORITE) { + $where .= 'AND e1.is_favorite = 0 '; + } + switch ($order) { case 'DESC': case 'ASC': @@ -310,70 +465,84 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $where .= 'AND e1.id ' . ($order === 'DESC' ? '<=' : '>=') . $firstId . ' '; } if (($date_min > 0) && ($type !== 's')) { - $where .= 'AND (e1.id >= ' . $date_min . '000000 OR e1.is_read = 0 OR e1.is_favorite = 1 OR (f.keep_history <> 0'; - if (intval($keepHistoryDefault) === 0) { - $where .= ' AND f.keep_history <> -2'; //default + $where .= 'AND (e1.id >= ' . $date_min . '000000'; + if ($showOlderUnreadsorFavorites) { //Lax date constraint + $where .= ' OR e1.is_read = 0 OR e1.is_favorite = 1 OR (f.keep_history <> 0'; + if (intval($keepHistoryDefault) === 0) { + $where .= ' AND f.keep_history <> -2'; //default + } + $where .= ')'; } - $where .= ')) '; + $where .= ') '; $joinFeed = true; } $search = ''; if ($filter !== '') { + require_once(LIB_PATH . '/lib_date.php'); $filter = trim($filter); $filter = addcslashes($filter, '\\%_'); - if (stripos($filter, 'intitle:') === 0) { - $filter = substr($filter, strlen('intitle:')); - $intitle = true; - } else { - $intitle = false; - } - if (stripos($filter, 'inurl:') === 0) { - $filter = substr($filter, strlen('inurl:')); - $inurl = true; - } else { - $inurl = false; - } - if (stripos($filter, 'author:') === 0) { - $filter = substr($filter, strlen('author:')); - $author = true; - } else { - $author = false; - } $terms = array_unique(explode(' ', $filter)); - sort($terms); //Put #tags first + //sort($terms); //Put #tags first //TODO: Put the cheapest filters first foreach ($terms as $word) { $word = trim($word); - if (strlen($word) > 0) { - if ($intitle) { - $search .= 'AND e1.title LIKE ? '; - $values[] = '%' . $word .'%'; - } elseif ($inurl) { - $search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? '; - $values[] = '%' . $word .'%'; - } elseif ($author) { - $search .= 'AND e1.author LIKE ? '; + if (stripos($word, 'intitle:') === 0) { + $word = substr($word, strlen('intitle:')); + $search .= 'AND e1.title LIKE ? '; + $values[] = '%' . $word .'%'; + } elseif (stripos($word, 'inurl:') === 0) { + $word = substr($word, strlen('inurl:')); + $search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? '; + $values[] = '%' . $word .'%'; + } elseif (stripos($word, 'author:') === 0) { + $word = substr($word, strlen('author:')); + $search .= 'AND e1.author LIKE ? '; + $values[] = '%' . $word .'%'; + } elseif (stripos($word, 'date:') === 0) { + $word = substr($word, strlen('date:')); + list($minDate, $maxDate) = parseDateInterval($word); + if ($minDate) { + $search .= 'AND e1.id >= ' . $minDate . '000000 '; + } + if ($maxDate) { + $search .= 'AND e1.id <= ' . $maxDate . '000000 '; + } + } elseif (stripos($word, 'pubdate:') === 0) { + $word = substr($word, strlen('pubdate:')); + list($minDate, $maxDate) = parseDateInterval($word); + if ($minDate) { + $search .= 'AND e1.date >= ' . $minDate . ' '; + } + if ($maxDate) { + $search .= 'AND e1.date <= ' . $maxDate . ' '; + } + } else { + if ($word[0] === '#' && isset($word[1])) { + $search .= 'AND e1.tags LIKE ? '; $values[] = '%' . $word .'%'; } else { - if ($word[0] === '#' && isset($word[1])) { - $search .= 'AND e1.tags LIKE ? '; - $values[] = '%' . $word .'%'; - } else { - $search .= 'AND CONCAT(e1.title, UNCOMPRESS(e1.content_bin)) LIKE ? '; - $values[] = '%' . $word .'%'; - } + $search .= 'AND CONCAT(e1.title, UNCOMPRESS(e1.content_bin)) LIKE ? '; + $values[] = '%' . $word .'%'; } } } } + return array($values, + 'SELECT e1.id FROM `' . $this->prefix . 'entry` e1 ' + . ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed = f.id ' : '') + . 'WHERE ' . $where + . $search + . 'ORDER BY e1.id ' . $order + . ($limit > 0 ? ' LIMIT ' . $limit : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ + } + + public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { + list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault); + $sql = 'SELECT e.id, e.guid, e.title, e.author, UNCOMPRESS(e.content_bin) AS content, e.link, e.date, e.is_read, e.is_favorite, e.id_feed, e.tags ' . 'FROM `' . $this->prefix . 'entry` e ' - . 'INNER JOIN (SELECT e1.id FROM `' . $this->prefix . 'entry` e1 ' - . ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed = f.id ' : '') - . 'WHERE ' . $where - . $search - . 'ORDER BY e1.id ' . $order - . ($limit > 0 ? ' LIMIT ' . $limit : '') //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ + . 'INNER JOIN (' + . $sql . ') e2 ON e2.id = e.id ' . 'ORDER BY e.id ' . $order; @@ -383,6 +552,15 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return self::daoToEntry ($stm->fetchAll (PDO::FETCH_ASSOC)); } + public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { //For API + list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault); + + $stm = $this->bd->prepare($sql); + $stm->execute($values); + + return $stm->fetchAll(PDO::FETCH_COLUMN, 0); + } + public function listLastGuidsByFeed($id, $n) { $sql = 'SELECT guid FROM `' . $this->prefix . 'entry` WHERE id_feed=? ORDER BY id DESC LIMIT ' . intval($n); $stm = $this->bd->prepare ($sql); diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 22c019080..757eacd59 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -193,10 +193,10 @@ class FreshRSS_Feed extends Minz_Model { } $feed = customSimplePie(); $feed->set_feed_url ($url); - $feed->init (); + $mtime = $feed->init(); - if ($feed->error ()) { - throw new FreshRSS_Feed_Exception ($feed->error . ' [' . $url . ']'); + if ((!$mtime) || $feed->error()) { + throw new FreshRSS_Feed_Exception ($feed->error() . ' [' . $url . ']'); } // si on a utilisé l'auto-discover, notre url va avoir changé @@ -210,18 +210,27 @@ class FreshRSS_Feed extends Minz_Model { } if ($loadDetails) { - $title = htmlspecialchars(html_only_entity_decode($feed->get_title()), ENT_COMPAT, 'UTF-8'); - $this->_name ($title === null ? $this->url : $title); + $title = strtr(html_only_entity_decode($feed->get_title()), array('<' => '<', '>' => '>', '"' => '"')); //HTML to HTML-PRE //ENT_COMPAT except & + $this->_name ($title == '' ? $this->url : $title); $this->_website(html_only_entity_decode($feed->get_link())); $this->_description(html_only_entity_decode($feed->get_description())); } - // et on charge les articles du flux - $this->loadEntries ($feed); + if (($mtime === true) || ($mtime > $this->lastUpdate)) { + syslog(LOG_DEBUG, 'FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $subscribe_url); + $this->loadEntries($feed); // et on charge les articles du flux + } else { + syslog(LOG_DEBUG, 'FreshRSS use cache for ' . $subscribe_url); + $this->entries = array(); + } + + $feed->__destruct(); //http://simplepie.org/wiki/faq/i_m_getting_memory_leaks + unset($feed); } } } + private function loadEntries ($feed) { $entries = array (); @@ -245,11 +254,16 @@ class FreshRSS_Feed extends Minz_Model { $elinks = array(); foreach ($item->get_enclosures() as $enclosure) { $elink = $enclosure->get_link(); - if (array_key_exists($elink, $elinks)) continue; - $elinks[$elink] = '1'; - $mime = strtolower($enclosure->get_type()); - if (strpos($mime, 'image/') === 0) { - $content .= '
'; + if (empty($elinks[$elink])) { + $elinks[$elink] = '1'; + $mime = strtolower($enclosure->get_type()); + if (strpos($mime, 'image/') === 0) { + $content .= '
'; + } elseif (strpos($mime, 'audio/') === 0) { + $content .= '