Browse Source

Merge branch 'dev' into beta

Marien Fressinaud 12 năm trước cách đây
mục cha
commit
f58fdfe93d
100 tập tin đã thay đổi với 2836 bổ sung1694 xóa
  1. 49 0
      CHANGELOG
  2. 14 5
      README.md
  3. 39 93
      app/Controllers/configureController.php
  4. 6 1
      app/Controllers/entryController.php
  5. 109 120
      app/Controllers/feedController.php
  6. 390 0
      app/Controllers/importExportController.php
  7. 55 17
      app/Controllers/indexController.php
  8. 1 1
      app/Controllers/javascriptController.php
  9. 24 1
      app/Controllers/usersController.php
  10. 0 6
      app/Exceptions/OpmlException.php
  11. 0 4
      app/FreshRSS.php
  12. 1 13
      app/Models/Category.php
  13. 19 13
      app/Models/CategoryDAO.php
  14. 56 33
      app/Models/Configuration.php
  15. 5 0
      app/Models/Entry.php
  16. 268 90
      app/Models/EntryDAO.php
  17. 45 12
      app/Models/Feed.php
  18. 71 16
      app/Models/FeedDAO.php
  19. 44 0
      app/Models/Share.php
  20. 5 0
      app/Models/Themes.php
  21. 15 20
      app/actualize_script.php
  22. 42 13
      app/i18n/en.php
  23. 41 12
      app/i18n/fr.php
  24. 4 1
      app/layout/aside_configure.phtml
  25. 14 3
      app/layout/aside_feed.phtml
  26. 2 2
      app/layout/aside_flux.phtml
  27. 3 2
      app/layout/header.phtml
  28. 27 14
      app/layout/layout.phtml
  29. 126 112
      app/layout/nav_menu.phtml
  30. 0 1
      app/sql.php
  31. 7 5
      app/views/configure/categorize.phtml
  32. 17 110
      app/views/configure/display.phtml
  33. 15 6
      app/views/configure/feed.phtml
  34. 0 40
      app/views/configure/importExport.phtml
  35. 125 0
      app/views/configure/reading.phtml
  36. 38 43
      app/views/configure/sharing.phtml
  37. 42 14
      app/views/configure/shortcut.phtml
  38. 56 7
      app/views/configure/users.phtml
  39. 9 1
      app/views/error/index.phtml
  40. 91 0
      app/views/feed/add.phtml
  41. 47 0
      app/views/helpers/export/articles.phtml
  42. 28 0
      app/views/helpers/export/opml.phtml
  43. 14 3
      app/views/helpers/javascript_vars.phtml
  44. 15 66
      app/views/helpers/view/normal_view.phtml
  45. 2 1
      app/views/helpers/view/reader_view.phtml
  46. 1 1
      app/views/helpers/view/rss_view.phtml
  47. 52 0
      app/views/importExport/index.phtml
  48. 27 29
      app/views/index/formLogin.phtml
  49. 6 11
      app/views/index/index.phtml
  50. 34 21
      app/views/javascript/actualize.phtml
  51. 6 1
      constants.php
  52. 75 0
      data/shares.php
  53. 1 5
      lib/Minz/ActionController.php
  54. 0 116
      lib/Minz/Cache.php
  55. 47 32
      lib/Minz/Configuration.php
  56. 38 70
      lib/Minz/Dispatcher.php
  57. 24 8
      lib/Minz/Error.php
  58. 33 26
      lib/Minz/FrontController.php
  59. 17 0
      lib/Minz/Log.php
  60. 2 5
      lib/Minz/Request.php
  61. 0 60
      lib/Minz/Response.php
  62. 0 16
      lib/Minz/RouteNotFoundException.php
  63. 0 209
      lib/Minz/Router.php
  64. 4 11
      lib/Minz/Url.php
  65. 10 0
      lib/Minz/View.php
  66. 59 10
      lib/SimplePie/SimplePie.php
  67. 1 1
      lib/SimplePie/SimplePie/Author.php
  68. 1 1
      lib/SimplePie/SimplePie/Cache.php
  69. 1 1
      lib/SimplePie/SimplePie/Cache/Base.php
  70. 1 1
      lib/SimplePie/SimplePie/Cache/DB.php
  71. 1 1
      lib/SimplePie/SimplePie/Cache/File.php
  72. 5 7
      lib/SimplePie/SimplePie/Cache/Memcache.php
  73. 4 3
      lib/SimplePie/SimplePie/Cache/MySQL.php
  74. 1 1
      lib/SimplePie/SimplePie/Caption.php
  75. 1 1
      lib/SimplePie/SimplePie/Category.php
  76. 1 1
      lib/SimplePie/SimplePie/Content/Type/Sniffer.php
  77. 1 1
      lib/SimplePie/SimplePie/Copyright.php
  78. 1 1
      lib/SimplePie/SimplePie/Core.php
  79. 1 1
      lib/SimplePie/SimplePie/Credit.php
  80. 1 1
      lib/SimplePie/SimplePie/Decode/HTML/Entities.php
  81. 2 2
      lib/SimplePie/SimplePie/Enclosure.php
  82. 3 3
      lib/SimplePie/SimplePie/File.php
  83. 1 1
      lib/SimplePie/SimplePie/HTTP/Parser.php
  84. 1 1
      lib/SimplePie/SimplePie/IRI.php
  85. 3 3
      lib/SimplePie/SimplePie/Item.php
  86. 2 2
      lib/SimplePie/SimplePie/Locator.php
  87. 23 30
      lib/SimplePie/SimplePie/Misc.php
  88. 1 1
      lib/SimplePie/SimplePie/Net/IPv6.php
  89. 1 1
      lib/SimplePie/SimplePie/Parse/Date.php
  90. 6 1
      lib/SimplePie/SimplePie/Parser.php
  91. 1 1
      lib/SimplePie/SimplePie/Rating.php
  92. 1 1
      lib/SimplePie/SimplePie/Registry.php
  93. 1 1
      lib/SimplePie/SimplePie/Restriction.php
  94. 6 2
      lib/SimplePie/SimplePie/Sanitize.php
  95. 1 1
      lib/SimplePie/SimplePie/Source.php
  96. 1 1
      lib/SimplePie/SimplePie/XML/Declaration/Parser.php
  97. 1 1
      lib/SimplePie/SimplePie/gzdecode.php
  98. 131 0
      lib/lib_date.php
  99. 194 84
      lib/lib_opml.php
  100. 18 3
      lib/lib_rss.php

+ 49 - 0
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

+ 14 - 5
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 <dev@marienfressinaud.fr>
-* 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)

+ 39 - 93
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,22 +173,30 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 
 		$this->view->themes = FreshRSS_Themes::get();
 
-		Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' · ');
+		Minz_View::prependTitle (Minz_Translate::t ('display_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),
+	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 (
@@ -213,80 +205,34 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			);
 			Minz_Session::_param ('notification', $notif);
 
-			Minz_Request::forward (array ('c' => 'configure', 'a' => 'sharing'), true);
+			Minz_Request::forward (array ('c' => 'configure', 'a' => 'reading'), true);
 		}
 
-		Minz_View::prependTitle (Minz_Translate::t ('sharing') . ' · ');
+		Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' · ');
 	}
 
-	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'])
-					);
+	public function sharingAction () {
+		if (Minz_Request::isPost ()) {
+			$params = Minz_Request::params();
+			$this->view->conf->_sharing ($params['share']);
+			$this->view->conf->save();
+			invalidateHttpCache();
 
-					// 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);
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('configuration_updated')
+			);
+			Minz_Session::_param ('notification', $notif);
 
-					Minz_Request::forward (array (
-						'c' => 'configure',
-						'a' => 'importExport'
-					), true);
-				}
-			}
+			Minz_Request::forward (array ('c' => 'configure', 'a' => 'sharing'), 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') . ' · ');
+		Minz_View::prependTitle (Minz_Translate::t ('sharing') . ' · ');
 	}
 
 	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',

+ 6 - 1
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(

+ 109 - 120
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')))
 				);
 			}
 		}
+	}
+
+	public function addAction () {
+		$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 ();
-	}
 
-	public function addAction () {
-		@set_time_limit(300);
+		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;
+					}
+
+					$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
-				$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);
+					// 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);
-			}
-		}
-	}
 }

+ 390 - 0
app/Controllers/importExportController.php

@@ -0,0 +1,390 @@
+<?php
+
+class FreshRSS_importExport_Controller extends Minz_ActionController {
+	public function firstAction() {
+		if (!$this->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');
+	}
+}

+ 55 - 17
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();
 	}

+ 1 - 1
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() {

+ 24 - 1
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();
 				}
 			}

+ 0 - 6
app/Exceptions/OpmlException.php

@@ -1,6 +0,0 @@
-<?php
-class FreshRSS_Opml_Exception extends FreshRSS_Feed_Exception {
-	public function __construct ($name_file) {
-		parent::__construct ('OPML file is invalid');
-	}
-}

+ 0 - 4
app/FreshRSS.php

@@ -94,10 +94,6 @@ class FreshRSS extends Minz_FrontController {
 					$loginOk = false;
 					break;
 			}
-			if ((!$loginOk) && (PHP_SAPI === 'cli') && (Minz_Request::actionName() === 'actualize')) {	//Command line
-				Minz_Configuration::_authType('none');
-				$loginOk = true;
-			}
 		}
 		Minz_View::_param ('loginOk', $loginOk);
 		return $loginOk;

+ 1 - 13
app/Models/Category.php

@@ -3,14 +3,12 @@
 class FreshRSS_Category extends Minz_Model {
 	private $id = 0;
 	private $name;
-	private $color;
 	private $nbFeed = -1;
 	private $nbNotRead = -1;
 	private $feeds = null;
 
-	public function __construct ($name = '', $color = '#0062BE', $feeds = null) {
+	public function __construct ($name = '', $feeds = null) {
 		$this->_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);

+ 19 - 13
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;

+ 56 - 33
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, "<?php\n return " . var_export($this->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;
 	}

+ 5 - 0
app/Models/Entry.php

@@ -1,6 +1,11 @@
 <?php
 
 class FreshRSS_Entry extends Minz_Model {
+	const STATE_ALL = 0;
+	const STATE_READ = 1;
+	const STATE_NOT_READ = 2;
+	const STATE_FAVORITE = 4;
+	const STATE_NOT_FAVORITE = 8;
 
 	private $id = 0;
 	private $guid;

+ 268 - 90
app/Models/EntryDAO.php

@@ -35,11 +35,45 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		}
 	}
 
-	public function markFavorite ($id, $is_favorite = true) {
+	public function addEntryObject($entry, $conf, $feedHistory) {
+		$existingGuids = array_fill_keys(
+			$this->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':
+			}
+		}
+		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 ';
-				break;
-			default:
-				throw new FreshRSS_EntriesGetter_Exception ('Bad state in Entry->listByType: [' . $state . ']!');
+			}
+		}
+		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);

+ 45 - 12
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('<' => '&lt;', '>' => '&gt;', '"' => '&quot;'));	//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 .= '<br /><img src="' . $elink . '" alt="" />';
+				if (empty($elinks[$elink])) {
+					$elinks[$elink] = '1';
+					$mime = strtolower($enclosure->get_type());
+					if (strpos($mime, 'image/') === 0) {
+						$content .= '<br /><img src="' . $elink . '" alt="" />';
+					} elseif (strpos($mime, 'audio/') === 0) {
+						$content .= '<br /><audio src="' . $elink . '" controls="controls" />';
+					} elseif (strpos($mime, 'video/') === 0) {
+						$content .= '<br /><video src="' . $elink . '" controls="controls" />';
+					}
 				}
 			}
 
@@ -267,8 +281,27 @@ class FreshRSS_Feed extends Minz_Model {
 			$entry->loadCompleteContent($this->pathEntries());
 
 			$entries[] = $entry;
+			unset($item);
 		}
 
 		$this->entries = $entries;
 	}
+
+	function lock() {
+		$lock = TMP_PATH . '/' . md5(Minz_Configuration::salt() . $this->url) . '.freshrss.lock';
+		if (file_exists($lock) && ((time() - @filemtime($lock)) > 3600)) {
+			@unlink($lock);
+		}
+		if (($handle = @fopen($lock, 'x')) === false) {
+			return false;
+		}
+		//register_shutdown_function('unlink', $lock);
+		@fclose($handle);
+		return true;
+	}
+
+	function unlock() {
+		$lock = TMP_PATH . '/' . md5(Minz_Configuration::salt() . $this->url) . '.freshrss.lock';
+		@unlink($lock);
+	}
 }

+ 71 - 16
app/Models/FeedDAO.php

@@ -24,6 +24,36 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		}
 	}
 
+	public function addFeedObject($feed) {
+		// TODO: not sure if we should write this method in DAO since DAO
+		// should not be aware about feed class
+
+		// Add feed only if we don't find it in DB
+		$feed_search = $this->searchByUrl($feed->url());
+		if (!$feed_search) {
+			$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()
+			);
+
+			$id = $this->addFeed($values);
+			if ($id) {
+				$feed->_id($id);
+				$feed->faviconPrepare();
+			}
+
+			return $id;
+		}
+
+		return $feed_search->id();
+	}
+
 	public function updateFeed ($id, $valuesTmp) {
 		$set = '';
 		foreach ($valuesTmp as $key => $v) {
@@ -52,21 +82,27 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		}
 	}
 
-	public function updateLastUpdate ($id, $inError = 0) {
-		$sql = 'UPDATE `' . $this->prefix . 'feed` f '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
-		     . 'SET f.cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=f.id),'
-		     . 'f.cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=f.id AND e2.is_read=0),'
-		     . 'lastUpdate=?, error=? '
-		     . 'WHERE f.id=?';
-
-		$stm = $this->bd->prepare ($sql);
+	public function updateLastUpdate ($id, $inError = 0, $updateCache = true) {
+		if ($updateCache) {
+			$sql = 'UPDATE `' . $this->prefix . 'feed` f '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
+			     . 'SET f.cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=f.id),'
+			     . 'f.cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=f.id AND e2.is_read=0),'
+			     . 'lastUpdate=?, error=? '
+			     . 'WHERE f.id=?';
+		} else {
+			$sql = 'UPDATE `' . $this->prefix . 'feed` f '
+			     . 'SET lastUpdate=?, error=? '
+			     . 'WHERE f.id=?';
+		}
 
 		$values = array (
-			time (),
+			time(),
 			$inError,
 			$id,
 		);
 
+		$stm = $this->bd->prepare ($sql);
+
 		if ($stm && $stm->execute ($values)) {
 			return $stm->rowCount();
 		} else {
@@ -164,7 +200,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		if (isset ($feed[$id])) {
 			return $feed[$id];
 		} else {
-			return false;
+			return null;
 		}
 	}
 	public function searchByUrl ($url) {
@@ -180,7 +216,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		if (isset ($feed)) {
 			return $feed;
 		} else {
-			return false;
+			return null;
 		}
 	}
 
@@ -192,8 +228,27 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
 	}
 
-	public function listFeedsOrderUpdate () {
-		$sql = 'SELECT id, name, url, pathEntries, httpAuth, keep_history FROM `' . $this->prefix . 'feed` ORDER BY lastUpdate';
+	public function arrayFeedCategoryNames() {	//For API
+		$sql = 'SELECT f.id, f.name, c.name as c_name FROM `' . $this->prefix . 'feed` f '
+		     . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category';
+		$stm = $this->bd->prepare ($sql);
+		$stm->execute ();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$feedCategoryNames = array();
+		foreach ($res as $line) {
+			$feedCategoryNames[$line['id']] = array(
+				'name' => $line['name'],
+				'c_name' => $line['c_name'],
+			);
+		}
+		return $feedCategoryNames;
+	}
+
+	public function listFeedsOrderUpdate ($cacheDuration = 1500) {
+		$sql = 'SELECT id, name, url, lastUpdate, pathEntries, httpAuth, keep_history '
+		     . 'FROM `' . $this->prefix . 'feed` '
+		     . 'WHERE lastUpdate < ' . (time() - intval($cacheDuration))
+		     . ' ORDER BY lastUpdate';
 		$stm = $this->bd->prepare ($sql);
 		$stm->execute ();
 
@@ -220,6 +275,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 
 		return $res[0]['count'];
 	}
+
 	public function countNotRead ($id) {
 		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND is_read=0';
 		$stm = $this->bd->prepare ($sql);
@@ -229,6 +285,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 
 		return $res[0]['count'];
 	}
+
 	public function updateCachedValues () {	//For one single feed, call updateLastUpdate($id)
 		$sql = 'UPDATE `' . $this->prefix . 'feed` f '
 		     . 'INNER JOIN ('
@@ -241,9 +298,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		     . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads';
 		$stm = $this->bd->prepare ($sql);
 
-		$values = array ($feed_id);
-
-		if ($stm && $stm->execute ($values)) {
+		if ($stm && $stm->execute()) {
 			return $stm->rowCount();
 		} else {
 			$info = $stm->errorInfo();

+ 44 - 0
app/Models/Share.php

@@ -0,0 +1,44 @@
+<?php
+
+class FreshRSS_Share {
+
+	static public function generateUrl($options, $selected, $link, $title) {
+		$share = $options[$selected['type']];
+		$matches = array(
+			'~URL~',
+			'~TITLE~',
+			'~LINK~',
+		);
+		$replaces = array(
+			$selected['url'],
+			self::transformData($title, self::getTransform($share, 'title')),
+			self::transformData($link, self::getTransform($share, 'link')),
+		);
+		$url = str_replace($matches, $replaces, $share['url']);
+		return $url;
+	}
+
+	static private function transformData($data, $transform) {
+		if (!is_array($transform)) {
+			return $data;
+		}
+		if (count($transform) === 0) {
+			return $data;
+		}
+		foreach ($transform as $action) {
+			$data = call_user_func($action, $data);
+		}
+		return $data;
+	}
+
+	static private function getTransform($options, $type) {
+		$transform = $options['transform'];
+
+		if (array_key_exists($type, $transform)) {
+			return $transform[$type];
+		}
+
+		return $transform;
+	}
+
+}

+ 5 - 0
app/Models/Themes.php

@@ -77,6 +77,7 @@ class FreshRSS_Themes extends Minz_Model {
 			'down' => '▽',
 			'favorite' => '★',
 			'help' => 'ⓘ',
+			'key' => '⚿',
 			'link' => '↗',
 			'login' => '🔒',
 			'logout' => '🔓',
@@ -84,6 +85,7 @@ class FreshRSS_Themes extends Minz_Model {
 			'non-starred' => '☆',
 			'prev' => '⏪',
 			'read' => '☑',
+			'rss' => '☄',
 			'unread' => '☐',
 			'refresh' => '🔃',	//↻
 			'search' => '🔍',
@@ -91,6 +93,9 @@ class FreshRSS_Themes extends Minz_Model {
 			'starred' => '★',
 			'tag' => '⚐',
 			'up' => '△',
+			'view-normal' => '☰',
+			'view-global' => '☷',
+			'view-reader' => '☕',
 		);
 		if (!isset($alts[$name])) {
 			return '';

+ 15 - 20
app/actualize_script.php

@@ -1,21 +1,5 @@
 <?php
 require(dirname(__FILE__) . '/../constants.php');
-
-//<Mutex>
-$lock = DATA_PATH . '/actualize.lock.txt';
-if (file_exists($lock) && ((time() - @filemtime($lock)) > 3600)) {
-	@unlink($lock);
-}
-if (($handle = @fopen($lock, 'x')) === false) {
-	syslog(LOG_NOTICE, 'FreshRSS actualize already running?');
-	fwrite(STDERR, 'FreshRSS actualize already running?' . "\n");
-	return;
-}
-register_shutdown_function('unlink', $lock);
-//Could use http://php.net/function.pcntl-signal.php to catch interruptions
-@fclose($handle);
-//</Mutex>
-
 require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
 
 session_cache_limiter('');
@@ -32,7 +16,9 @@ $users = array_unique($users);
 
 foreach ($users as $myUser) {
 	syslog(LOG_INFO, 'FreshRSS actualize ' . $myUser);
-	fwrite(STDOUT, 'Actualize ' . $myUser . "...\n");	//Unbuffered
+	if (defined('STDOUT')) {
+		fwrite(STDOUT, 'Actualize ' . $myUser . "...\n");	//Unbuffered
+	}
 	echo $myUser, ' ';	//Buffered
 
 	$_GET['c'] = 'feed';
@@ -42,7 +28,8 @@ foreach ($users as $myUser) {
 	$_SERVER['HTTP_HOST'] = '';
 
 	$freshRSS = new FreshRSS();
-	$freshRSS->_useOb(false);
+
+	Minz_Configuration::_authType('none');
 
 	Minz_Session::init('FreshRSS');
 	Minz_Session::_param('currentUser', $myUser);
@@ -50,10 +37,18 @@ foreach ($users as $myUser) {
 	$freshRSS->init();
 	$freshRSS->run();
 
-	invalidateHttpCache();
+	if (!invalidateHttpCache()) {
+		syslog(LOG_NOTICE, 'FreshRSS write access problem in ' . LOG_PATH . '/*.log!');
+		if (defined('STDERR')) {
+			fwrite(STDERR, 'Write access problem in ' . LOG_PATH . '/*.log!' . "\n");
+		}
+	}
 	Minz_Session::unset_session(true);
 	Minz_ModelPdo::clean();
 }
 syslog(LOG_INFO, 'FreshRSS actualize done.');
+if (defined('STDOUT')) {
+	fwrite(STDOUT, 'Done.' . "\n");
+}
+echo 'End.', "\n";
 ob_end_flush();
-fwrite(STDOUT, 'Done.' . "\n");

+ 42 - 13
app/i18n/en.php

@@ -3,6 +3,7 @@
 return array (
 	// LAYOUT
 	'login'				=> 'Login',
+	'login_with_persona'		=> 'Login with Persona',
 	'logout'			=> 'Logout',
 	'search'			=> 'Search words or #tags',
 	'search_short'			=> 'Search',
@@ -20,12 +21,13 @@ return array (
 	'your_rss_feeds'		=> 'Your RSS feeds',
 	'add_rss_feed'			=> 'Add a RSS feed',
 	'no_rss_feed'			=> 'No RSS feed',
-	'import_export_opml'		=> 'Import / export (OPML)',
+	'import_export'			=> 'Import / export',
+	'bookmark'			=> 'Subscribe (FreshRSS bookmark)',
 
 	'subscription_management'	=> 'Subscriptions management',
 	'main_stream'			=> 'Main stream',
 	'all_feeds'			=> 'All feeds',
-	'favorite_feeds'		=> 'Favourites (%d)',
+	'favorite_feeds'		=> 'Favourites (%s)',
 	'not_read'			=> '%d unread',
 	'not_reads'			=> '%d unread',
 
@@ -49,7 +51,8 @@ return array (
 	'show_all_articles'		=> 'Show all articles',
 	'show_not_reads'		=> 'Show only unread',
 	'show_read'			=> 'Show only read',
-	'show_favorite'			=> 'Show favorites',
+	'show_favorite'			=> 'Show only favorites',
+	'show_not_favorite'		=> 'Show all but favorites',
 	'older_first'			=> 'Oldest first',
 	'newer_first'			=> 'Newer first',
 
@@ -77,14 +80,17 @@ return array (
 	'sharing_management'		=> 'Sharing options management',
 	'bad_opml_file'			=> 'Your OPML file is invalid',
 	'shortcuts_updated'		=> 'Shortcuts have been updated',
-	'shortcuts_management'		=> 'Shortcuts management',
+	'shortcuts_navigation'		=> 'Navigation',
+	'shortcuts_navigation_help'	=> 'With the "Shift" modifier, navigation shortcuts apply on feeds.<br/>With the "Alt" modifier, navigation shortcuts apply on categories.',
+	'shortcuts_article_action'	=> 'Article actions',
+	'shortcuts_other_action'	=> 'Other actions',
 	'feeds_marked_read'		=> 'Feeds have been marked as read',
 	'updated'			=> 'Modifications have been updated',
 
 	'already_subscribed'		=> 'You have already subscribed to <em>%s</em>',
 	'feed_added'			=> 'RSS feed <em>%s</em> has been added',
 	'feed_not_added'		=> '<em>%s</em> could not be added',
-	'internal_problem_feed'		=> 'The RSS feed could not be added. Check FressRSS logs for details.',
+	'internal_problem_feed'		=> 'The RSS feed could not be added. <a href="%s">Check FressRSS logs</a> for details.',
 	'invalid_url'			=> 'URL <em>%s</em> is invalid',
 	'feed_actualized'		=> '<em>%s</em> has been updated',
 	'n_feeds_actualized'		=> '%d feeds have been updated',
@@ -121,23 +127,31 @@ return array (
 	'javascript_for_shortcuts'	=> 'JavaScript must be enabled in order to use shortcuts',
 	'javascript_should_be_activated'=> 'JavaScript must be enabled',
 	'shift_for_all_read'		=> '+ <code>shift</code> to mark all articles as read',
-	'see_on_website'		=> 'See article on its original website',
+	'see_on_website'		=> 'See on original website',
 	'next_article'			=> 'Skip to the next article',
-	'shift_for_last'		=> '+ <code>shift</code> to skip to the last article of page',
+	'last_article'			=> 'Skip to the last article',
 	'previous_article'		=> 'Skip to the previous article',
-	'shift_for_first'		=> '+ <code>shift</code> to skip to the first article of page',
+	'first_article'			=> 'Skip to the first article',
 	'next_page'			=> 'Skip to the next page',
 	'previous_page'			=> 'Skip to the previous page',
-	'collapse_article'		=> 'Collapse current article',
-	'auto_share'			=> 'Share current article',
+	'collapse_article'		=> 'Collapse',
+	'auto_share'			=> 'Share',
+	'auto_share_help'		=> 'If there is only one sharing mode, it is used. Else modes are accessible by their number.',
+	'focus_search'			=> 'Access search box',
 
-	'file_to_import'		=> 'File to import',
+	'file_to_import'		=> 'File to import<br />(OPML, Json or Zip)',
 	'import'			=> 'Import',
 	'export'			=> 'Export',
+	'export_opml'			=> 'Export list of feeds (OPML)',
+	'export_starred'		=> 'Export your favourites',
+	'starred_list'			=> 'List of favourite articles',
+	'feed_list'			=> 'List of %s articles',
 	'or'				=> 'or',
 
 	'informations'			=> 'Information',
+	'damn'				=> 'Damn!',
 	'feed_in_error'			=> 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.',
+	'feed_empty'			=> 'This feed is empty. Please verify that it is still maintained.',
 	'feed_description'		=> 'Description',
 	'website_url'			=> 'Website URL',
 	'feed_url'			=> 'Feed URL',
@@ -158,6 +172,8 @@ return array (
 	'http_username'			=> 'HTTP username',
 	'http_password'			=> 'HTTP password',
 	'blank_to_disable'		=> 'Leave blank to disable',
+	'share_name'			=> 'Share name to display',
+	'share_url'			=> 'Share URL to use',
 	'not_yet_implemented'		=> 'Not yet implemented',
 	'access_protected_feeds'	=> 'Connection allows to access HTTP protected RSS feeds',
 	'no_selected_feed'		=> 'No feed selected.',
@@ -166,8 +182,12 @@ return array (
 	'current_user'			=> 'Current user',
 	'default_user'			=> 'Username of the default user <small>(maximum 16 alphanumeric characters)</small>',
 	'password_form'			=> 'Password<br /><small>(for the Web-form login method)</small>',
+	'password_api'			=> 'Password API<br /><small>(e.g., for mobile apps)</small>',
 	'persona_connection_email'	=> 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 	'allow_anonymous'		=> 'Allow anonymous reading of the articles of the default user (%s)',
+	'allow_anonymous_refresh'	=> 'Allow anonymous refresh of the articles',
+	'unsafe_autologin'		=> 'Allow unsafe automatic login using the format: ',
+	'api_enabled'			=> 'Allow <abbr>API</abbr> access <small>(required for mobile apps)</small>',
 	'auth_token'			=> 'Authentication token',
 	'explain_token'			=> 'Allows to access RSS output of the default user without authentication.<br /><kbd>%s?output=rss&token=%s</kbd>',
 	'login_configuration'		=> 'Login',
@@ -193,6 +213,7 @@ return array (
 	'purge_completed'		=> 'Purge completed (%d articles deleted)',
 	'archiving_configuration_help'	=> 'More options are available in the individual stream settings',
 	'reading_configuration'		=> 'Reading',
+	'display_configuration'		=> 'Display',
 	'articles_per_page'		=> 'Number of articles per page',
 	'default_view'			=> 'Default view',
 	'sort_order'			=> 'Sort order',
@@ -200,10 +221,11 @@ return array (
 	'display_articles_unfolded'	=> 'Show articles unfolded by default',
 	'after_onread'			=> 'After “mark all as read”,',
 	'jump_next'			=> 'jump to next unread sibling (feed or category)',
-	'reading_icons'			=> 'Reading icons',
+	'article_icons'			=> 'Article icons',
 	'top_line'			=> 'Top line',
 	'bottom_line'			=> 'Bottom line',
 	'img_with_lazyload'		=> 'Use "lazy load" mode to load pictures',
+	'sticky_post'			=> 'Stick the article to the top when opened',
 	'auto_read_when'		=> 'Mark article as read…',
 	'article_selected'		=> 'when article is selected',
 	'article_open_on_website'	=> 'when article is opened on its original website',
@@ -218,9 +240,15 @@ return array (
 	'optimize_bdd'			=> 'Optimize database',
 	'optimize_todo_sometimes'	=> 'To do occasionally to reduce the size of the database',
 	'theme'				=> 'Theme',
+	'content_width'			=> 'Content width',
+	'width_thin'			=> 'Thin',
+	'width_medium'			=> 'Medium',
+	'width_large'			=> 'Large',
+	'width_no_limit'		=> 'No limit',
 	'more_information'		=> 'More information',
 	'activate_sharing'		=> 'Activate sharing',
 	'shaarli'			=> 'Shaarli',
+	'blogotext'			=> 'Blogotext',
 	'wallabag'			=> 'wallabag',
 	'diaspora'			=> 'Diaspora*',
 	'twitter'			=> 'Twitter',
@@ -241,6 +269,7 @@ return array (
 	'rss_feeds_of'			=> 'RSS feed of %s',
 
 	'refresh'			=> 'Refresh',
+	'no_feed_to_refresh'		=> 'There is no feed to refresh…',
 
 	'today'				=> 'Today',
 	'yesterday'			=> 'Yesterday',
@@ -267,7 +296,7 @@ return array (
 	'logs_empty'			=> 'Log file is empty',
 	'clear_logs'			=> 'Clear the logs',
 
-	'forbidden_access'		=> 'Access forbidden! (%s)',
+	'forbidden_access'		=> 'Access is forbidden!',
 	'login_required'		=> 'Login required:',
 
 	'confirm_action'		=> 'Are you sure you want to perform this action? It cannot be cancelled!',

+ 41 - 12
app/i18n/fr.php

@@ -3,6 +3,7 @@
 return array (
 	// LAYOUT
 	'login'				=> 'Connexion',
+	'login_with_persona'		=> 'Connexion avec Persona',
 	'logout'			=> 'Déconnexion',
 	'search'			=> 'Rechercher des mots ou des #tags',
 	'search_short'			=> 'Rechercher',
@@ -20,12 +21,13 @@ return array (
 	'your_rss_feeds'		=> 'Vos flux RSS',
 	'add_rss_feed'			=> 'Ajouter un flux RSS',
 	'no_rss_feed'			=> 'Aucun flux RSS',
-	'import_export_opml'		=> 'Importer / exporter (OPML)',
+	'import_export'			=> 'Importer / exporter',
+	'bookmark'			=> 'S’abonner (bookmark FreshRSS)',
 
 	'subscription_management'	=> 'Gestion des abonnements',
 	'main_stream'			=> 'Flux principal',
 	'all_feeds'			=> 'Tous les flux',
-	'favorite_feeds'		=> 'Favoris (%d)',
+	'favorite_feeds'		=> 'Favoris (%s)',
 	'not_read'			=> '%d non lu',
 	'not_reads'			=> '%d non lus',
 
@@ -50,6 +52,7 @@ return array (
 	'show_not_reads'		=> 'Afficher les non lus',
 	'show_read'			=> 'Afficher les lus',
 	'show_favorite'			=> 'Afficher les favoris',
+	'show_not_favorite'		=> 'Afficher tout sauf les favoris',
 	'older_first'			=> 'Plus anciens en premier',
 	'newer_first'			=> 'Plus récents en premier',
 
@@ -77,14 +80,17 @@ return array (
 	'sharing_management'		=> 'Gestion des options de partage',
 	'bad_opml_file'			=> 'Votre fichier OPML n’est pas valide',
 	'shortcuts_updated'		=> 'Les raccourcis ont été mis à jour',
-	'shortcuts_management'		=> 'Gestion des raccourcis',
+	'shortcuts_navigation'		=> 'Navigation',
+	'shortcuts_navigation_help'	=> 'Avec le modificateur "Shift", les raccourcis de navigation s’appliquent aux flux.<br/>Avec le modificateur "Alt", les raccourcis de navigation s’appliquent aux catégories.',
+	'shortcuts_article_action'	=> 'Actions associées à l’article courant',
+	'shortcuts_other_action'	=> 'Autres actions',
 	'feeds_marked_read'		=> 'Les flux ont été marqués comme lus',
 	'updated'			=> 'Modifications enregistrées',
 
 	'already_subscribed'		=> 'Vous êtes déjà abonné à <em>%s</em>',
 	'feed_added'			=> 'Le flux <em>%s</em> a bien été ajouté',
 	'feed_not_added'		=> '<em>%s</em> n’a pas pu être ajouté',
-	'internal_problem_feed'		=> 'Le flux n’a pas pu être ajouté. Consulter les logs de FreshRSS pour plus de détails.',
+	'internal_problem_feed'		=> 'Le flux ne peut pas être ajouté. <a href="%s">Consulter les logs de FreshRSS</a> pour plus de détails.',
 	'invalid_url'			=> 'L’url <em>%s</em> est invalide',
 	'feed_actualized'		=> '<em>%s</em> a été mis à jour',
 	'n_feeds_actualized'		=> '%d flux ont été mis à jour',
@@ -121,23 +127,31 @@ return array (
 	'javascript_for_shortcuts'	=> 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis',
 	'javascript_should_be_activated'=> 'Le JavaScript doit être activé',
 	'shift_for_all_read'		=> '+ <code>shift</code> pour marquer tous les articles comme lus',
-	'see_on_website'		=> 'Voir l’article sur le site d’origine',
+	'see_on_website'		=> 'Voir sur le site d’origine',
 	'next_article'			=> 'Passer à l’article suivant',
-	'shift_for_last'		=> '+ <code>shift</code> pour passer au dernier article de la page',
+	'last_article'			=> 'Passer au dernier article',
 	'previous_article'		=> 'Passer à l’article précédent',
-	'shift_for_first'		=> '+ <code>shift</code> pour passer au premier article de la page',
+	'first_article'			=> 'Passer au premier article',
 	'next_page'			=> 'Passer à la page suivante',
 	'previous_page'			=> 'Passer à la page précédente',
-	'collapse_article'		=> 'Refermer l’article courant',
-	'auto_share'			=> 'Partager l’article courant',
+	'collapse_article'		=> 'Refermer',
+	'auto_share'			=> 'Partager',
+	'auto_share_help'		=> 'Si il n’y a qu’un mode de partage, celui ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.',
+	'focus_search'			=> 'Accéder à la recherche',
 
-	'file_to_import'		=> 'Fichier à importer',
+	'file_to_import'		=> 'Fichier à importer<br />(OPML, Json ou Zip)',
 	'import'			=> 'Importer',
 	'export'			=> 'Exporter',
+	'export_opml'			=> 'Exporter la liste des flux (OPML)',
+	'export_starred'		=> 'Exporter les favoris',
+	'starred_list'			=> 'Liste des articles favoris',
+	'feed_list'			=> 'Liste des articles de %s',
 	'or'				=> 'ou',
 
 	'informations'			=> 'Informations',
+	'damn'				=> 'Arf !',
 	'feed_in_error'			=> 'Ce flux a rencontré un problème. Veuillez vérifier qu’il est toujours accessible puis actualisez-le.',
+	'feed_empty'			=> 'Ce flux est vide. Veuillez vérifier qu’il est toujours maintenu.',
 	'feed_description'		=> 'Description',
 	'website_url'			=> 'URL du site',
 	'feed_url'			=> 'URL du flux',
@@ -158,6 +172,8 @@ return array (
 	'http_username'			=> 'Identifiant HTTP',
 	'http_password'			=> 'Mot de passe HTTP',
 	'blank_to_disable'		=> 'Laissez vide pour désactiver',
+	'share_name'			=> 'Nom du partage à afficher',
+	'share_url'			=> 'URL du partage à utiliser',
 	'not_yet_implemented'		=> 'Pas encore implémenté',
 	'access_protected_feeds'	=> 'La connexion permet d’accéder aux flux protégés par une authentification HTTP',
 	'no_selected_feed'		=> 'Aucun flux sélectionné.',
@@ -165,9 +181,13 @@ return array (
 
 	'current_user'			=> 'Utilisateur actuel',
 	'password_form'			=> 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
+	'password_api'			=> 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>',
 	'default_user'			=> 'Nom de l’utilisateur par défaut <small>(16 caractères alphanumériques maximum)</small>',
 	'persona_connection_email'	=> 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 	'allow_anonymous'		=> 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)',
+	'allow_anonymous_refresh'	=> 'Autoriser le rafraîchissement anonyme des flux',
+	'unsafe_autologin'		=> 'Autoriser les connexion automatiques non-sûres au format : ',
+	'api_enabled'			=> 'Autoriser l’accès par <abbr>API</abbr> <small>(nécessaire pour les applis mobiles)</small>',
 	'auth_token'			=> 'Jeton d’identification',
 	'explain_token'			=> 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.<br /><kbd>%s?output=rss&token=%s</kbd>',
 	'login_configuration'		=> 'Identification',
@@ -193,6 +213,7 @@ return array (
 	'purge_completed'		=> 'Purge effectuée (%d articles supprimés)',
 	'archiving_configuration_help'	=> 'D’autres options sont disponibles dans la configuration individuelle des flux',
 	'reading_configuration'		=> 'Lecture',
+	'display_configuration'		=> 'Affichage',
 	'articles_per_page'		=> 'Nombre d’articles par page',
 	'default_view'			=> 'Vue par défaut',
 	'sort_order'			=> 'Ordre de tri',
@@ -200,10 +221,11 @@ return array (
 	'display_articles_unfolded'	=> 'Afficher les articles dépliés par défaut',
 	'after_onread'			=> 'Après “marquer tout comme lu”,',
 	'jump_next'			=> 'sauter au prochain voisin non lu (flux ou catégorie)',
-	'reading_icons'			=> 'Icônes de lecture',
+	'article_icons'			=> 'Icônes d’article',
 	'top_line'			=> 'Ligne du haut',
 	'bottom_line'			=> 'Ligne du bas',
 	'img_with_lazyload'		=> 'Utiliser le mode “chargement différé” pour les images',
+	'sticky_post'			=> 'Aligner l’article en haut quand il est ouvert',
 	'auto_read_when'		=> 'Marquer un article comme lu…',
 	'article_selected'		=> 'lorsque l’article est sélectionné',
 	'article_open_on_website'	=> 'lorsque l’article est ouvert sur le site d’origine',
@@ -218,9 +240,15 @@ return array (
 	'optimize_bdd'			=> 'Optimiser la base de données',
 	'optimize_todo_sometimes'	=> 'À faire de temps en temps pour réduire la taille de la BDD',
 	'theme'				=> 'Thème',
+	'content_width'			=> 'Largeur du contenu',
+	'width_thin'			=> 'Fine',
+	'width_medium'			=> 'Moyenne',
+	'width_large'			=> 'Large',
+	'width_no_limit'		=> 'Pas de limite',
 	'more_information'		=> 'Plus d’informations',
 	'activate_sharing'		=> 'Activer le partage',
 	'shaarli'			=> 'Shaarli',
+	'blogotext'			=> 'Blogotext',
 	'wallabag'			=> 'wallabag',
 	'diaspora'			=> 'Diaspora*',
 	'twitter'			=> 'Twitter',
@@ -241,6 +269,7 @@ return array (
 	'rss_feeds_of'			=> 'Flux RSS de %s',
 
 	'refresh'			=> 'Actualisation',
+	'no_feed_to_refresh'		=> 'Il n’y a aucun flux à actualiser…',
 
 	'today'				=> 'Aujourd’hui',
 	'yesterday'			=> 'Hier',
@@ -267,7 +296,7 @@ return array (
 	'logs_empty'			=> 'Les logs sont vides',
 	'clear_logs'			=> 'Effacer les logs',
 
-	'forbidden_access'		=> 'Accès interdit ! (%s)',
+	'forbidden_access'		=> 'L’accès vous est interdit !',
 	'login_required'		=> 'Accès protégé par mot de passe :',
 
 	'confirm_action'		=> 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !',

+ 4 - 1
app/layout/aside_configure.phtml

@@ -1,7 +1,10 @@
 <ul class="nav nav-list aside">
 	<li class="nav-header"><?php echo Minz_Translate::t ('configuration'); ?></li>
 	<li class="item<?php echo Minz_Request::actionName () == 'display' ? ' active' : ''; ?>">
-		<a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a>
+		<a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('display_configuration'); ?></a>
+	</li>
+	<li class="item<?php echo Minz_Request::actionName () == 'reading' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('configure', 'reading'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a>
 	</li>
 	<li class="item<?php echo Minz_Request::actionName () == 'archiving' ? ' active' : ''; ?>">
 		<a href="<?php echo _url ('configure', 'archiving'); ?>"><?php echo Minz_Translate::t ('archiving_configuration'); ?></a>

+ 14 - 3
app/layout/aside_feed.phtml

@@ -20,9 +20,14 @@
 							<?php echo $cat->name (); ?>
 						</option>
 						<?php } ?>
+						<option value="nc"><?php echo Minz_Translate::t ('new_category'); ?></option>
 						</select>
 					</li>
 
+					<li class="input" style="display:none">
+						<input type="text" name="new_category[name]" id="new_category_name" autocomplete="off" placeholder="<?php echo Minz_Translate::t ('new_category'); ?>" />
+					</li>
+
 					<li class="separator"></li>
 
 					<li class="dropdown-header"><?php echo Minz_Translate::t ('http_authentication'); ?></li>
@@ -38,8 +43,14 @@
 		</div>
 	</form></li>
 
-	<li class="item<?php echo Minz_Request::actionName () == 'importExport' ? ' active' : ''; ?>">
-		<a href="<?php echo _url ('configure', 'importExport'); ?>"><?php echo Minz_Translate::t ('import_export_opml'); ?></a>
+	<li class="item">
+		<a onclick="return false;" href="javascript:(function(){var%20url%20=%20location.href;window.open('<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'add'), 'html', true); ?>&amp;url_rss='+encodeURIComponent(url), '_blank');})();">
+			<?php echo Minz_Translate::t('bookmark'); ?>
+		</a>
+	</li>
+
+	<li class="item<?php echo Minz_Request::controllerName () == 'importExport' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('importExport', 'index'); ?>"><?php echo Minz_Translate::t ('import_export'); ?></a>
 	</li>
 
 	<li class="item<?php echo Minz_Request::actionName () == 'categorize' ? ' active' : ''; ?>">
@@ -51,7 +62,7 @@
 	<?php if (!empty ($this->feeds)) { ?>
 	<?php foreach ($this->feeds as $feed) { ?>
 	<?php $nbEntries = $feed->nbEntries (); ?>
-	<li class="item<?php echo ($this->flux && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>">
+	<li class="item<?php echo (isset($this->flux) && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>">
 		<a href="<?php echo _url ('configure', 'feed', 'id', $feed->id ()); ?>">
 			<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" />
 			<?php echo $feed->name (); ?>

+ 2 - 2
app/layout/aside_flux.phtml

@@ -20,7 +20,7 @@
 			}
 		?>
 		<li>
-			<div class="category all">
+			<div class="category all<?php echo $this->get_c == 'a' ? ' active' : ''; ?>">
 				<a data-unread="<?php echo formatNumber($this->nb_not_read); ?>" class="btn<?php echo $this->get_c == 'a' ? ' active' : ''; ?>" href="<?php echo Minz_Url::display($arUrl); ?>">
 					<?php echo FreshRSS_Themes::icon('all'); ?>
 					<?php echo Minz_Translate::t ('main_stream'); ?>
@@ -29,7 +29,7 @@
 		</li>
 
 		<li>
-			<div class="category favorites">
+			<div class="category favorites<?php echo $this->get_c == 's' ? ' active' : ''; ?>">
 				<a data-unread="<?php echo formatNumber($this->nb_favorites['unread']); ?>" class="btn<?php echo $this->get_c == 's' ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 's'; echo Minz_Url::display($arUrl); ?>">
 					<?php echo FreshRSS_Themes::icon('bookmark'); ?>
 					<?php echo Minz_Translate::t('favorite_feeds', formatNumber($this->nb_favorites['all'])); ?>

+ 3 - 2
app/layout/header.phtml

@@ -67,7 +67,8 @@ if (Minz_Configuration::canLogIn()) {
 			<ul class="dropdown-menu">
 				<li class="dropdown-close"><a href="#close">❌</a></li>
 				<li class="dropdown-header"><?php echo Minz_Translate::t ('configuration'); ?></li>
-				<li class="item"><a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a></li>
+				<li class="item"><a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('display_configuration'); ?></a></li>
+				<li class="item"><a href="<?php echo _url ('configure', 'reading'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a></li>
 				<li class="item"><a href="<?php echo _url ('configure', 'archiving'); ?>"><?php echo Minz_Translate::t ('archiving_configuration'); ?></a></li>
 				<li class="item"><a href="<?php echo _url ('configure', 'sharing'); ?>"><?php echo Minz_Translate::t ('sharing'); ?></a></li>
 				<li class="item"><a href="<?php echo _url ('configure', 'shortcut'); ?>"><?php echo Minz_Translate::t ('shortcuts'); ?></a></li>
@@ -75,8 +76,8 @@ if (Minz_Configuration::canLogIn()) {
 				<li class="item"><a href="<?php echo _url ('configure', 'users'); ?>"><?php echo Minz_Translate::t ('users'); ?></a></li>
 				<li class="separator"></li>
 				<li class="item"><a href="<?php echo _url ('index', 'stats'); ?>"><?php echo Minz_Translate::t ('stats'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about'); ?></a></li>
 				<li class="item"><a href="<?php echo _url ('index', 'logs'); ?>"><?php echo Minz_Translate::t ('logs'); ?></a></li>
+				<li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about'); ?></a></li>
 				<?php
 				if (Minz_Configuration::canLogIn()) {
 					?><li class="separator"></li><?php

+ 27 - 14
app/layout/layout.phtml

@@ -3,46 +3,59 @@
 	<head>
 		<meta charset="UTF-8" />
 		<meta name="viewport" content="initial-scale=1.0" />
-		<?php echo self::headTitle (); ?>
-		<?php echo self::headStyle (); ?>
-		<?php echo self::headScript (); ?>
+		<?php echo self::headTitle(); ?>
+		<?php echo self::headStyle(); ?>
+		<?php echo self::headScript(); ?>
 		<script>//<![CDATA[
-<?php $this->renderHelper ('javascript_vars'); ?>
+<?php $this->renderHelper('javascript_vars'); ?>
 		//]]></script>
 <?php
 	if (!empty($this->nextId)) {
-		$params = Minz_Request::params ();
+		$params = Minz_Request::params();
 		$params['next'] = $this->nextId;
 ?>
-		<link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display (array ('c' => Minz_Request::controllerName (), 'a' => Minz_Request::actionName (), 'params' => $params)); ?>" />
+		<link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display(array('c' => Minz_Request::controllerName(), 'a' => Minz_Request::actionName(), 'params' => $params)); ?>" />
 <?php } ?>
 		<link rel="shortcut icon" type="image/x-icon" sizes="16x16 64x64" href="<?php echo Minz_Url::display('/favicon.ico'); ?>" />
 		<link rel="icon msapplication-TileImage apple-touch-icon" type="image/png" sizes="256x256" href="<?php echo Minz_Url::display('/themes/icons/favicon-256.png'); ?>" />
-<?php if (isset ($this->rss_url)) { ?>
-		<link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display ($this->rss_url); ?>" />
+<?php
+	if (isset($this->url)) {
+		$rss_url = $this->url;
+		$rss_url['params']['output'] = 'rss';
+?>
+		<link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display($rss_url); ?>" />
 <?php } ?>
 		<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('starred', true); ?>">
 		<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('non-starred', true); ?>">
 		<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('read', true); ?>">
 		<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('unread', true); ?>">
+		<link rel="apple-touch-icon" href="<?php echo Minz_Url::display('/themes/icons/apple-touch-icon.png'); ?>">
+		<meta name="apple-mobile-web-app-capable" content="yes" />
+		<meta name="apple-mobile-web-app-status-bar-style" content="black" />
+		<meta name="apple-mobile-web-app-title" content="<?php echo Minz_Configuration::title(); ?>">
 		<meta name="msapplication-TileColor" content="#FFF" />
 		<meta name="robots" content="noindex,nofollow" />
 	</head>
 	<body class="<?php echo Minz_Request::param('output', 'normal'); ?>">
-<?php $this->partial ('header'); ?>
+<?php $this->partial('header'); ?>
 
 <div id="global">
-	<?php $this->render (); ?>
+	<?php $this->render(); ?>
 </div>
 
 <?php
-	if (isset ($this->notification)) {
+	$msg = '';
+	$status = 'closed';
+	if (isset($this->notification)) {
+		$msg = $this->notification['content'];
+		$status = $this->notification['type'];
+
 		invalidateHttpCache();
+	}
 ?>
-<div class="notification <?php echo $this->notification['type']; ?>">
-	<?php echo $this->notification['content']; ?>
+<div id="notification" class="notification <?php echo $status; ?>">
+	<span class="msg"><?php echo $msg; ?></span>
 	<a class="close" href=""><?php echo FreshRSS_Themes::icon('close'); ?></a>
 </div>
-<?php } ?>
 	</body>
 </html>

+ 126 - 112
app/layout/nav_menu.phtml

@@ -7,8 +7,80 @@
 	<?php } ?>
 
 	<?php if ($this->loginOk) { ?>
-	<a id="actualize" class="btn" href="<?php echo _url ('feed', 'actualize'); ?>"><?php echo FreshRSS_Themes::icon('refresh'); ?></a>
-
+	<?php $url_state = $this->url;
+		if ($this->state & FreshRSS_Entry::STATE_READ) {
+			$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_READ;
+			$checked = 'true';
+			$class = 'active';
+		} else {
+			$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_READ;
+			$checked = 'false';
+			$class = '';
+		}
+	?>
+	<div class="stick">
+		<a id="toggle-read"
+		   class="btn <?php echo $class; ?>"
+		   aria-checked="<?php echo $checked; ?>"
+		   href="<?php echo Minz_Url::display ($url_state); ?>"
+		   title="<?php echo Minz_Translate::t ('show_read'); ?>">
+			<?php echo FreshRSS_Themes::icon('read'); ?>
+		</a>
+		<?php
+			if ($this->state & FreshRSS_Entry::STATE_NOT_READ) {
+				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_READ;
+				$checked = 'true';
+				$class = 'active';
+			} else {
+				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_NOT_READ;
+				$checked = 'false';
+				$class = '';
+			}
+		?>
+		<a id="toggle-unread"
+		   class="btn <?php echo $class; ?>"
+		   aria-checked="<?php echo $checked; ?>"
+		   href="<?php echo Minz_Url::display ($url_state); ?>"
+		   title="<?php echo Minz_Translate::t ('show_not_reads'); ?>">
+			<?php echo FreshRSS_Themes::icon('unread'); ?>
+		</a>
+		<?php
+			if ($this->state & FreshRSS_Entry::STATE_FAVORITE) {
+				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_FAVORITE;
+				$checked = 'true';
+				$class = 'active';
+			} else {
+				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_FAVORITE;
+				$checked = 'false';
+				$class = '';
+			}
+		?>
+		<a id="toggle-favorite"
+		   class="btn <?php echo $class; ?>"
+		   aria-checked="<?php echo $checked; ?>"
+		   href="<?php echo Minz_Url::display ($url_state); ?>"
+		   title="<?php echo Minz_Translate::t ('show_favorite'); ?>">
+			<?php echo FreshRSS_Themes::icon('starred'); ?>
+		</a>
+		<?php
+			if ($this->state & FreshRSS_Entry::STATE_NOT_FAVORITE) {
+				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_FAVORITE;
+				$checked = 'true';
+				$class = 'active';
+			} else {
+				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_NOT_FAVORITE;
+				$checked = 'false';
+				$class = '';
+			}
+		?>
+		<a id="toggle-not-favorite"
+		   class="btn <?php echo $class; ?>"
+		   aria-checked="<?php echo $checked; ?>"
+		   href="<?php echo Minz_Url::display ($url_state); ?>"
+		   title="<?php echo Minz_Translate::t ('show_not_favorite'); ?>">
+			<?php echo FreshRSS_Themes::icon('non-starred'); ?>
+		</a>
+	</div>
 	<?php
 		$get = false;
 		$string_mark = Minz_Translate::t ('mark_all_read');
@@ -59,8 +131,12 @@
 					break;
 			}
 		}
-		$p = isset($this->entries[0]) ? $this->entries[0] : null;
-		$idMax = $p === null ? '0' : $p->id();
+		if ($this->order === 'ASC') {
+			$idMax = 0;
+		} else {
+			$p = isset($this->entries[0]) ? $this->entries[0] : null;
+			$idMax = $p === null ? '0' : $p->id();
+		}
 
 		$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('get' => $get, 'nextGet' => $nextGet, 'idMax' => $idMax));
 		$output = Minz_Request::param('output', '');
@@ -80,7 +156,7 @@
 			<ul class="dropdown-menu">
 				<li class="dropdown-close"><a href="#close">❌</a></li>
 
-				<li class="item"><a href="<?php echo $markReadUrl; ?>"><?php echo $string_mark; ?></a></li> 
+				<li class="item"><a href="<?php echo $markReadUrl; ?>"><?php echo $string_mark; ?></a></li>
 				<li class="separator"></li>
 <?php
 	$today = $this->today;
@@ -93,113 +169,30 @@
 	</div>
 	<?php } ?>
 
-	<?php
-		$params = Minz_Request::params ();
-		if (isset ($params['search'])) {
-			$params['search'] = urlencode ($params['search']);
-		}
-		$url = array (
-			'c' => 'index',
-			'a' => 'index',
-			'params' => $params
-		);
-	?>
-	<div class="dropdown" id="nav_menu_views">
-		<div id="dropdown-views" class="dropdown-target"></div>
-		<a class="dropdown-toggle btn" href="#dropdown-views"><?php echo Minz_Translate::t ('display'); ?> <?php echo FreshRSS_Themes::icon('down'); ?></a>
-		<ul class="dropdown-menu">
-			<li class="dropdown-close"><a href="#close">❌</a></li>
-
-			<?php
-				$url_output = $url;
-				if ($actual_view !== 'normal') { ?>
-			<li class="item">
-				<?php $url_output['params']['output'] = 'normal'; ?>
-				<a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
-					<?php echo Minz_Translate::t ('normal_view'); ?>
-				</a>
-			</li>
-			<?php } if($actual_view !== 'reader') { ?>
-			<li class="item">
-				<?php $url_output['params']['output'] = 'reader'; ?>
-				<a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
-					<?php echo Minz_Translate::t ('reader_view'); ?>
-				</a>
-			</li>
-			<?php } if($actual_view !== 'global') { ?>
-			<li class="item">
-				<?php $url_output['params']['output'] = 'global'; ?>
-				<a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
-					<?php echo Minz_Translate::t ('global_view'); ?>
-				</a>
-			</li>
-			<?php } ?>
-
-			<li class="separator"></li>
-
-			<?php
-				$url_state = $url;
-				$url_state['params']['state'] = 'all';
-			?>
-			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'all') ? 'true' :'false'; ?>">
-				<a class="print_all" href="<?php echo Minz_Url::display ($url_state); ?>">
-					<?php echo Minz_Translate::t ('show_all_articles'); ?>
-				</a>
-			</li>
-			<?php
-				$url_state['params']['state'] = 'not_read';
-			?>
-			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'not_read') ? 'true' :'false'; ?>">
-				<a class="print_non_read" href="<?php echo Minz_Url::display ($url_state); ?>">
-					<?php echo Minz_Translate::t ('show_not_reads'); ?>
-				</a>
-			</li>
-			<?php
-				$url_state['params']['state'] = 'read';
-			?>
-			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'read') ? 'true' :'false'; ?>">
-				<a class="print_read" href="<?php echo Minz_Url::display ($url_state); ?>">
-					<?php echo Minz_Translate::t ('show_read'); ?>
-				</a>
-			</li>
-			<?php
-				$url_state['params']['state'] = 'favorite';
-			?>
-			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'favorite') ? 'true' :'false'; ?>">
-				<a class="print_favorite" href="<?php echo Minz_Url::display ($url_state); ?>">
-					<?php echo Minz_Translate::t ('show_favorite'); ?>
-				</a>
-			</li>
-
-			<li class="separator"></li>
-
-			<li class="item">
-				<?php
-					$url_order = $url;
-					if ($this->order === 'DESC') {
-						$url_order['params']['order'] = 'ASC';
-				?>
-				<a href="<?php echo Minz_Url::display ($url_order); ?>">
-					<?php echo Minz_Translate::t ('older_first'); ?>
-				</a>
-				<?php
-					} else {
-						$url_order['params']['order'] = 'DESC';
-				?>
-				<a href="<?php echo Minz_Url::display ($url_order); ?>">
-					<?php echo Minz_Translate::t ('newer_first'); ?>
-				</a>
-				<?php } ?>
-			</li>
-
-			<li class="separator"></li>
-
-			<li class="item">
-				<a class="view_rss" target="_blank" href="<?php echo Minz_Url::display ($this->rss_url); ?>">
-					<?php echo Minz_Translate::t ('rss_view'); ?>
-				</a>
-			</li>
-		</ul>
+	<?php $url_output = $this->url; ?>
+	<div class="stick" id="nav_menu_views">
+		<?php $url_output['params']['output'] = 'normal'; ?>
+		<a class="view_normal btn <?php echo $actual_view == 'normal'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('normal_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo FreshRSS_Themes::icon("view-normal"); ?>
+		</a>
+
+		<?php $url_output['params']['output'] = 'global'; ?>
+		<a class="view_global btn <?php echo $actual_view == 'global'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('global_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo FreshRSS_Themes::icon("view-global"); ?>
+		</a>
+
+		<?php $url_output['params']['output'] = 'reader'; ?>
+		<a class="view_reader btn <?php echo $actual_view == 'reader'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('reader_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo FreshRSS_Themes::icon("view-reader"); ?>
+		</a>
+
+		<?php
+			$url_output['params']['output'] = 'rss';
+			$url_output['params']['token'] = $this->conf->token;
+		?>
+		<a class="view_rss btn" target="_blank" title="<?php echo Minz_Translate::t ('rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo FreshRSS_Themes::icon('rss'); ?>
+		</a>
 	</div>
 
 	<div class="item search">
@@ -223,4 +216,25 @@
 			<?php } ?>
 		</form>
 	</div>
+	
+	<?php
+		if ($this->order === 'DESC') {
+			$order = 'ASC';
+			$icon = 'up';
+			$title = 'older_first';
+		} else {
+			$order = 'DESC';
+			$icon = 'down';
+			$title = 'newer_first';
+		}
+		$url_order = $this->url;
+		$url_order['params']['order'] = $order;
+	?>
+	<a class="btn" href="<?php echo Minz_Url::display ($url_order); ?>" title="<?php echo Minz_Translate::t ($title); ?>">
+		<?php echo FreshRSS_Themes::icon($icon); ?>
+	</a>
+	
+	<?php if ($this->loginOk || Minz_Configuration::allowAnonymousRefresh()) { ?>
+	<a id="actualize" class="btn" href="<?php echo _url ('feed', 'actualize'); ?>"><?php echo FreshRSS_Themes::icon('refresh'); ?></a>
+	<?php } ?>
 </div>

+ 0 - 1
app/sql.php

@@ -3,7 +3,6 @@ define('SQL_CREATE_TABLES', '
 CREATE TABLE IF NOT EXISTS `%1$scategory` (
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
 	`name` varchar(255) NOT NULL,
-	`color` char(7),
 	PRIMARY KEY (`id`),
 	UNIQUE KEY (`name`)	-- v0.7
 ) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci

+ 7 - 5
app/views/configure/categorize.phtml

@@ -14,14 +14,16 @@
 				<?php echo Minz_Translate::t ('category_number', $i); ?>
 			</label>
 			<div class="group-controls">
-				<input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" />
+				<div class="stick">
+					<input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" />
 
-				<?php if ($cat->nbFeed () > 0) { ?>
-				<a class="confirm" href="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Minz_Translate::t ('ask_empty'); ?></a>
-				<?php } ?>
+					<?php if ($cat->nbFeed () > 0) { ?>
+					<button type="submit" class="btn btn-attention confirm" formaction="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Minz_Translate::t ('ask_empty'); ?></button>
+					<?php } ?>
+				</div>
 				(<?php echo Minz_Translate::t ('number_feeds', $cat->nbFeed ()); ?>)
 
-				<?php if ($cat->id () == $this->defaultCategory->id ()) { ?>
+				<?php if ($cat->id () === $this->defaultCategory->id ()) { ?>
 				<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t ('can_not_be_deleted'); ?>
 				<?php } ?>
 

+ 17 - 110
app/views/configure/display.phtml

@@ -4,7 +4,7 @@
 	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
 
 	<form method="post" action="<?php echo _url ('configure', 'display'); ?>">
-		<legend><?php echo Minz_Translate::t ('theme'); ?></legend>
+		<legend><?php echo Minz_Translate::t ('display_configuration'); ?></legend>
 
 		<div class="form-group">
 			<label class="group-name" for="language"><?php echo Minz_Translate::t ('language'); ?></label>
@@ -35,122 +35,29 @@
 			</div>
 		</div>
 
-		<div class="form-group form-actions">
-			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
-				<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
-			</div>
-		</div>
-
-		<legend><?php echo Minz_Translate::t ('reading_configuration'); ?></legend>
-
-		<div class="form-group">
-			<label class="group-name" for="posts_per_page"><?php echo Minz_Translate::t ('articles_per_page'); ?></label>
-			<div class="group-controls">
-				<input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo $this->conf->posts_per_page; ?>" />
-			</div>
-		</div>
-
+		<?php $width = $this->conf->content_width; ?>
 		<div class="form-group">
-			<label class="group-name" for="sort_order"><?php echo Minz_Translate::t ('sort_order'); ?></label>
+			<label class="group-name" for="content_width"><?php echo Minz_Translate::t('content_width'); ?></label>
 			<div class="group-controls">
-				<select name="sort_order" id="sort_order">
-					<option value="DESC"<?php echo $this->conf->sort_order === 'DESC' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('newer_first'); ?></option>
-					<option value="ASC"<?php echo $this->conf->sort_order === 'ASC' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('older_first'); ?></option>
+				<select name="content_width" id="content_width" required="">
+					<option value="thin" <?php echo $width === 'thin'? 'selected="selected"' : ''; ?>>
+						<?php echo Minz_Translate::t('width_thin'); ?>
+					</option>
+					<option value="medium" <?php echo $width === 'medium'? 'selected="selected"' : ''; ?>>
+						<?php echo Minz_Translate::t('width_medium'); ?>
+					</option>
+					<option value="large" <?php echo $width === 'large'? 'selected="selected"' : ''; ?>>
+						<?php echo Minz_Translate::t('width_large'); ?>
+					</option>
+					<option value="no_limit" <?php echo $width === 'no_limit'? 'selected="selected"' : ''; ?>>
+						<?php echo Minz_Translate::t('width_no_limit'); ?>
+					</option>
 				</select>
 			</div>
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="view_mode"><?php echo Minz_Translate::t ('default_view'); ?></label>
-			<div class="group-controls">
-				<select name="view_mode" id="view_mode">
-					<option value="normal"<?php echo $this->conf->view_mode === 'normal' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('normal_view'); ?></option>
-					<option value="reader"<?php echo $this->conf->view_mode === 'reader' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('reader_view'); ?></option>
-					<option value="global"<?php echo $this->conf->view_mode === 'global' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('global_view'); ?></option>
-				</select>
-				<label class="radio" for="radio_all">
-					<input type="radio" name="default_view" id="radio_all" value="all"<?php echo $this->conf->default_view === 'all' ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('show_all_articles'); ?>
-				</label>
-				<label class="radio" for="radio_not_read">
-					<input type="radio" name="default_view" id="radio_not_read" value="not_read"<?php echo $this->conf->default_view === 'not_read' ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('show_not_reads'); ?>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<div class="group-controls">
-				<label class="checkbox" for="auto_load_more">
-					<input type="checkbox" name="auto_load_more" id="auto_load_more" value="1"<?php echo $this->conf->auto_load_more ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('auto_load_more'); ?>
-					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<div class="group-controls">
-				<label class="checkbox" for="display_posts">
-					<input type="checkbox" name="display_posts" id="display_posts" value="1"<?php echo $this->conf->display_posts ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('display_articles_unfolded'); ?>
-					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<div class="group-controls">
-				<label class="checkbox" for="lazyload">
-					<input type="checkbox" name="lazyload" id="lazyload" value="1"<?php echo $this->conf->lazyload ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('img_with_lazyload'); ?>
-					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name"><?php echo Minz_Translate::t ('auto_read_when'); ?></label>
-			<div class="group-controls">
-				<label class="checkbox" for="check_open_article">
-					<input type="checkbox" name="mark_open_article" id="check_open_article" value="1"<?php echo $this->conf->mark_when['article'] ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('article_selected'); ?>
-				</label>
-				<label class="checkbox" for="check_open_site">
-					<input type="checkbox" name="mark_open_site" id="check_open_site" value="1"<?php echo $this->conf->mark_when['site'] ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('article_open_on_website'); ?>
-				</label>
-				<label class="checkbox" for="check_scroll">
-					<input type="checkbox" name="mark_scroll" id="check_scroll" value="1"<?php echo $this->conf->mark_when['scroll'] ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('scroll'); ?>
-				</label>
-				<label class="checkbox" for="check_reception">
-					<input type="checkbox" name="mark_upon_reception" id="check_reception" value="1"<?php echo $this->conf->mark_when['reception'] ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('upon_reception'); ?>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name"><?php echo Minz_Translate::t ('after_onread'); ?></label>
-			<div class="group-controls">
-				<label class="checkbox" for="onread_jump_next">
-					<input type="checkbox" name="onread_jump_next" id="onread_jump_next" value="1"<?php echo $this->conf->onread_jump_next ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('jump_next'); ?>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group form-actions">
-			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
-				<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
-			</div>
-		</div>
-
-		<legend><?php echo Minz_Translate::t ('reading_icons'); ?></legend>
-		<div class="form-group">
+			<label class="group-name" for="theme"><?php echo Minz_Translate::t ('article_icons'); ?></label>
 			<table>
 				<thead>
 					<tr>

+ 15 - 6
app/views/configure/feed.phtml

@@ -7,8 +7,12 @@
 	<h1><?php echo $this->flux->name (); ?></h1>
 	<?php echo $this->flux->description (); ?>
 
+	<?php $nbEntries = $this->flux->nbEntries (); ?>
+
 	<?php if ($this->flux->inError ()) { ?>
 	<p class="alert alert-error"><span class="alert-head"><?php echo Minz_Translate::t ('damn'); ?></span> <?php echo Minz_Translate::t ('feed_in_error'); ?></p>
+	<?php } elseif ($nbEntries === 0) { ?>
+	<p class="alert alert-warn"><?php echo Minz_Translate::t ('feed_empty'); ?></p>
 	<?php } ?>
 
 	<form method="post" action="<?php echo _url ('configure', 'feed', 'id', $this->flux->id ()); ?>" autocomplete="off">
@@ -28,16 +32,21 @@
 		<div class="form-group">
 			<label class="group-name" for="website"><?php echo Minz_Translate::t ('website_url'); ?></label>
 			<div class="group-controls">
-				<input type="text" name="website" id="website" class="extend" value="<?php echo $this->flux->website (); ?>" />
-				<a target="_blank" href="<?php echo $this->flux->website (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+				<div class="stick">
+					<input type="text" name="website" id="website" class="extend" value="<?php echo $this->flux->website (); ?>" />
+					<a class="btn" target="_blank" href="<?php echo $this->flux->website (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+				</div>
 			</div>
 		</div>
 		<div class="form-group">
 			<label class="group-name" for="url"><?php echo Minz_Translate::t ('feed_url'); ?></label>
 			<div class="group-controls">
-				<input type="text" name="url" id="url" class="extend" value="<?php echo $this->flux->url (); ?>" />
-				<a target="_blank" href="<?php echo $this->flux->url (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
-				  <a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->flux->url (); ?>"><?php echo Minz_Translate::t ('feed_validator'); ?></a>
+				<div class="stick">
+					<input type="text" name="url" id="url" class="extend" value="<?php echo $this->flux->url (); ?>" />
+					<a class="btn" target="_blank" href="<?php echo $this->flux->url (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+				</div>
+
+				<a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->flux->url (); ?>"><?php echo Minz_Translate::t ('feed_validator'); ?></a>
 			</div>
 		</div>
 		<div class="form-group">
@@ -81,7 +90,7 @@
 		<div class="form-group">
 			<label class="group-name"><?php echo Minz_Translate::t ('number_articles'); ?></label>
 			<div class="group-controls">
-				<span class="control"><?php echo $this->flux->nbEntries (); ?></span>
+				<span class="control"><?php echo $nbEntries; ?></span>
 			</div>
 		</div>
 		<div class="form-group">

+ 0 - 40
app/views/configure/importExport.phtml

@@ -1,40 +0,0 @@
-<?php
-require_once(LIB_PATH . '/lib_opml.php');
-if ($this->req == 'export') {
-	echo '<?xml version="1.0" encoding="UTF-8" ?>';
-?>
-<!-- Generated by <?php echo Minz_Configuration::title (); ?> -->
-<opml version="2.0">
-	<head>
-		<title><?php echo Minz_Configuration::title (); ?> OPML Feed</title>
-		<dateCreated><?php echo date('D, d M Y H:i:s'); ?></dateCreated>
-	</head>
-	<body>
-<?php echo opml_export ($this->categories); ?>
-	</body>
-</opml>
-<?php } else { ?>
-<?php $this->partial ('aside_feed'); ?>
-
-<div class="post ">
-	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
-
-	<form method="post" action="<?php echo Minz_Url::display (array ('c' => 'configure', 'a' => 'importExport', 'params' => array ('q' => 'import'))); ?>" enctype="multipart/form-data">
-		<legend><?php echo Minz_Translate::t ('import_export_opml'); ?></legend>
-		<div class="form-group">
-			<label class="group-name" for="file"><?php echo Minz_Translate::t ('file_to_import'); ?></label>
-			<div class="group-controls">
-				<input type="file" name="file" id="file" />
-			</div>
-		</div>
-
-		<div class="form-group form-actions">
-			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('import'); ?></button>
-				<?php echo Minz_Translate::t ('or'); ?>
-				<a target="_blank" class="btn btn-important" href="<?php echo _url ('configure', 'importExport', 'q', 'export'); ?>"><?php echo Minz_Translate::t ('export'); ?></a>
-			</div>
-		</div>
-	</form>
-</div>
-<?php } ?>

+ 125 - 0
app/views/configure/reading.phtml

@@ -0,0 +1,125 @@
+<?php $this->partial ('aside_configure'); ?>
+
+<div class="post">
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
+
+	<form method="post" action="<?php echo _url ('configure', 'reading'); ?>">
+		<legend><?php echo Minz_Translate::t ('reading_configuration'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="posts_per_page"><?php echo Minz_Translate::t ('articles_per_page'); ?></label>
+			<div class="group-controls">
+				<input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo $this->conf->posts_per_page; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="sort_order"><?php echo Minz_Translate::t ('sort_order'); ?></label>
+			<div class="group-controls">
+				<select name="sort_order" id="sort_order">
+					<option value="DESC"<?php echo $this->conf->sort_order === 'DESC' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('newer_first'); ?></option>
+					<option value="ASC"<?php echo $this->conf->sort_order === 'ASC' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('older_first'); ?></option>
+				</select>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="view_mode"><?php echo Minz_Translate::t ('default_view'); ?></label>
+			<div class="group-controls">
+				<select name="view_mode" id="view_mode">
+					<option value="normal"<?php echo $this->conf->view_mode === 'normal' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('normal_view'); ?></option>
+					<option value="reader"<?php echo $this->conf->view_mode === 'reader' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('reader_view'); ?></option>
+					<option value="global"<?php echo $this->conf->view_mode === 'global' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('global_view'); ?></option>
+				</select>
+				<label class="radio" for="radio_all">
+					<input type="radio" name="default_view" id="radio_all" value="<?php echo FreshRSS_Entry::STATE_ALL; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_ALL ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('show_all_articles'); ?>
+				</label>
+				<label class="radio" for="radio_not_read">
+					<input type="radio" name="default_view" id="radio_not_read" value="<?php echo FreshRSS_Entry::STATE_NOT_READ; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_NOT_READ ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('show_not_reads'); ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="auto_load_more">
+					<input type="checkbox" name="auto_load_more" id="auto_load_more" value="1"<?php echo $this->conf->auto_load_more ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('auto_load_more'); ?>
+					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="display_posts">
+					<input type="checkbox" name="display_posts" id="display_posts" value="1"<?php echo $this->conf->display_posts ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('display_articles_unfolded'); ?>
+					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="lazyload">
+					<input type="checkbox" name="lazyload" id="lazyload" value="1"<?php echo $this->conf->lazyload ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('img_with_lazyload'); ?>
+					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="sticky_post">
+					<input type="checkbox" name="sticky_post" id="sticky_post" value="1"<?php echo $this->conf->sticky_post ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('sticky_post'); ?>
+					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name"><?php echo Minz_Translate::t ('auto_read_when'); ?></label>
+			<div class="group-controls">
+				<label class="checkbox" for="check_open_article">
+					<input type="checkbox" name="mark_open_article" id="check_open_article" value="1"<?php echo $this->conf->mark_when['article'] ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('article_selected'); ?>
+				</label>
+				<label class="checkbox" for="check_open_site">
+					<input type="checkbox" name="mark_open_site" id="check_open_site" value="1"<?php echo $this->conf->mark_when['site'] ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('article_open_on_website'); ?>
+				</label>
+				<label class="checkbox" for="check_scroll">
+					<input type="checkbox" name="mark_scroll" id="check_scroll" value="1"<?php echo $this->conf->mark_when['scroll'] ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('scroll'); ?>
+				</label>
+				<label class="checkbox" for="check_reception">
+					<input type="checkbox" name="mark_upon_reception" id="check_reception" value="1"<?php echo $this->conf->mark_when['reception'] ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('upon_reception'); ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name"><?php echo Minz_Translate::t ('after_onread'); ?></label>
+			<div class="group-controls">
+				<label class="checkbox" for="onread_jump_next">
+					<input type="checkbox" name="onread_jump_next" id="onread_jump_next" value="1"<?php echo $this->conf->onread_jump_next ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('jump_next'); ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
+			</div>
+		</div>
+
+	</form>
+</div>

+ 38 - 43
app/views/configure/sharing.phtml

@@ -3,54 +3,49 @@
 <div class="post">
 	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
 
-	<form method="post" action="<?php echo _url ('configure', 'sharing'); ?>">
+	<form method="post" action="<?php echo _url ('configure', 'sharing'); ?>"
+		data-simple='<div class="form-group"><label class="group-name">##label##</label><div class="group-controls"><a href="#" class="share remove btn btn-attention"><?php echo FreshRSS_Themes::icon('close'); ?></a>
+			<input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" /></div></div>'
+		data-advanced='<div class="form-group"><label class="group-name">##label##</label><div class="group-controls">
+			<input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" />
+			<div class="stick">
+			<input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
+			<input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
+			<a href="#" class="share remove btn btn-attention"><?php echo FreshRSS_Themes::icon('close'); ?></a></div>
+			<a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="##help##"><?php echo FreshRSS_Themes::icon('help'); ?></a>
+			</div></div>'>
 		<legend><?php echo Minz_Translate::t ('sharing'); ?></legend>
-		<div class="form-group">
-			<label class="group-name" for="shaarli">
-				<?php echo Minz_Translate::t ('your_shaarli'); ?>
-			</label>
-			<div class="group-controls">
-				<input type="url" id="shaarli" name="shaarli" class="extend" value="<?php echo $this->conf->sharing ('shaarli'); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" size="64" />
-
-				<?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="http://sebsauvage.net/wiki/doku.php?id=php:shaarli"><?php echo Minz_Translate::t ('more_information'); ?></a>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name" for="wallabag">
-				<?php echo Minz_Translate::t ('your_wallabag'); ?>
-			</label>
-			<div class="group-controls">
-				<input type="url" id="wallabag" name="wallabag" class="extend" value="<?php echo $this->conf->sharing ('wallabag'); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" size="64" />
-
-				<?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="http://www.wallabag.org"><?php echo Minz_Translate::t ('more_information'); ?></a>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name" for="diaspora">
-				<?php echo Minz_Translate::t ('your_diaspora_pod'); ?>
-			</label>
-			<div class="group-controls">
-				<input type="url" id="diaspora" name="diaspora" class="extend" value="<?php echo $this->conf->sharing ('diaspora'); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" size="64" />
-
-				<?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="https://diasporafoundation.org/"><?php echo Minz_Translate::t ('more_information'); ?></a>
+		<?php foreach ($this->conf->sharing as $key => $sharing): ?>
+			<?php $share = $this->conf->shares[$sharing['type']]; ?>
+			<div class="form-group">
+				<label class="group-name">
+					<?php echo Minz_Translate::t ($sharing['type']); ?>
+				</label>
+				<div class="group-controls">
+					<input type='hidden' id='share_<?php echo $key;?>_type' name="share[<?php echo $key;?>][type]" value='<?php echo $sharing['type']?>' />
+					<?php if ($share['form'] === 'advanced'){ ?>
+						<div class="stick">
+							<input type="text" id="share_<?php echo $key;?>_name" name="share[<?php echo $key;?>][name]" class="extend" value="<?php echo $sharing['name']?>" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
+							<input type="url" id="share_<?php echo $key;?>_url" name="share[<?php echo $key;?>][url]" class="extend" value="<?php echo $sharing['url']?>" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
+							<a href='#' class='share remove btn btn-attention'><?php echo FreshRSS_Themes::icon('close'); ?></a>
+						</div>
+
+						<a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="<?php echo $share['help']?>"><?php echo FreshRSS_Themes::icon('help'); ?></a>
+					<?php } else { ?>
+					<a href='#' class='share remove btn btn-attention'><?php echo FreshRSS_Themes::icon('close'); ?></a>
+					<?php } ?>
+				</div>
 			</div>
-		</div>
+		<?php endforeach;?>
 
 		<div class="form-group">
-			<label class="group-name"><?php echo Minz_Translate::t ('activate_sharing'); ?></label>
 			<div class="group-controls">
-				<?php
-					$services = array ('twitter', 'g+', 'facebook', 'email', 'print');
-
-					foreach ($services as $service) {
-				?>
-				<label class="checkbox" for="<?php echo $service; ?>">
-					<input type="checkbox" name="<?php echo $service; ?>" id="<?php echo $service; ?>" value="1"<?php echo $this->conf->sharing($service) ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ($service); ?>
-				</label>
-				<?php } ?>
+				<select>
+					<?php foreach($this->conf->shares as $key => $params):?>
+						<option value='<?php echo $key?>' data-form='<?php echo $params['form']?>' data-help='<?php if (!empty($params['help'])) {echo $params['help'];}?>'><?php echo Minz_Translate::t($key) ?></option>
+					<?php endforeach; ?>
+				</select>
+				<a href='#' class='share add btn'><?php echo FreshRSS_Themes::icon('add'); ?></a>
 			</div>
 		</div>
 

+ 42 - 14
app/views/configure/shortcut.phtml

@@ -12,10 +12,44 @@
 	<?php $s = $this->conf->shortcuts; ?>
 
 	<form method="post" action="<?php echo _url ('configure', 'shortcut'); ?>">
-		<legend><?php echo Minz_Translate::t ('shortcuts_management'); ?></legend>
+		<legend><?php echo Minz_Translate::t ('shortcuts'); ?></legend>
 
 		<noscript><p class="alert alert-error"><?php echo Minz_Translate::t ('javascript_for_shortcuts'); ?></p></noscript>
 
+		<legend><?php echo Minz_Translate::t ('shortcuts_navigation'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="next_entry"><?php echo Minz_Translate::t ('next_article'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="next_entry" name="shortcuts[next_entry]" list="keys" value="<?php echo $s['next_entry']; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="prev_entry"><?php echo Minz_Translate::t ('previous_article'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="prev_entry" name="shortcuts[prev_entry]" list="keys" value="<?php echo $s['prev_entry']; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="first_entry"><?php echo Minz_Translate::t ('first_article'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="first_entry" name="shortcuts[first_entry]" list="keys" value="<?php echo $s['first_entry']; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="last_entry"><?php echo Minz_Translate::t ('last_article'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="last_entry" name="shortcuts[last_entry]" list="keys" value="<?php echo $s['last_entry']; ?>" />
+			</div>
+		</div>
+
+		<div><?php echo Minz_Translate::t ('shortcuts_navigation_help');?></div>
+
+		<legend><?php echo Minz_Translate::t ('shortcuts_article_action');?></legend>
+
 		<div class="form-group">
 			<label class="group-name" for="mark_read"><?php echo Minz_Translate::t ('mark_read'); ?></label>
 			<div class="group-controls">
@@ -39,18 +73,10 @@
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="next_entry"><?php echo Minz_Translate::t ('next_article'); ?></label>
-			<div class="group-controls">
-				<input type="text" id="next_entry" name="shortcuts[next_entry]" list="keys" value="<?php echo $s['next_entry']; ?>" />
-				<?php echo Minz_Translate::t ('shift_for_last'); ?>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name" for="prev_entry"><?php echo Minz_Translate::t ('previous_article'); ?></label>
+			<label class="group-name" for="auto_share_shortcut"><?php echo Minz_Translate::t ('auto_share'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="prev_entry" name="shortcuts[prev_entry]" list="keys" value="<?php echo $s['prev_entry']; ?>" />
-				<?php echo Minz_Translate::t ('shift_for_first'); ?>
+				<input type="text" id="auto_share_shortcut" name="shortcuts[auto_share]" list="keys" value="<?php echo $s['auto_share']; ?>" />
+				<?php echo Minz_Translate::t ('auto_share_help'); ?>
 			</div>
 		</div>
 
@@ -61,6 +87,8 @@
 			</div>
 		</div>
 
+		<legend><?php echo Minz_Translate::t ('shortcuts_other_action');?></legend>
+
 		<div class="form-group">
 			<label class="group-name" for="load_more_shortcut"><?php echo Minz_Translate::t ('load_more'); ?></label>
 			<div class="group-controls">
@@ -69,9 +97,9 @@
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="auto_share_shortcut"><?php echo Minz_Translate::t ('auto_share'); ?></label>
+			<label class="group-name" for="focus_search_shortcut"><?php echo Minz_Translate::t ('focus_search'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="auto_share_shortcut" name="shortcuts[auto_share]" list="keys" value="<?php echo $s['auto_share']; ?>" />
+				<input type="text" id="focus_search_shortcut" name="shortcuts[focus_search]" list="keys" value="<?php echo $s['focus_search']; ?>" />
 			</div>
 		</div>
 

+ 56 - 7
app/views/configure/users.phtml

@@ -20,16 +20,31 @@
 		<div class="form-group">
 			<label class="group-name" for="passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
 			<div class="group-controls">
-				<input type="password" id="passwordPlain" name="passwordPlain" pattern=".{7,}" />
+				<div class="stick">
+					<input type="password" id="passwordPlain" name="passwordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
+					<a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				</div>
 				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
 			</div>
 		</div>
 
+		<?php if (Minz_Configuration::apiEnabled()) { ?>
+		<div class="form-group">
+			<label class="group-name" for="apiPasswordPlain"><?php echo Minz_Translate::t('password_api'); ?></label>
+			<div class="group-controls">
+				<div class="stick">
+					<input type="password" id="apiPasswordPlain" name="apiPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
+					<a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				</div>
+			</div>
+		</div>
+		<?php } ?>
+
 		<div class="form-group">
 			<label class="group-name" for="mail_login"><?php echo Minz_Translate::t('persona_connection_email'); ?></label>
 			<?php $mail = $this->conf->mail_login; ?>
 			<div class="group-controls">
-				<input type="email" id="mail_login" name="mail_login" class="extend" value="<?php echo $mail; ?>" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? '' : 'disabled="disabled"'; ?> placeholder="alice@example.net" />
+				<input type="email" id="mail_login" name="mail_login" class="extend" autocomplete="off" value="<?php echo $mail; ?>" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? '' : 'disabled="disabled"'; ?> placeholder="alice@example.net" />
 				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
 			</div>
 		</div>
@@ -52,7 +67,7 @@
 					<?php if (!in_array(Minz_Configuration::authType(), array('form', 'persona', 'http_auth', 'none'))) { ?>
 						<option selected="selected"></option>
 					<?php } ?>
-					<option value="form"<?php echo Minz_Configuration::authType() === 'form' ? ' selected="selected"' : '', version_compare(PHP_VERSION, '5.3', '<') ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('auth_form'); ?></option>
+					<option value="form"<?php echo Minz_Configuration::authType() === 'form' ? ' selected="selected"' : '', cryptAvailable() ? '' : ' disabled="disabled"'; ?>><?php echo Minz_Translate::t('auth_form'); ?></option>
 					<option value="persona"<?php echo Minz_Configuration::authType() === 'persona' ? ' selected="selected"' : '', $this->conf->mail_login == '' ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('auth_persona'); ?></option>
 					<option value="http_auth"<?php echo Minz_Configuration::authType() === 'http_auth' ? ' selected="selected"' : '', httpAuthUser() == '' ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('http_auth'); ?> (REMOTE_USER = '<?php echo httpAuthUser(); ?>')</option>
 					<option value="none"<?php echo Minz_Configuration::authType() === 'none' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t('auth_none'); ?></option>
@@ -70,18 +85,49 @@
 			</div>
 		</div>
 
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="anon_refresh">
+					<input type="checkbox" name="anon_refresh" id="anon_refresh" value="1"<?php echo Minz_Configuration::allowAnonymousRefresh() ? ' checked="checked"' : '',
+						Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
+					<?php echo Minz_Translate::t('allow_anonymous_refresh'); ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="unsafe_autologin">
+					<input type="checkbox" name="unsafe_autologin" id="unsafe_autologin" value="1"<?php echo Minz_Configuration::unsafeAutologinEnabled() ? ' checked="checked"' : '',
+						Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
+					<?php echo Minz_Translate::t('unsafe_autologin'); ?>
+					<kbd>p/i/?a=formLogin&amp;u=Alice&amp;p=1234</kbd>
+				</label>
+			</div>
+		</div>
+
 		<?php if (Minz_Configuration::canLogIn()) { ?>
 		<div class="form-group">
 			<label class="group-name" for="token"><?php echo Minz_Translate::t('auth_token'); ?></label>
 			<?php $token = $this->conf->token; ?>
 			<div class="group-controls">
-				<input type="text" id="token" name="token" value="<?php echo $token; ?>"  placeholder="<?php echo Minz_Translate::t('blank_to_disable'); ?>"<?php
+				<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo Minz_Translate::t('blank_to_disable'); ?>"<?php
 					echo Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
 				<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('explain_token', Minz_Url::display(null, 'html', true), $token); ?>
 			</div>
 		</div>
 		<?php } ?>
 
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="api_enabled">
+					<input type="checkbox" name="api_enabled" id="api_enabled" value="1"<?php echo Minz_Configuration::apiEnabled() ? ' checked="checked"' : '',
+						Minz_Configuration::needsLogin() ? '' : ' disabled="disabled"'; ?> />
+					<?php echo Minz_Translate::t('api_enabled'); ?>
+				</label>
+			</div>
+		</div>
+
 		<div class="form-group form-actions">
 			<div class="group-controls">
 				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button>
@@ -129,14 +175,17 @@
 		<div class="form-group">
 			<label class="group-name" for="new_user_name"><?php echo Minz_Translate::t('username'); ?></label>
 			<div class="group-controls">
-				<input id="new_user_name" name="new_user_name" type="text" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" placeholder="demo" />
+				<input id="new_user_name" name="new_user_name" type="text" size="16" required="required" maxlength="16" autocomplete="off" pattern="[0-9a-zA-Z]{1,16}" placeholder="demo" />
 			</div>
 		</div>
 
 		<div class="form-group">
 			<label class="group-name" for="new_user_passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
 			<div class="group-controls">
-				<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" pattern=".{7,}" />
+				<div class="stick">
+					<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="off" pattern=".{7,}" />
+					<a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				</div>
 				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
 			</div>
 		</div>
@@ -145,7 +194,7 @@
 			<label class="group-name" for="new_user_email"><?php echo Minz_Translate::t('persona_connection_email'); ?></label>
 			<?php $mail = $this->conf->mail_login; ?>
 			<div class="group-controls">
-				<input type="email" id="new_user_email" name="new_user_email" class="extend" placeholder="alice@example.net" />
+				<input type="email" id="new_user_email" name="new_user_email" class="extend" autocomplete="off" placeholder="alice@example.net" />
 			</div>
 		</div>
 

+ 9 - 1
app/views/error/index.phtml

@@ -3,7 +3,15 @@
 		<h1 class="alert-head"><?php echo $this->code; ?></h1>
 
 		<p>
-			<?php echo Minz_Translate::t ('page_not_found'); ?><br />
+			<?php
+			switch(Minz_Request::param ('code')) {
+			case 403:
+				echo Minz_Translate::t ('forbidden_access');
+				break;
+			case 404:
+			default:
+				echo Minz_Translate::t ('page_not_found');
+			} ?><br />
 			<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
 		</p>
 	</div>

+ 91 - 0
app/views/feed/add.phtml

@@ -0,0 +1,91 @@
+<?php if ($this->feed) { ?>
+<div class="post">
+	<h1><?php echo Minz_Translate::t ('add_rss_feed'); ?></h1>
+
+	<?php if (!$this->load_ok) { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo Minz_Translate::t('damn'); ?></span> <?php echo Minz_Translate::t('internal_problem_feed', _url('index', 'logs')); ?></p>
+	<?php } ?>
+
+	<form method="post" action="<?php echo _url('feed', 'add'); ?>" autocomplete="off">
+		<legend><?php echo Minz_Translate::t('informations'); ?></legend>
+		<?php if ($this->load_ok) { ?>
+		<div class="form-group">
+			<label class="group-name"><?php echo Minz_Translate::t('title'); ?></label>
+			<div class="group-controls">
+				<label><?php echo $this->feed->name() ; ?></label>
+			</div>
+		</div>
+
+		<?php $desc = $this->feed->description(); if ($desc != '') { ?>
+		<div class="form-group">
+			<label class="group-name"><?php echo Minz_Translate::t('feed_description'); ?></label>
+			<div class="group-controls">
+				<label><?php echo htmlspecialchars($desc, ENT_NOQUOTES, 'UTF-8'); ?></label>
+			</div>
+		</div>
+		<?php } ?>
+
+		<div class="form-group">
+			<label class="group-name"><?php echo Minz_Translate::t('website_url'); ?></label>
+			<div class="group-controls">
+				<?php echo $this->feed->website(); ?>
+				<a class="btn" target="_blank" href="<?php echo $this->feed->website(); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+			</div>
+		</div>
+		<?php } ?>
+
+		<div class="form-group">
+			<label class="group-name" for="url"><?php echo Minz_Translate::t('feed_url'); ?></label>
+			<div class="group-controls">
+				<div class="stick">
+					<input type="text" name="url_rss" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" />
+					<a class="btn" target="_blank" href="<?php echo $this->feed->url(); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+				</div>
+				<a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->feed->url(); ?>"><?php echo Minz_Translate::t('feed_validator'); ?></a>
+			</div>
+		</div>
+		<div class="form-group">
+			<label class="group-name" for="category"><?php echo Minz_Translate::t('category'); ?></label>
+			<div class="group-controls">
+				<select name="category" id="category">
+					<?php foreach ($this->categories as $cat) { ?>
+					<option value="<?php echo $cat->id(); ?>"<?php echo $cat->id() == 1 ? ' selected="selected"' : ''; ?>>
+						<?php echo $cat->name(); ?>
+					</option>
+					<?php } ?>
+					<option value="nc"><?php echo Minz_Translate::t('new_category'); ?></option>
+				</select>
+
+				<span style="display: none;">
+					<input type="text" name="new_category[name]" id="new_category_name" autocomplete="off" placeholder="<?php echo Minz_Translate::t('new_category'); ?>" />
+				</span>
+			</div>
+		</div>
+
+		<legend><?php echo Minz_Translate::t('http_authentication'); ?></legend>
+		<?php $auth = $this->feed->httpAuth(false); ?>
+		<div class="form-group">
+			<label class="group-name" for="http_user"><?php echo Minz_Translate::t('http_username'); ?></label>
+			<div class="group-controls">
+				<input type="text" name="http_user" id="http_user" class="extend" value="<?php echo $auth['username']; ?>" autocomplete="off" />
+			</div>
+
+			<label class="group-name" for="http_pass"><?php echo Minz_Translate::t('http_password'); ?></label>
+			<div class="group-controls">
+				<input type="password" name="http_pass" id="http_pass" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="off" />
+			</div>
+
+			<div class="group-controls">
+				<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('access_protected_feeds'); ?>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
+			</div>
+		</div>
+	</form>
+</div>
+<?php } ?>

+ 47 - 0
app/views/helpers/export/articles.phtml

@@ -0,0 +1,47 @@
+<?php
+    $username = Minz_Session::param('currentUser', '_');
+
+    $articles = array(
+        'id' => 'user/' . str_replace('/', '', $username) . '/state/org.freshrss/' . $this->type,
+        'title' => $this->list_title,
+        'author' => $username,
+        'items' => array()
+    );
+
+    foreach ($this->entries as $entry) {
+        if (!isset($this->feed)) {
+            $feed = FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feed ());
+        } else {
+            $feed = $this->feed;
+        }
+
+        $articles['items'][] = array(
+            'id' => $entry->guid(),
+            'categories' => array_values($entry->tags()),
+            'title' => $entry->title(),
+            'author' => $entry->author(),
+            'published' => $entry->date(true),
+            'updated' => $entry->date(true),
+            'alternate' => array(array(
+                'href' => $entry->link(),
+                'type' => 'text/html'
+            )),
+            'content' => array(
+                'content' => $entry->content()
+            ),
+            'origin' => array(
+                'streamId' => $feed->id(),
+                'title' => $feed->name(),
+                'htmlUrl' => $feed->website(),
+                'feedUrl' => $feed->url()
+            )
+        );
+    }
+
+    $options = 0;
+    if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
+        $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
+    }
+
+    echo json_encode($articles, $options);
+?>

+ 28 - 0
app/views/helpers/export/opml.phtml

@@ -0,0 +1,28 @@
+<?php
+
+$opml_array = array(
+	'head' => array(
+		'title' => Minz_Configuration::title(),
+		'dateCreated' => date('D, d M Y H:i:s')
+	),
+	'body' => array()
+);
+
+foreach ($this->categories as $key => $cat) {
+	$opml_array['body'][$key] = array(
+		'text' => $cat['name'],
+		'@outlines' => array()
+	);
+
+	foreach ($cat['feeds'] as $feed) {
+		$opml_array['body'][$key]['@outlines'][] = array(
+			'text' => htmlspecialchars_decode($feed->name()),
+			'type' => 'rss',
+			'xmlUrl' => $feed->url(),
+			'htmlUrl' => $feed->website(),
+			'description' => $feed->description()
+		);
+	}
+}
+
+echo libopml_render($opml_array);

+ 14 - 3
app/views/helpers/javascript_vars.phtml

@@ -5,12 +5,14 @@ echo '"use strict";', "\n";
 $mark = $this->conf->mark_when;
 echo 'var ',
 	'hide_posts=', ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'false' : 'true',
+	',display_order="', Minz_Request::param('order', $this->conf->sort_order), '"',
 	',auto_mark_article=', $mark['article'] ? 'true' : 'false',
 	',auto_mark_site=', $mark['site'] ? 'true' : 'false',
 	',auto_mark_scroll=', $mark['scroll'] ? 'true' : 'false',
 	',auto_load_more=', $this->conf->auto_load_more ? 'true' : 'false',
 	',full_lazyload=', $this->conf->lazyload && ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'true' : 'false',
-	',does_lazyload=', $this->conf->lazyload ? 'true' : 'false';
+	',does_lazyload=', $this->conf->lazyload ? 'true' : 'false',
+	',sticky_post=', $this->conf->sticky_post ? 'true' : 'false';
 
 $s = $this->conf->shortcuts;
 echo ',shortcuts={',
@@ -19,9 +21,12 @@ echo ',shortcuts={',
 	'go_website:"', $s['go_website'], '",',
 	'prev_entry:"', $s['prev_entry'], '",',
 	'next_entry:"', $s['next_entry'], '",',
+	'first_entry:"', $s['first_entry'], '",',
+	'last_entry:"', $s['last_entry'], '",',
 	'collapse_entry:"', $s['collapse_entry'], '",',
 	'load_more:"', $s['load_more'], '",',
-	'auto_share:"', $s['auto_share'], '"',
+	'auto_share:"', $s['auto_share'], '",',
+	'focus_search:"', $s['focus_search'], '"',
 "},\n";
 
 if (Minz_Request::param ('output') === 'global') {
@@ -30,7 +35,13 @@ if (Minz_Request::param ('output') === 'global') {
 
 $authType = Minz_Configuration::authType();
 if ($authType === 'persona') {
-	echo 'current_user_mail="' . Minz_Session::param ('mail', '') . '",';
+	// If user is disconnected, current_user_mail MUST be null
+	$mail = Minz_Session::param ('mail', false);
+	if ($mail) {
+		echo 'current_user_mail="' . $mail . '",';
+	} else {
+		echo 'current_user_mail=null,';
+	}
 }
 
 echo 'authType="', $authType, '",',

+ 15 - 66
app/views/helpers/view/normal_view.phtml

@@ -8,19 +8,10 @@ if (!empty($this->entries)) {
 	$display_yesterday = true;
 	$display_others = true;
 	if ($this->loginOk) {
-		$shaarli = $this->conf->sharing ('shaarli');
-		$wallabag = $this->conf->sharing ('wallabag');
-		$diaspora = $this->conf->sharing ('diaspora');
+		$sharing = $this->conf->sharing;
 	} else {
-		$shaarli = '';
-		$wallabag = '';
-		$diaspora = '';
+		$sharing = array();
 	}
-	$twitter = $this->conf->sharing ('twitter');
-	$google_plus = $this->conf->sharing ('g+');
-	$facebook = $this->conf->sharing ('facebook');
-	$email = $this->conf->sharing ('email');
-	$print = $this->conf->sharing ('print');
 	$hidePosts = !$this->conf->display_posts;
 	$lazyload = $this->conf->lazyload;
 	$topline_read = $this->conf->topline_read;
@@ -29,17 +20,17 @@ if (!empty($this->entries)) {
 	$topline_link = $this->conf->topline_link;
 	$bottomline_read = $this->conf->bottomline_read;
 	$bottomline_favorite = $this->conf->bottomline_favorite;
-	$bottomline_sharing = $this->conf->bottomline_sharing && (
-		$shaarli || $wallabag || $diaspora || $twitter ||
-		$google_plus || $facebook || $email || $print);
+	$bottomline_sharing = $this->conf->bottomline_sharing && (count($sharing));
 	$bottomline_tags = $this->conf->bottomline_tags;
 	$bottomline_date = $this->conf->bottomline_date;
 	$bottomline_link = $this->conf->bottomline_link;
+
+	$content_width = $this->conf->content_width;
 ?>
 
 <div id="stream" class="normal<?php echo $hidePosts ? ' hide_posts' : ''; ?>"><?php
 	?><div id="new-article">
-		<a href="<?php echo _url('index', 'index'); ?>"><?php echo Minz_Translate::t ('new_article'); ?></a>
+		<a href="<?php echo Minz_Url::display ($this->url); ?>"><?php echo Minz_Translate::t ('new_article'); ?></a>
 	</div><?php
 	foreach ($this->entries as $item) {
 		if ($display_today && $item->isDay (FreshRSS_Days::TODAY, $this->today)) {
@@ -98,8 +89,8 @@ if (!empty($this->entries)) {
 		</ul>
 
 		<div class="flux_content">
-			<div class="content">
-				<h1 class="title"><?php echo $item->title (); ?></h1>
+			<div class="content <?php echo $content_width; ?>">
+				<h1 class="title"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo $item->title (); ?></a></h1>
 				<?php
 					$author = $item->author ();
 					echo $author != '' ? '<div class="author">' . Minz_Translate::t ('by_author', $author) . '</div>' : '';
@@ -146,55 +137,13 @@ if (!empty($this->entries)) {
 
 						<ul class="dropdown-menu">
 							<li class="dropdown-close"><a href="#close">❌</a></li>
-							<?php if ($shaarli) { ?>
-							<li class="item">
-								<a target="_blank" href="<?php echo $shaarli . '?post=' . $link . '&amp;title=' . $title . '&amp;source=FreshRSS'; ?>">
-									<?php echo Minz_Translate::t ('shaarli'); ?>
-								</a>
-							</li>
-							<?php } if ($wallabag) { ?>
-							<li class="item">
-								<a target="_blank" href="<?php echo $wallabag . '?action=add&amp;url=' . base64_encode (urldecode($link)); ?>">
-									<?php echo Minz_Translate::t ('wallabag'); ?>
-								</a>
-							</li>
-							<?php } if ($diaspora) { ?>
-							<li class="item">
-								<a target="_blank" href="<?php echo $diaspora . '/bookmarklet?url=' . $link . '&amp;title=' . $title; ?>">
-									<?php echo Minz_Translate::t ('diaspora'); ?>
-								</a>
-							</li>
-							<?php } if ($twitter) { ?>
-							<li class="item">
-								<a target="_blank" href="https://twitter.com/share?url=<?php echo $link; ?>&amp;text=<?php echo $title; ?>">
-									<?php echo Minz_Translate::t ('twitter'); ?>
-								</a>
-							</li>
-							<?php } if ($google_plus) { ?>
-							<li class="item">
-								<a target="_blank" href="https://plus.google.com/share?url=<?php echo $link; ?>">
-									<?php echo Minz_Translate::t ('g+'); ?>
-								</a>
-							</li>
-							<?php } if ($facebook) { ?>
-							<li class="item">
-								<a target="_blank" href="https://www.facebook.com/sharer.php?u=<?php echo $link; ?>&amp;t=<?php echo $title; ?>">
-									<?php echo Minz_Translate::t ('facebook'); ?>
-								</a>
-							</li>
-							<?php } if ($email) { ?>
-							<li class="item">
-								<a href="mailto:?subject=<?php echo urldecode($title); ?>&amp;body=<?php echo $link; ?>">
-									<?php echo Minz_Translate::t ('by_email'); ?>
-								</a>
-							</li>
-							<?php } if ($print) { ?>
-							<li class="item">
-								<a href="#" class="print-article">
-									<?php echo Minz_Translate::t ('print'); ?>
-								</a>
-							</li>
-							<?php } ?>
+							<?php foreach ($sharing as $share) :?>
+								<li class="item share">
+									<a target="_blank" href="<?php echo FreshRSS_Share::generateUrl($this->conf->shares, $share, $item->link(), $item->title() . ' . ' . $feed->name())?>">
+										<?php echo Minz_Translate::t ($share['name']);?>
+									</a>
+								</li>
+							<?php endforeach;?>
 						</ul>
 					</div>
 					<?php } ?>

+ 2 - 1
app/views/helpers/view/reader_view.phtml

@@ -3,6 +3,7 @@ $this->partial ('nav_menu');
 
 if (!empty($this->entries)) {
 	$lazyload = $this->conf->lazyload;
+	$content_width = $this->conf->content_width;
 ?>
 
 <div id="stream" class="reader">
@@ -10,7 +11,7 @@ if (!empty($this->entries)) {
 
 	<div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>">
 		<div class="flux_content">
-			<div class="content">
+			<div class="content <?php echo $content_width; ?>">
 				<?php
 					$feed = FreshRSS_CategoryDAO::findFeed($this->cat_aside, $item->feed ());	//We most likely already have the feed object in cache
 					if (empty($feed)) $feed = $item->feed (true);

+ 1 - 1
app/views/helpers/view/rss_view.phtml

@@ -6,7 +6,7 @@
 		<description><?php echo Minz_Translate::t ('rss_feeds_of', $this->rss_title); ?></description>
 		<pubDate><?php echo date('D, d M Y H:i:s O'); ?></pubDate>
 		<lastBuildDate><?php echo gmdate('D, d M Y H:i:s'); ?> GMT</lastBuildDate>
-		<atom:link href="<?php echo Minz_Url::display ($this->rss_url, 'html', true); ?>" rel="self" type="application/rss+xml" />
+		<atom:link href="<?php echo Minz_Url::display ($this->url, 'html', true); ?>" rel="self" type="application/rss+xml" />
 <?php
 foreach ($this->entries as $item) {
 ?>

+ 52 - 0
app/views/importExport/index.phtml

@@ -0,0 +1,52 @@
+<?php $this->partial ('aside_feed'); ?>
+
+<div class="post ">
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
+
+	<form method="post" action="<?php echo _url('importExport', 'import'); ?>" enctype="multipart/form-data">
+		<legend><?php echo Minz_Translate::t ('import'); ?></legend>
+		<div class="form-group">
+			<label class="group-name" for="file"><?php echo Minz_Translate::t ('file_to_import'); ?></label>
+			<div class="group-controls">
+				<input type="file" name="file" id="file" />
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('import'); ?></button>
+			</div>
+		</div>
+	</form>
+
+	<?php if (count($this->feeds) > 0) { ?>
+	<form method="post" action="<?php echo _url('importExport', 'export'); ?>">
+		<legend><?php echo Minz_Translate::t ('export'); ?></legend>
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="export_opml">
+					<input type="checkbox" name="export_opml" id="export_opml" value="1" checked="checked" />
+					<?php echo Minz_Translate::t ('export_opml'); ?>
+				</label>
+
+				<label class="checkbox" for="export_starred">
+					<input type="checkbox" name="export_starred" id="export_starred" value="1" checked="checked" />
+					<?php echo Minz_Translate::t ('export_starred'); ?>
+				</label>
+
+				<select name="export_feeds[]" size="<?php echo min(10, count($this->feeds)); ?>" multiple="multiple">
+					<?php foreach ($this->feeds as $feed) { ?>
+					<option value="<?php echo $feed->id(); ?>"><?php echo $feed->name(); ?></option>
+					<?php } ?>
+				</select>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('export'); ?></button>
+			</div>
+		</div>
+	</form>
+	<?php } ?>
+</div>

+ 27 - 29
app/views/index/formLogin.phtml

@@ -1,34 +1,32 @@
 <div class="prompt">
-<?php
-if (Minz_Configuration::canLogIn()) {
-	?><h1><?php echo Minz_Translate::t('login'); ?></h1><?php
-	switch (Minz_Configuration::authType()) {
+	<h1><?php echo Minz_Translate::t('login'); ?></h1><?php
 
-		case 'form':
-		?><form id="loginForm" method="post" action="<?php echo _url('index', 'formLogin'); ?>">
-			<p>
-				<label for="username"><?php echo Minz_Translate::t('username'); ?></label>
-				<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
-			</p><p>
-				<label for="passwordPlain"><?php echo Minz_Translate::t('password'); ?></label>
-					<input type="password" id="passwordPlain" required="required" />
-					<input type="hidden" id="challenge" name="challenge" /><br />
-					<noscript><strong><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></strong></noscript>
-			</p><p>
-				<button id="loginButton" type="submit" class="btn btn-important"><?php echo Minz_Translate::t('login'); ?></button>
-			</p>
-		</form><?php
-			break;
+	switch (Minz_Configuration::authType()) {
+	case 'form':
+	?><form id="loginForm" method="post" action="<?php echo _url('index', 'formLogin'); ?>">
+		<div>
+			<label for="username"><?php echo Minz_Translate::t('username'); ?></label>
+			<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
+		</div>
+		<div>
+			<label for="passwordPlain"><?php echo Minz_Translate::t('password'); ?></label>
+				<input type="password" id="passwordPlain" required="required" />
+				<input type="hidden" id="challenge" name="challenge" /><br />
+				<noscript><strong><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></strong></noscript>
+		</div>
+		<div>
+			<button id="loginButton" type="submit" class="btn btn-important"><?php echo Minz_Translate::t('login'); ?></button>
+		</div>
+	</form><?php
+		break;
 
-		case 'persona':
-			?><p><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t('login'); ?></a></p><?php
-			break;
-	}
-} else {
-	?><h1>FreshRSS</h1>
-	<p><?php echo Minz_Translate::t('forbidden_access', Minz_Configuration::authType()); ?></p><?php
-}
-?>
+	case 'persona':
+		?><p>
+			<?php echo FreshRSS_Themes::icon('login'); ?>
+			<a class="signin" href="#"><?php echo Minz_Translate::t('login_with_persona'); ?></a>
+		</p><?php
+		break;
+	} ?>
 
-<p><a href="<?php echo _url('index', 'about'); ?>"><?php echo Minz_Translate::t('about_freshrss'); ?></a></p>
+	<p><a href="<?php echo _url('index', 'about'); ?>"><?php echo Minz_Translate::t('about_freshrss'); ?></a></p>
 </div>

+ 6 - 11
app/views/index/index.phtml

@@ -5,26 +5,21 @@ $output = Minz_Request::param ('output', 'normal');
 if ($this->loginOk || Minz_Configuration::allowAnonymous()) {
 	if ($output === 'normal') {
 		$this->renderHelper ('view/normal_view');
-	} elseif ($output === 'rss') {
-		$this->renderHelper ('view/rss_view');
 	} elseif ($output === 'reader') {
 		$this->renderHelper ('view/reader_view');
 	} elseif ($output === 'global') {
 		$this->renderHelper ('view/global_view');
+	} elseif ($output === 'rss') {
+		$this->renderHelper ('view/rss_view');
 	} else {
 		Minz_Request::_param ('output', 'normal');
 		$output = 'normal';
 		$this->renderHelper ('view/normal_view');
 	}
 } elseif ($output === 'rss') {
-	$token = $this->conf->token;
-	$token_param = Minz_Request::param ('token', '');
-	$token_is_ok = ($token != '' && $token == $token_param);
-	if ($token_is_ok) {
-		$this->renderHelper ('view/rss_view');
-	} else {
-		Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'), true);
-	}
+	// token has already been checked in the controller so we can show the view
+	$this->renderHelper ('view/rss_view');
 } else {
-	Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'), true);
+	// Normally, it should not happen, but log it anyway
+	Minz_Log::record ('Something is wrong in ' . __FILE__ . ' line ' . __LINE__, Minz_Log::ERROR);
 }

+ 34 - 21
app/views/javascript/actualize.phtml

@@ -1,14 +1,17 @@
 "use strict";
-var feeds = [];
-<?php foreach ($this->feeds as $feed) { ?>
-feeds.push("<?php echo Minz_Url::display (array ('c' => 'feed', 'a' => 'actualize', 'params' => array ('id' => $feed->id (), 'ajax' => '1')), 'php'); ?>");
-<?php } ?>
+var feeds = [<?php
+	foreach ($this->feeds as $feed) {
+		echo "'", Minz_Url::display(array('c' => 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'), "',\n";
+	}
+	?>],
+	feed_processed = 0,
+	feed_count = feeds.length;
 
 function initProgressBar(init) {
 	if (init) {
-		$("body").after("\<div id=\"actualizeProgress\" class=\"actualizeProgress\">\
-			<?php echo Minz_Translate::t ('refresh'); ?> <span class=\"progress\">0 / " + feeds.length + "</span><br />\
-			<progress id=\"actualizeProgressBar\" value=\"0\" max=\"" + feeds.length + "\"></progress>\
+		$("body").after("\<div id=\"actualizeProgress\" class=\"notification good\">\
+			<?php echo Minz_Translate::t ('refresh'); ?> <span class=\"progress\">0 / " + feed_count + "</span><br />\
+			<progress id=\"actualizeProgressBar\" value=\"0\" max=\"" + feed_count + "\"></progress>\
 		</div>");
 	} else {
 		window.location.reload();
@@ -16,27 +19,37 @@ function initProgressBar(init) {
 }
 function updateProgressBar(i) {
 	$("#actualizeProgressBar").val(i);
-	$("#actualizeProgress .progress").html(i + " / " + feeds.length);
+	$("#actualizeProgress .progress").html(i + " / " + feed_count);
 }
 
 function updateFeeds() {
-	if (feeds.length === 0) {
+	if (feed_count === 0) {
+		openNotification("<?php echo Minz_Translate::t ('no_feed_to_refresh'); ?>", "good");
 		return;
 	}
 	initProgressBar(true);
 
-	var i = 0;
-	for (var f in feeds) {
-		$.ajax({
-			type: 'POST',
-			url: feeds[f],
-		}).done(function (data) {
-			i++;
-			updateProgressBar(i);
+	for (var i = 0; i < 10; i++) {
+		updateFeed();
+	}
+}
 
-			if (i === feeds.length) {
-				initProgressBar(false);
-			}
-		});
+function updateFeed() {
+	var feed = feeds.pop();
+	if (feed == undefined) {
+		return;
 	}
+	$.ajax({
+		type: 'POST',
+		url: feed,
+	}).complete(function (data) {
+		feed_processed++;
+		updateProgressBar(feed_processed);
+
+		if (feed_processed === feed_count) {
+			initProgressBar(false);
+		} else {
+			updateFeed();
+		}
+	});
 }

+ 6 - 1
constants.php

@@ -1,7 +1,10 @@
 <?php
-define('FRESHRSS_VERSION', '0.7');
+define('FRESHRSS_VERSION', '0.8-dev');
 define('FRESHRSS_WEBSITE', 'http://freshrss.org');
 
+// PHP text output compression http://php.net/ob_gzhandler (better to do it at Web server level)
+define('PHP_COMPRESSION', false);
+
 // Constantes de chemins
 define('FRESHRSS_PATH', dirname(__FILE__));
 
@@ -15,3 +18,5 @@ define('FRESHRSS_PATH', dirname(__FILE__));
 
 	define('LIB_PATH', FRESHRSS_PATH . '/lib');
 		define('APP_PATH', FRESHRSS_PATH . '/app');
+
+define('TMP_PATH', sys_get_temp_dir());

+ 75 - 0
data/shares.php

@@ -0,0 +1,75 @@
+<?php
+
+/*
+ * This is a configuration file. You shouldn't modify it unless you know what
+ * you are doing. If you want to add a share type, this is where you need to do
+ * it.
+ * 
+ * For each share there is different configuration options. Here is the description
+ * of those options:
+ *   - url is a mandatory option. It is a string representing the share URL. It
+ *     supports 3 different placeholders for custom data. The ~URL~ placeholder
+ *     represents the URL of the system used to share, it is configured by the
+ *     user. The ~LINK~ placeholder represents the link of the shared article.
+ *     The ~TITLE~ placeholder represents the title of the shared article.
+ *   - transform is an array of transformation to apply on links and titles
+ *   - help is a URL to a help page
+ */
+
+return array(
+	'shaarli' => array(
+		'url' => '~URL~?post=~LINK~&amp;title=~TITLE~&amp;source=FreshRSS',
+		'transform' => array('urlencode'),
+		'help' => 'http://sebsauvage.net/wiki/doku.php?id=php:shaarli',
+		'form' => 'advanced',
+	),
+	'blogotext' => array(
+		'url' => '~URL~/admin/links.php?url=~LINK~',
+		'transform' => array(),
+		'help' => 'http://lehollandaisvolant.net/blogotext/fr/',
+		'form' => 'advanced',
+	),
+	'wallabag' => array(
+		'url' => '~URL~?action=add&amp;url=~LINK~',
+		'transform' => array(
+			'link' => array('base64_encode'),
+			'title' => array(),
+		),
+		'help' => 'http://www.wallabag.org/',
+		'form' => 'advanced',
+	),
+	'diaspora' => array(
+		'url' => '~URL~/bookmarklet?url=~LINK~&amp;title=~TITLE~',
+		'transform' => array('urlencode'),
+		'help' => 'https://diasporafoundation.org/',
+		'form' => 'advanced',
+	),
+	'twitter' => array(
+		'url' => 'https://twitter.com/share?url=~LINK~&amp;text=~TITLE~',
+		'transform' => array('urlencode'),
+		'form' => 'simple',
+	),
+	'g+' => array(
+		'url' => 'https://plus.google.com/share?url=~LINK~',
+		'transform' => array('urlencode'),
+		'form' => 'simple',
+	),
+	'facebook' => array(
+		'url' => 'https://www.facebook.com/sharer.php?u=~LINK~&amp;t=~TITLE~',
+		'transform' => array('urlencode'),
+		'form' => 'simple',
+	),
+	'email' => array(
+		'url' => 'mailto:?subject=~TITLE~&amp;body=~LINK~',
+		'transform' => array(
+			'link' => array('urlencode'),
+			'title' => array(),
+		),
+		'form' => 'simple',
+	),
+	'print' => array(
+		'url' => '#',
+		'transform' => array(),
+		'form' => 'simple',
+	),
+);

+ 1 - 5
lib/Minz/ActionController.php

@@ -8,16 +8,12 @@
  * La classe ActionController représente le contrôleur de l'application
  */
 class Minz_ActionController {
-	protected $router;
 	protected $view;
 
 	/**
 	 * Constructeur
-	 * @param $controller nom du controller
-	 * @param $action nom de l'action à lancer
 	 */
-	public function __construct ($router) {
-		$this->router = $router;
+	public function __construct () {
 		$this->view = new Minz_View ();
 		$this->view->attributeParams ();
 	}

+ 0 - 116
lib/Minz/Cache.php

@@ -1,116 +0,0 @@
-<?php
-/** 
- * MINZ - Copyright 2011 Marien Fressinaud
- * Sous licence AGPL3 <http://www.gnu.org/licenses/>
-*/
-
-/**
- * La classe Cache permet de gérer facilement les pages en cache
- */
-class Minz_Cache {
-	/**
-	 * $expire timestamp auquel expire le cache de $url
-	 */
-	private $expire = 0;
-
-	/**
-	 * $file est le nom du fichier de cache
-	 */
-	private $file = '';
-
-	/**
-	 * $enabled permet de déterminer si le cache est activé
-	 */
-	private static $enabled = true;
-
-	/**
-	 * Constructeur
-	 */
-	public function __construct () {
-		$this->_fileName ();
-		$this->_expire ();
-	}
-
-	/**
-	 * Setteurs
-	 */
-	public function _fileName () {
-		$file = md5 (Minz_Request::getURI ());
-
-		$this->file = CACHE_PATH . '/'.$file;
-	}
-
-	public function _expire () {
-		if ($this->exist ()) {
-			$this->expire = filemtime ($this->file)
-			              + Minz_Configuration::delayCache ();
-		}
-	}
-
-	/**
-	 * Permet de savoir si le cache est activé
-	 * @return true si activé, false sinon
-	 */
-	public static function isEnabled () {
-		return Minz_Configuration::cacheEnabled () && self::$enabled;
-	}
-
-	/**
-	 * Active / désactive le cache
-	 */
-	public static function switchOn () {
-		self::$enabled = true;
-	}
-	public static function switchOff () {
-		self::$enabled = false;
-	}
-
-	/**
-	 * Détermine si le cache de $url a expiré ou non
-	 * @return true si il a expiré, false sinon
-	 */
-	public function expired () {
-		return time () > $this->expire;
-	}
-
-	/**
-	 * Affiche le contenu du cache
-	 * @print le code html du cache
-	 */
-	public function render () {
-		if ($this->exist ()) {
-			include ($this->file);
-		}
-	}
-
-	/**
-	 * Enregistre $html en cache
-	 * @param $html le html à mettre en cache
-	 */
-	public function cache ($html) {
-		file_put_contents ($this->file, $html);
-	}
-
-	/**
-	 * Permet de savoir si le cache existe
-	 * @return true si il existe, false sinon
-	 */
-	public function exist () {
-		return file_exists ($this->file);
-	}
-
-	/**
-	 * Nettoie le cache en supprimant tous les fichiers
-	 */
-	public static function clean () {
-		$files = opendir (CACHE_PATH);
-
-		while ($fic = readdir ($files)) {
-			if ($fic != '.' && $fic != '..') {
-				unlink (CACHE_PATH.'/'.$fic);
-			}
-		}
-
-		closedir ($files);
-	}
-}

+ 47 - 32
lib/Minz/Configuration.php

@@ -30,12 +30,9 @@ class Minz_Configuration {
 	 * définition des variables de configuration
 	 * $salt une chaîne de caractères aléatoires (obligatoire)
 	 * $environment gère le niveau d'affichage pour log et erreurs
-	 * $use_url_rewriting indique si on utilise l'url_rewriting
 	 * $base_url le chemin de base pour accéder à l'application
 	 * $title le nom de l'application
 	 * $language la langue par défaut de l'application
-	 * $cacheEnabled permet de savoir si le cache doit être activé
-	 * $delayCache la limite de cache
 	 * $db paramètres pour la base de données (tableau)
 	 *     - host le serveur de la base
 	 *     - user nom d'utilisateur
@@ -45,14 +42,14 @@ class Minz_Configuration {
 	private static $salt = '';
 	private static $environment = Minz_Configuration::PRODUCTION;
 	private static $base_url = '';
-	private static $use_url_rewriting = false;
 	private static $title = '';
 	private static $language = 'en';
-	private static $cache_enabled = false;
-	private static $delay_cache = 3600;
 	private static $default_user = '';
 	private static $allow_anonymous = false;
+	private static $allow_anonymous_refresh = false;
 	private static $auth_type = 'none';
+	private static $api_enabled = false;
+	private static $unsafe_autologin_enabled = false;
 
 	private static $db = array (
 		'type' => 'mysql',
@@ -91,21 +88,12 @@ class Minz_Configuration {
 	public static function baseUrl () {
 		return self::$base_url;
 	}
-	public static function useUrlRewriting () {
-		return self::$use_url_rewriting;
-	}
 	public static function title () {
 		return self::$title;
 	}
 	public static function language () {
 		return self::$language;
 	}
-	public static function cacheEnabled () {
-		return self::$cache_enabled;
-	}
-	public static function delayCache () {
-		return self::$delay_cache;
-	}
 	public static function dataBase () {
 		return self::$db;
 	}
@@ -118,6 +106,9 @@ class Minz_Configuration {
 	public static function allowAnonymous() {
 		return self::$allow_anonymous;
 	}
+	public static function allowAnonymousRefresh() {
+		return self::$allow_anonymous_refresh;
+	}
 	public static function authType() {
 		return self::$auth_type;
 	}
@@ -127,10 +118,19 @@ class Minz_Configuration {
 	public static function canLogIn() {
 		return self::$auth_type === 'form' || self::$auth_type === 'persona';
 	}
+	public static function apiEnabled() {
+		return self::$api_enabled;
+	}
+	public static function unsafeAutologinEnabled() {
+		return self::$unsafe_autologin_enabled;
+	}
 
 	public static function _allowAnonymous($allow = false) {
 		self::$allow_anonymous = ((bool)$allow) && self::canLogIn();
 	}
+	public static function _allowAnonymousRefresh($allow = false) {
+		self::$allow_anonymous_refresh = ((bool)$allow) && self::allowAnonymous();
+	}
 	public static function _authType($value) {
 		$value = strtolower($value);
 		switch ($value) {
@@ -144,6 +144,13 @@ class Minz_Configuration {
 		self::_allowAnonymous(self::$allow_anonymous);
 	}
 
+	public static function _enableApi($value = false) {
+		self::$api_enabled = (bool)$value;
+	}
+	public static function _enableAutologin($value = false) {
+		self::$unsafe_autologin_enabled = (bool)$value;
+	}
+
 	/**
 	 * Initialise les variables de configuration
 	 * @exception Minz_FileNotExistException si le CONF_PATH_NAME n'existe pas
@@ -164,13 +171,15 @@ class Minz_Configuration {
 		$ini_array = array(
 			'general' => array(
 				'environment' => self::environment(true),
-				'use_url_rewriting' => self::$use_url_rewriting,
 				'salt' => self::$salt,
 				'base_url' => self::$base_url,
 				'title' => self::$title,
 				'default_user' => self::$default_user,
 				'allow_anonymous' => self::$allow_anonymous,
+				'allow_anonymous_refresh' => self::$allow_anonymous_refresh,
 				'auth_type' => self::$auth_type,
+				'api_enabled' => self::$api_enabled,
+				'unsafe_autologin_enabled' => self::$unsafe_autologin_enabled,
 			),
 			'db' => self::$db,
 		);
@@ -247,9 +256,6 @@ class Minz_Configuration {
 		if (isset ($general['base_url'])) {
 			self::$base_url = $general['base_url'];
 		}
-		if (isset ($general['use_url_rewriting'])) {
-			self::$use_url_rewriting = $general['use_url_rewriting'];
-		}
 
 		if (isset ($general['title'])) {
 			self::$title = $general['title'];
@@ -257,18 +263,6 @@ class Minz_Configuration {
 		if (isset ($general['language'])) {
 			self::$language = $general['language'];
 		}
-		if (isset ($general['cache_enabled'])) {
-			self::$cache_enabled = $general['cache_enabled'];
-			if (CACHE_PATH === false && self::$cache_enabled) {
-				throw new FileNotExistException (
-					'CACHE_PATH',
-					Minz_Exception::ERROR
-				);
-			}
-		}
-		if (isset ($general['delay_cache'])) {
-			self::$delay_cache = inval($general['delay_cache']);
-		}
 		if (isset ($general['default_user'])) {
 			self::$default_user = $general['default_user'];
 		}
@@ -276,7 +270,28 @@ class Minz_Configuration {
 			self::_authType($general['auth_type']);
 		}
 		if (isset ($general['allow_anonymous'])) {
-			self::$allow_anonymous = ((bool)($general['allow_anonymous'])) && ($general['allow_anonymous'] !== 'no');
+			self::$allow_anonymous = (
+				((bool)($general['allow_anonymous'])) &&
+				($general['allow_anonymous'] !== 'no')
+			);
+		}
+		if (isset ($general['allow_anonymous_refresh'])) {
+			self::$allow_anonymous_refresh = (
+				((bool)($general['allow_anonymous_refresh'])) &&
+				($general['allow_anonymous_refresh'] !== 'no')
+			);
+		}
+		if (isset ($general['api_enabled'])) {
+			self::$api_enabled = (
+				((bool)($general['api_enabled'])) &&
+				($general['api_enabled'] !== 'no')
+			);
+		}
+		if (isset ($general['unsafe_autologin_enabled'])) {
+			self::$unsafe_autologin_enabled = (
+				((bool)($general['unsafe_autologin_enabled'])) &&
+				($general['unsafe_autologin_enabled'] !== 'no')
+			);
 		}
 
 		// Base de données

+ 38 - 70
lib/Minz/Dispatcher.php

@@ -14,85 +14,55 @@ class Minz_Dispatcher {
 
 	/* singleton */
 	private static $instance = null;
+	private static $needsReset;
 
-	private $router;
 	private $controller;
 
 	/**
 	 * Récupère l'instance du Dispatcher
 	 */
-	public static function getInstance ($router) {
+	public static function getInstance () {
 		if (self::$instance === null) {
-			self::$instance = new Minz_Dispatcher ($router);
+			self::$instance = new Minz_Dispatcher ();
 		}
 		return self::$instance;
 	}
 
-	/**
-	 * Constructeur
-	 */
-	private function __construct ($router) {
-		$this->router = $router;
-	}
-
 	/**
 	 * Lance le controller indiqué dans Request
 	 * Remplit le body de Response à partir de la Vue
 	 * @exception Minz_Exception
 	 */
-	public function run ($ob = true) {
-		$cache = new Minz_Cache();
-		// Le ob_start est dupliqué : sans ça il y a un bug sous Firefox
-		// ici on l'appelle avec 'ob_gzhandler', après sans.
-		// Vraisemblablement la compression fonctionne mais c'est sale
-		// J'ignore les effets de bord :(
-		if ($ob) {
-			ob_start ('ob_gzhandler');
-		}
+	public function run () {
+		do {
+			self::$needsReset = false;
 
-		if (Minz_Cache::isEnabled () && !$cache->expired ()) {
-			if ($ob) {
-				ob_start ();
-			}
-			$cache->render ();
-			if ($ob) {
-				$text = ob_get_clean();
-			}
-		} else {
-			$text = '';	//TODO: Clean this code
-			while (Minz_Request::$reseted) {
-				Minz_Request::$reseted = false;
-
-				try {
-					$this->createController ('FreshRSS_' . Minz_Request::controllerName () . '_Controller');
-					$this->controller->init ();
-					$this->controller->firstAction ();
+			try {
+				$this->createController ('FreshRSS_' . Minz_Request::controllerName () . '_Controller');
+				$this->controller->init ();
+				$this->controller->firstAction ();
+				if (!self::$needsReset) {
 					$this->launchAction (
 						Minz_Request::actionName ()
 						. 'Action'
 					);
-					$this->controller->lastAction ();
-
-					if (!Minz_Request::$reseted) {
-						if ($ob) {
-							ob_start ();
-						}
-						$this->controller->view ()->build ();
-						if ($ob) {
-							$text = ob_get_clean();
-						}
-					}
-				} catch (Minz_Exception $e) {
-					throw $e;
 				}
-			}
+				$this->controller->lastAction ();
 
-			if (Minz_Cache::isEnabled ()) {
-				$cache->cache ($text);
+				if (!self::$needsReset) {
+					$this->controller->view ()->build ();
+				}
+			} catch (Minz_Exception $e) {
+				throw $e;
 			}
-		}
+		} while (self::$needsReset);
+	}
 
-		Minz_Response::setBody ($text);
+	/**
+	 * Informe le contrôleur qu'il doit recommancer car la requête a été modifiée
+	 */
+	public static function reset() {
+		self::$needsReset = true;
 	}
 
 	/**
@@ -112,7 +82,7 @@ class Minz_Dispatcher {
 				Minz_Exception::ERROR
 			);
 		}
-		$this->controller = new $controller_name ($this->router);
+		$this->controller = new $controller_name ();
 
 		if (! ($this->controller instanceof Minz_ActionController)) {
 			throw new Minz_ControllerNotActionControllerException (
@@ -129,21 +99,19 @@ class Minz_Dispatcher {
 	 *  le controller
 	 */
 	private function launchAction ($action_name) {
-		if (!Minz_Request::$reseted) {
-			if (!is_callable (array (
-				$this->controller,
-				$action_name
-			))) {
-				throw new Minz_ActionException (
-					get_class ($this->controller),
-					$action_name,
-					Minz_Exception::ERROR
-				);
-			}
-			call_user_func (array (
-				$this->controller,
-				$action_name
-			));
+		if (!is_callable (array (
+			$this->controller,
+			$action_name
+		))) {
+			throw new Minz_ActionException (
+				get_class ($this->controller),
+				$action_name,
+				Minz_Exception::ERROR
+			);
 		}
+		call_user_func (array (
+			$this->controller,
+			$action_name
+		));
 	}
 }

+ 24 - 8
lib/Minz/Error.php

@@ -23,13 +23,32 @@ class Minz_Error {
 		$logs = self::processLogs ($logs);
 		$error_filename = APP_PATH . '/Controllers/errorController.php';
 
+		switch ($code) {
+			case 200 :
+				header('HTTP/1.1 200 OK');
+				break;
+			case 403 :
+				header('HTTP/1.1 403 Forbidden');
+				break;
+			case 404 :
+				header('HTTP/1.1 404 Not Found');
+				break;
+			case 500 :
+				header('HTTP/1.1 500 Internal Server Error');
+				break;
+			case 503 :
+				header('HTTP/1.1 503 Service Unavailable');
+				break;
+			default :
+				header('HTTP/1.1 500 Internal Server Error');
+		}
+
 		if (file_exists ($error_filename)) {
 			$params = array (
 				'code' => $code,
 				'logs' => $logs
 			);
 
-			Minz_Response::setHeader ($code);
 			if ($redirect) {
 				Minz_Request::forward (array (
 					'c' => 'error'
@@ -41,19 +60,16 @@ class Minz_Error {
 				), false);
 			}
 		} else {
-			$text = '<h1>An error occured</h1>'."\n";
+			echo '<h1>An error occured</h1>' . "\n";
 
 			if (!empty ($logs)) {
-				$text .= '<ul>'."\n";
+				echo '<ul>' . "\n";
 				foreach ($logs as $log) {
-					$text .= '<li>' . $log . '</li>'."\n";
+					echo '<li>' . $log . '</li>' . "\n";
 				}
-				$text .= '</ul>'."\n";
+				echo '</ul>' . "\n";
 			}
 
-			Minz_Response::setHeader ($code);
-			Minz_Response::setBody ($text);
-			Minz_Response::send ();
 			exit ();
 		}
 	}

+ 33 - 26
lib/Minz/FrontController.php

@@ -24,13 +24,10 @@
  */
 class Minz_FrontController {
 	protected $dispatcher;
-	protected $router;
-
-	private $useOb = true;
 
 	/**
 	 * Constructeur
-	 * Initialise le router et le dispatcher
+	 * Initialise le dispatcher, met à jour la Request
 	 */
 	public function __construct () {
 		if (LOG_PATH === false) {
@@ -42,29 +39,50 @@ class Minz_FrontController {
 
 			Minz_Request::init ();
 
-			$this->router = new Minz_Router ();
-			$this->router->init ();
-		} catch (Minz_RouteNotFoundException $e) {
-			Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
-			Minz_Error::error (
-				404,
-				array ('error' => array ($e->getMessage ()))
+			$url = $this->buildUrl();
+			$url['params'] = array_merge (
+				$url['params'],
+				Minz_Request::fetchPOST ()
 			);
+			Minz_Request::forward ($url);
 		} catch (Minz_Exception $e) {
 			Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
 			$this->killApp ($e->getMessage ());
 		}
 
-		$this->dispatcher = Minz_Dispatcher::getInstance ($this->router);
+		$this->dispatcher = Minz_Dispatcher::getInstance();
 	}
 
 	/**
-	 * Démarre l'application (lance le dispatcher et renvoie la réponse
+	 * Retourne un tableau représentant l'url passée par la barre d'adresses
+	 * @return tableau représentant l'url
+	 */
+	private function buildUrl() {
+		$url = array ();
+
+		$url['c'] = Minz_Request::fetchGET (
+			'c',
+			Minz_Request::defaultControllerName ()
+		);
+		$url['a'] = Minz_Request::fetchGET (
+			'a',
+			Minz_Request::defaultActionName ()
+		);
+		$url['params'] = Minz_Request::fetchGET ();
+
+		// post-traitement
+		unset ($url['params']['c']);
+		unset ($url['params']['a']);
+
+		return $url;
+	}
+
+	/**
+	 * Démarre l'application (lance le dispatcher et renvoie la réponse)
 	 */
 	public function run () {
 		try {
-			$this->dispatcher->run ($this->useOb);
-			Minz_Response::send ();
+			$this->dispatcher->run();
 		} catch (Minz_Exception $e) {
 			try {
 				Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
@@ -96,15 +114,4 @@ class Minz_FrontController {
 		}
 		exit ('### Application problem ###<br />'."\n".$txt);
 	}
-
-	public function useOb() {
-		return $this->useOb;
-	}
-
-	/**
-	 * Use ob_start('ob_gzhandler') or not.
-	 */
-	public function _useOb($ob) {
-		return $this->useOb = (bool)$ob;
-	}
 }

+ 17 - 0
lib/Minz/Log.php

@@ -80,4 +80,21 @@ class Minz_Log {
 		self::record($msg_get, Minz_Log::DEBUG, $file_name);
 		self::record($msg_post, Minz_Log::DEBUG, $file_name);
 	}
+
+	/**
+	 * Some helpers to Minz_Log::record() method
+	 * Parameters are the same of those of the record() method.
+	 */
+	public static function debug($msg, $file_name = null) {
+		self::record($msg, Minz_Log::DEBUG, $file_name);
+	}
+	public static function notice($msg, $file_name = null) {
+		self::record($msg, Minz_Log::NOTICE, $file_name);
+	}
+	public static function warning($msg, $file_name = null) {
+		self::record($msg, Minz_Log::WARNING, $file_name);
+	}
+	public static function error($msg, $file_name = null) {
+		self::record($msg, Minz_Log::ERROR, $file_name);
+	}
 }

+ 2 - 5
lib/Minz/Request.php

@@ -15,8 +15,6 @@ class Minz_Request {
 	private static $default_controller_name = 'index';
 	private static $default_action_name = 'index';
 
-	public static $reseted = true;
-
 	/**
 	 * Getteurs
 	 */
@@ -137,14 +135,13 @@ class Minz_Request {
 			header ('Location: ' . Minz_Url::display ($url, 'php'));
 			exit ();
 		} else {
-			self::$reseted = true;
-
 			self::_controllerName ($url['c']);
 			self::_actionName ($url['a']);
 			self::_params (array_merge (
 				self::$params,
 				$url['params']
 			));
+			Minz_Dispatcher::reset();
 		}
 	}
 
@@ -199,6 +196,6 @@ class Minz_Request {
 	}
 
 	public static function isPost () {
-		return !empty ($_POST) || !empty ($_FILES);
+		return $_SERVER['REQUEST_METHOD'] === 'POST';
 	}
 }

+ 0 - 60
lib/Minz/Response.php

@@ -1,60 +0,0 @@
-<?php
-/** 
- * MINZ - Copyright 2011 Marien Fressinaud
- * Sous licence AGPL3 <http://www.gnu.org/licenses/>
-*/
-
-/**
- * Response représente la requête http renvoyée à l'utilisateur
- */
-class Minz_Response {
-	private static $header = 'HTTP/1.0 200 OK';
-	private static $body = '';
-	
-	/**
-	 * Mets à jour le body de la Response
-	 * @param $text le texte à incorporer dans le body
-	 */
-	public static function setBody ($text) {
-		self::$body = $text;
-	}
-	
-	/**
-	 * Mets à jour le header de la Response
-	 * @param $code le code HTTP, valeurs possibles
-	 *	- 200 (OK)
-	 *	- 403 (Forbidden)
-	 *	- 404 (Forbidden)
-	 *	- 500 (Forbidden) -> par défaut si $code erroné
-	 *	- 503 (Forbidden)
-	 */
-	public static function setHeader ($code) {
-		switch ($code) {
-		case 200 :
-			self::$header = 'HTTP/1.0 200 OK';
-			break;
-		case 403 :
-			self::$header = 'HTTP/1.0 403 Forbidden';
-			break;
-		case 404 :
-			self::$header = 'HTTP/1.0 404 Not Found';
-			break;
-		case 500 :
-			self::$header = 'HTTP/1.0 500 Internal Server Error';
-			break;
-		case 503 :
-			self::$header = 'HTTP/1.0 503 Service Unavailable';
-			break;
-		default :
-			self::$header = 'HTTP/1.0 500 Internal Server Error';
-		}
-	}
-
-	/**
-	 * Envoie la Response à l'utilisateur
-	 */
-	public static function send () {
-		header (self::$header);
-		echo self::$body;
-	}
-}

+ 0 - 16
lib/Minz/RouteNotFoundException.php

@@ -1,16 +0,0 @@
-<?php
-class Minz_RouteNotFoundException extends Minz_Exception {
-	private $route;
-	
-	public function __construct ($route, $code = self::ERROR) {
-		$this->route = $route;
-		
-		$message = 'Route `' . $route . '` not found';
-		
-		parent::__construct ($message, $code);
-	}
-	
-	public function route () {
-		return $this->route;
-	}
-}

+ 0 - 209
lib/Minz/Router.php

@@ -1,209 +0,0 @@
-<?php
-/** 
- * MINZ - Copyright 2011 Marien Fressinaud
- * Sous licence AGPL3 <http://www.gnu.org/licenses/>
-*/
-
-/**
- * La classe Router gère le routage de l'application
- * Les routes sont définies dans APP_PATH.'/configuration/routes.php'
- */
-class Minz_Router {
-	const ROUTES_PATH_NAME = '/configuration/routes.php';
-
-	private $routes = array ();
-	
-	/**
-	 * Constructeur
-	 * @exception FileNotExistException si ROUTES_PATH_NAME n'existe pas
-	 *            et que l'on utilise l'url rewriting
-	 */
-	public function __construct () {
-		if (Minz_Configuration::useUrlRewriting ()) {
-			if (file_exists (APP_PATH . self::ROUTES_PATH_NAME)) {
-				$routes = include (
-					APP_PATH . self::ROUTES_PATH_NAME
-				);
-		
-				if (!is_array ($routes)) {
-					$routes = array ();
-				}
-				
-				$this->routes = array_map (
-					array ('Url', 'checkUrl'),
-					$routes
-				);
-			} else {
-				throw new Minz_FileNotExistException (
-					self::ROUTES_PATH_NAME,
-					Minz_Exception::ERROR
-				);
-			}
-		}
-	}
-	
-	/**
-	 * Initialise le Router en déterminant le couple Controller / Action
-	 * Mets à jour la Request
-	 * @exception RouteNotFoundException si l'uri n'est pas présente dans
-	 *          > la table de routage
-	 */
-	public function init () {
-		$url = array ();
-		
-		if (Minz_Configuration::useUrlRewriting ()) {
-			try {
-				$url = $this->buildWithRewriting ();
-			} catch (Minz_RouteNotFoundException $e) {
-				throw $e;
-			}
-		} else {
-			$url = $this->buildWithoutRewriting ();
-		}
-		
-		$url['params'] = array_merge (
-			$url['params'],
-			Minz_Request::fetchPOST ()
-		);
-		
-		Minz_Request::forward ($url);
-	}
-	
-	/**
-	 * Retourne un tableau représentant l'url passée par la barre d'adresses
-	 * Ne se base PAS sur la table de routage
-	 * @return tableau représentant l'url
-	 */
-	public function buildWithoutRewriting () {
-		$url = array ();
-		
-		$url['c'] = Minz_Request::fetchGET (
-			'c',
-			Minz_Request::defaultControllerName ()
-		);
-		$url['a'] = Minz_Request::fetchGET (
-			'a',
-			Minz_Request::defaultActionName ()
-		);
-		$url['params'] = Minz_Request::fetchGET ();
-		
-		// post-traitement
-		unset ($url['params']['c']);
-		unset ($url['params']['a']);
-		
-		return $url;
-	}
-	
-	/**
-	 * Retourne un tableau représentant l'url passée par la barre d'adresses
-	 * Se base sur la table de routage
-	 * @return tableau représentant l'url
-	 * @exception RouteNotFoundException si l'uri n'est pas présente dans
-	 *          > la table de routage
-	 */
-	public function buildWithRewriting () {
-		$url = array ();
-		$uri = Minz_Request::getURI ();
-		$find = false;
-		
-		foreach ($this->routes as $route) {
-			$regex = '*^' . $route['route'] . '$*';
-			if (preg_match ($regex, $uri, $matches)) {
-				$url['c'] = $route['controller'];
-				$url['a'] = $route['action'];
-				$url['params'] = $this->getParams (
-					$route['params'],
-					$matches
-				);
-				$find = true;
-				break;
-			}
-		}
-		
-		if (!$find && $uri != '/') {
-			throw new Minz_RouteNotFoundException (
-				$uri,
-				Minz_Exception::ERROR
-			);
-		}
-		
-		// post-traitement
-		$url = Minz_Url::checkUrl ($url);
-		
-		return $url;
-	}
-	
-	/**
-	 * Retourne l'uri d'une url en se basant sur la table de routage
-	 * @param l'url sous forme de tableau
-	 * @return l'uri formatée (string) selon une route trouvée
-	 */
-	public function printUriRewrited ($url) {
-		$route = $this->searchRoute ($url);
-		
-		if ($route !== false) {
-			return $this->replaceParams ($route, $url['params']);
-		}
-		
-		return '';
-	}
-	
-	/**
-	 * Recherche la route correspondante à une url
-	 * @param l'url sous forme de tableau
-	 * @return la route telle que spécifiée dans la table de routage,
-	 *         false si pas trouvée
-	 */
-	public function searchRoute ($url) {
-		foreach ($this->routes as $route) {
-			if ($route['controller'] == $url['c']
-			 && $route['action'] == $url['a']) {
-				// calcule la différence des tableaux de params
-				$params = array_flip ($route['params']);
-				$difference_params = array_diff_key (
-					$params,
-					$url['params']
-				);
-				
-				// vérifie que pas de différence
-				// et le cas où $params est vide et pas $url['params']
-				if (empty ($difference_params)
-				&& (!empty ($params) || empty ($url['params']))) {
-					return $route;
-				}
-			}
-		}
-		
-		return false;
-	}
-	
-	/**
-	 * Récupère un tableau dont
-	 * 	- les clés sont définies dans $params_route
-	 *	- les valeurs sont situées dans $matches
-	 * Le tableau $matches est décalé de +1 par rapport à $params_route
-	 */
-	private function getParams($params_route, $matches) {
-		$params = array ();
-		
-		for ($i = 0; $i < count ($params_route); $i++) {
-			$param = $params_route[$i];
-			$params[$param] = $matches[$i + 1];
-		}
-	
-		return $params;
-	}
-	
-	/**
-	 * Remplace les éléments de la route par les valeurs contenues dans $params
-	 */
-	private function replaceParams ($route, $params_replace) {
-		$uri = $route['route'];
-		$params = array();
-		foreach($route['params'] as $param) {
-			$uri = preg_replace('#\((.+)\)#U', $params_replace[$param], $uri, 1);
-		}
-
-		return stripslashes($uri);
-	 }
-}

+ 4 - 11
lib/Minz/Url.php

@@ -5,8 +5,7 @@
  */
 class Minz_Url {
 	/**
-	 * Affiche une Url formatée selon que l'on utilise l'url_rewriting ou non
-	 * si oui, on cherche dans la table de routage la correspondance pour formater
+	 * Affiche une Url formatée
 	 * @param $url l'url à formater définie comme un tableau :
 	 *                    $url['c'] = controller
 	 *                    $url['a'] = action
@@ -39,13 +38,7 @@ class Minz_Url {
 		}
 
 		if ($isArray) {
-			$router = new Minz_Router ();
-
-			if (Minz_Configuration::useUrlRewriting ()) {
-				$url_string .= $router->printUriRewrited ($url);
-			} else {
-				$url_string .= self::printUri ($url, $encodage);
-			}
+			$url_string .= self::printUri ($url, $encodage);
 		} else {
 			$url_string .= $url;
 		}
@@ -54,14 +47,14 @@ class Minz_Url {
 	}
 	
 	/**
-	 * Construit l'URI d'une URL sans url rewriting
+	 * Construit l'URI d'une URL
 	 * @param l'url sous forme de tableau
 	 * @param $encodage pour indiquer comment encoder les & (& ou &amp; pour html)
 	 * @return l'uri sous la forme ?key=value&key2=value2
 	 */
 	private static function printUri ($url, $encodage) {
 		$uri = '';
-		$separator = '/?';
+		$separator = '?';
 		
 		if($encodage == 'html') {
 			$and = '&amp;';

+ 10 - 0
lib/Minz/View.php

@@ -102,6 +102,16 @@ class Minz_View {
 		}
 	}
 
+	/**
+	 * Retourne renderHelper() dans une chaîne
+	 * @param $helper l'élément à traîter
+	 */
+	public function helperToString($helper) {
+		ob_start();
+		$this->renderHelper($helper);
+		return ob_get_clean();
+	}
+
 	/**
 	 * Permet de choisir si on souhaite utiliser le layout
 	 * @param $use true si on souhaite utiliser le layout, false sinon

+ 59 - 10
lib/SimplePie/SimplePie.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev-FreshRSS
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon
@@ -50,7 +50,7 @@ define('SIMPLEPIE_NAME', 'SimplePie');
 /**
  * SimplePie Version
  */
-define('SIMPLEPIE_VERSION', '1.3.1');
+define('SIMPLEPIE_VERSION', '1.4-dev-FreshRSS');
 
 /**
  * SimplePie Build
@@ -602,7 +602,7 @@ class SimplePie
 	public $strip_attributes = array('bgsound', 'class', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc');
 
 	/**
-	 * @var array Stores the default attributes to add to differet tags by add_attributes().
+	 * @var array Stores the default attributes to add to different tags by add_attributes().
 	 * @see SimplePie::add_attributes()
 	 * @access private
 	 */
@@ -644,7 +644,7 @@ class SimplePie
 		if (func_num_args() > 0)
 		{
 			$level = defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : E_USER_WARNING;
-			trigger_error('Passing parameters to the constructor is no longer supported. Please use set_feed_url(), set_cache_location(), and set_cache_location() directly.', $level);
+			trigger_error('Passing parameters to the constructor is no longer supported. Please use set_feed_url(), set_cache_location(), and set_cache_duration() directly.', $level);
 
 			$args = func_get_args();
 			switch (count($args)) {
@@ -1212,6 +1212,20 @@ class SimplePie
 		$this->item_limit = (int) $limit;
 	}
 
+	/**
+	 * Enable throwing exceptions
+	 *
+	 * @param boolean $enable Should we throw exceptions, or use the old-style error property?
+	 */
+	public function enable_exceptions($enable = true)
+	{
+		$this->enable_exceptions = $enable;
+	}
+
+	function cleanMd5($rss) {	//FreshRSS
+		return md5(preg_replace(array('#<(lastBuildDate|pubDate|updated|feedDate|dc:date|slash:comments)>[^<]+</\\1>#', '#<!--.+?-->#s'), '', $rss));
+	}
+
 	/**
 	 * Initialize the feed object
 	 *
@@ -1219,7 +1233,7 @@ class SimplePie
 	 * configuration options get processed, feeds are fetched, cached, and
 	 * parsed, and all of that other good stuff.
 	 *
-	 * @return boolean True if successful, false otherwise
+	 * @return positive integer with modification time if using cache, boolean true if otherwise successful, false otherwise
 	 */
 	public function init()
 	{
@@ -1298,13 +1312,17 @@ class SimplePie
 			// Fetch the data via SimplePie_File into $this->raw_data
 			if (($fetched = $this->fetch_data($cache)) === true)
 			{
-				return true;
+				return $this->data['mtime'];	//FreshRSS
 			}
 			elseif ($fetched === false) {
 				return false;
 			}
 
 			list($headers, $sniffed) = $fetched;
+
+			if (isset($this->data['md5'])) {	//FreshRSS
+				$md5 = $this->data['md5'];
+			}
 		}
 
 		// Set up array of possible encodings
@@ -1386,6 +1404,8 @@ class SimplePie
 						$this->data['headers'] = $headers;
 					}
 					$this->data['build'] = SIMPLEPIE_BUILD;
+					$this->data['mtime'] = time();	//FreshRSS
+					$this->data['md5'] = empty($md5) ? $this->cleanMd5($this->raw_data) : $md5;	//FreshRSS
 
 					// Cache the file if caching is enabled
 					if ($cache && !$cache->save($this))
@@ -1461,7 +1481,7 @@ class SimplePie
 				elseif ($cache->mtime() + $this->cache_duration < time())
 				{
 					// If we have last-modified and/or etag set
-					if (isset($this->data['headers']['last-modified']) || isset($this->data['headers']['etag']))
+					//if (isset($this->data['headers']['last-modified']) || isset($this->data['headers']['etag']))	//FreshRSS removed
 					{
 						$headers = array(
 							'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
@@ -1475,7 +1495,7 @@ class SimplePie
 							$headers['if-none-match'] = $this->data['headers']['etag'];
 						}
 
-						$file = $this->registry->create('File', array($this->feed_url, $this->timeout/10, 5, $headers, $this->useragent, $this->force_fsockopen));
+						$file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen));	//FreshRSS
 
 						if ($file->success)
 						{
@@ -1487,7 +1507,20 @@ class SimplePie
 						}
 						else
 						{
-							unset($file);
+							$this->error = $file->error;	//FreshRSS
+							return !empty($this->data);	//FreshRSS
+							//unset($file);	//FreshRSS removed
+						}
+					}
+					{	//FreshRSS
+						$md5 = $this->cleanMd5($file->body);
+						if ($this->data['md5'] === $md5) {
+							syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . $this->feed_url);
+							$cache->touch();
+							return true;	//Content unchanged even though server did not send a 304
+						} else {
+							syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . $this->feed_url);
+							$this->data['md5'] = $md5;
 						}
 					}
 				}
@@ -1555,6 +1588,8 @@ class SimplePie
 				if ($cache)
 				{
 					$this->data = array('url' => $this->feed_url, 'feed_url' => $file->url, 'build' => SIMPLEPIE_BUILD);
+					$this->data['mtime'] = time();	//FreshRSS
+					$this->data['md5'] = empty($md5) ? $this->cleanMd5($file->body) : $md5;	//FreshRSS
 					if (!$cache->save($this))
 					{
 						trigger_error("$this->cache_location is not writeable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
@@ -1987,7 +2022,21 @@ class SimplePie
 	 */
 	public function sanitize($data, $type, $base = '')
 	{
-		return $this->sanitize->sanitize($data, $type, $base);
+		try
+		{
+			return $this->sanitize->sanitize($data, $type, $base);
+		}
+		catch (SimplePie_Exception $e)
+		{
+			if (!$this->enable_exceptions)
+			{
+				$this->error = $e->getMessage();
+				$this->registry->call('Misc', 'error', array($this->error, E_USER_WARNING, $e->getFile(), $e->getLine()));
+				return '';
+			}
+
+			throw $e;
+		}
 	}
 
 	/**

+ 1 - 1
lib/SimplePie/SimplePie/Author.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Cache.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Cache/Base.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Cache/DB.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Cache/File.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 5 - 7
lib/SimplePie/SimplePie/Cache/Memcache.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon
@@ -95,10 +95,8 @@ class SimplePie_Cache_Memcache implements SimplePie_Cache_Base
 				'prefix' => 'simplepie_',
 			),
 		);
-		$parsed = SimplePie_Cache::parse_URL($location);
-		$this->options['host'] = empty($parsed['host']) ? $this->options['host'] : $parsed['host'];
-		$this->options['port'] = empty($parsed['port']) ? $this->options['port'] : $parsed['port'];
-		$this->options['extras'] = array_merge($this->options['extras'], $parsed['extras']);
+		$this->options = SimplePie_Misc::array_merge_recursive($this->options, SimplePie_Cache::parse_URL($location));
+
 		$this->name = $this->options['extras']['prefix'] . md5("$name:$type");
 
 		$this->cache = new Memcache();
@@ -147,7 +145,7 @@ class SimplePie_Cache_Memcache implements SimplePie_Cache_Base
 
 		if ($data !== false)
 		{
-			// essentially ignore the mtime because Memcache expires on it's own
+			// essentially ignore the mtime because Memcache expires on its own
 			return time();
 		}
 
@@ -165,7 +163,7 @@ class SimplePie_Cache_Memcache implements SimplePie_Cache_Base
 
 		if ($data !== false)
 		{
-			return $this->cache->set($this->name, $data, MEMCACHE_COMPRESSED, (int) $this->duration);
+			return $this->cache->set($this->name, $data, MEMCACHE_COMPRESSED, (int) $this->options['extras']['timeout']);
 		}
 
 		return false;

+ 4 - 3
lib/SimplePie/SimplePie/Cache/MySQL.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon
@@ -96,7 +96,8 @@ class SimplePie_Cache_MySQL extends SimplePie_Cache_DB
 				'prefix' => '',
 			),
 		);
-		$this->options = array_merge_recursive($this->options, SimplePie_Cache::parse_URL($location));
+		
+		$this->options = SimplePie_Misc::array_merge_recursive($this->options, SimplePie_Cache::parse_URL($location));
 
 		// Path is prefixed with a "/"
 		$this->options['dbname'] = substr($this->options['path'], 1);
@@ -136,7 +137,7 @@ class SimplePie_Cache_MySQL extends SimplePie_Cache_DB
 
 		if (!in_array($this->options['extras']['prefix'] . 'items', $db))
 		{
-			$query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'items` (`feed_id` TEXT CHARACTER SET utf8 NOT NULL, `id` TEXT CHARACTER SET utf8 NOT NULL, `data` TEXT CHARACTER SET utf8 NOT NULL, `posted` INT UNSIGNED NOT NULL, INDEX `feed_id` (`feed_id`(125)))');
+			$query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'items` (`feed_id` TEXT CHARACTER SET utf8 NOT NULL, `id` TEXT CHARACTER SET utf8 NOT NULL, `data` MEDIUMBLOB CHARACTER SET utf8 NOT NULL, `posted` INT UNSIGNED NOT NULL, INDEX `feed_id` (`feed_id`(125)))');
 			if ($query === false)
 			{
 				$this->mysql = null;

+ 1 - 1
lib/SimplePie/SimplePie/Caption.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Category.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Content/Type/Sniffer.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Copyright.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Core.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Credit.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Decode/HTML/Entities.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 2 - 2
lib/SimplePie/SimplePie/Enclosure.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon
@@ -942,7 +942,7 @@ class SimplePie_Enclosure
 	 * - `height` (integer): The height of the embedded media. Accepts any
 	 *    numeric pixel value (such as `360`) or `auto`. Defaults to `auto`,
 	 *    and it is recommended that you use this default.
-	 * - `loop` (boolean): Do you want the media to loop when its done?
+	 * - `loop` (boolean): Do you want the media to loop when it's done?
 	 *    Defaults to `false`.
 	 * - `mediaplayer` (string): The location of the included
 	 *    `mediaplayer.swf` file. This allows for the playback of Flash Video

+ 3 - 3
lib/SimplePie/SimplePie/File.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon
@@ -108,7 +108,7 @@ class SimplePie_File
 				curl_setopt($fp, CURLOPT_REFERER, $url);
 				curl_setopt($fp, CURLOPT_USERAGENT, $useragent);
 				curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2);
-				curl_setopt($fp, CURLOPT_SSL_VERIFYPEER, false);
+				curl_setopt($fp, CURLOPT_SSL_VERIFYPEER, false);	//FreshRSS
 				if (!ini_get('open_basedir') && !ini_get('safe_mode') && version_compare(SimplePie_Misc::get_curl_version(), '7.15.2', '>='))
 				{
 					curl_setopt($fp, CURLOPT_FOLLOWLOCATION, 1);
@@ -284,7 +284,7 @@ class SimplePie_File
 		else
 		{
 			$this->method = SIMPLEPIE_FILE_SOURCE_LOCAL | SIMPLEPIE_FILE_SOURCE_FILE_GET_CONTENTS;
-			if (!$this->body = file_get_contents($url))
+			if (empty($url) || !($this->body = file_get_contents($url)))
 			{
 				$this->error = 'file_get_contents could not read the file';
 				$this->success = false;

+ 1 - 1
lib/SimplePie/SimplePie/HTTP/Parser.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/IRI.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 3 - 3
lib/SimplePie/SimplePie/Item.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon
@@ -821,7 +821,7 @@ class SimplePie_Item
 			if (!empty($this->data['updated']['raw']))
 			{
 				$parser = $this->registry->call('Parse_Date', 'get');
-				$this->data['updated']['parsed'] = $parser->parse($this->data['date']['raw']);
+				$this->data['updated']['parsed'] = $parser->parse($this->data['updated']['raw']);
 			}
 			else
 			{
@@ -1080,7 +1080,7 @@ class SimplePie_Item
 	 *
 	 * @since Beta 2
 	 * @todo Add support for end-user defined sorting of enclosures by type/handler (so we can prefer the faster-loading FLV over MP4).
-	 * @todo If an element exists at a level, but it's value is empty, we should fall back to the value from the parent (if it exists).
+	 * @todo If an element exists at a level, but its value is empty, we should fall back to the value from the parent (if it exists).
 	 * @return array|null List of SimplePie_Enclosure items
 	 */
 	public function get_enclosures()

+ 2 - 2
lib/SimplePie/SimplePie/Locator.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon
@@ -277,7 +277,7 @@ class SimplePie_Locator
 				$parsed = $this->registry->call('Misc', 'parse_url', array($href));
 				if ($parsed['scheme'] === '' || preg_match('/^(http(s)|feed)?$/i', $parsed['scheme']))
 				{
-					if ($this->base_location < $link->getLineNo())
+					if (method_exists($link, 'getLineNo') && $this->base_location < $link->getLineNo())
 					{
 						$href = $this->registry->call('Misc', 'absolutize_url', array(trim($link->getAttribute('href')), $this->base));
 					}

+ 23 - 30
lib/SimplePie/SimplePie/Misc.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon
@@ -128,7 +128,7 @@ class SimplePie_Misc
 						{
 							$attribs[$j][2] = $attribs[$j][1];
 						}
-						$return[$i]['attribs'][strtolower($attribs[$j][1])]['data'] = SimplePie_Misc::entities_decode(end($attribs[$j]), 'UTF-8');
+						$return[$i]['attribs'][strtolower($attribs[$j][1])]['data'] = SimplePie_Misc::entities_decode(end($attribs[$j]), 'UTF-8');	//FreshRSS
 					}
 				}
 			}
@@ -142,7 +142,7 @@ class SimplePie_Misc
 		foreach ($element['attribs'] as $key => $value)
 		{
 			$key = strtolower($key);
-			$full .= " $key=\"" . htmlspecialchars($value['data'], ENT_COMPAT, 'UTF-8') . '"';
+			$full .= " $key=\"" . htmlspecialchars($value['data'], ENT_COMPAT, 'UTF-8') . '"';	//FreshRSS
 		}
 		if ($element['self_closing'])
 		{
@@ -228,6 +228,23 @@ class SimplePie_Misc
 		}
 	}
 
+	public static function array_merge_recursive($array1, $array2)
+	{
+		foreach ($array2 as $key => $value)
+		{
+			if (is_array($value))
+			{
+				$array1[$key] = SimplePie_Misc::array_merge_recursive($array1[$key], $value);
+			}
+			else
+			{
+				$array1[$key] = $value;
+			}            
+		}
+		
+		return $array1;
+	}
+
 	public static function parse_url($url)
 	{
 		$iri = new SimplePie_IRI($url);
@@ -2161,36 +2178,12 @@ function embed_wmedia(width, height, link) {
 	/**
 	 * Get the SimplePie build timestamp
 	 *
-	 * Uses the git index if it exists, otherwise uses the modification time
-	 * of the newest file.
+	 * Return SimplePie.php modification time.
 	 */
 	public static function get_build()
 	{
-		$root = dirname(dirname(__FILE__));
-		if (file_exists($root . '/.git/index'))
-		{
-			return filemtime($root . '/.git/index');
-		}
-		elseif (file_exists($root . '/SimplePie'))
-		{
-			$time = 0;
-			foreach (glob($root . '/SimplePie/*.php') as $file)
-			{
-				if (($mtime = filemtime($file)) > $time)
-				{
-					$time = $mtime;
-				}
-			}
-			return $time;
-		}
-		elseif (file_exists(dirname(__FILE__) . '/Core.php'))
-		{
-			return filemtime(dirname(__FILE__) . '/Core.php');
-		}
-		else
-		{
-			return filemtime(__FILE__);
-		}
+		$mtime = @filemtime(dirname(dirname(__FILE__)) . '/SimplePie.php');	//FreshRSS
+		return $mtime ? $mtime : filemtime(__FILE__);
 	}
 
 	/**

+ 1 - 1
lib/SimplePie/SimplePie/Net/IPv6.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Parse/Date.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 6 - 1
lib/SimplePie/SimplePie/Parser.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon
@@ -145,10 +145,15 @@ class SimplePie_Parser
 				$dom->loadXML($data);
 				$this->encoding = $encoding = $dom->encoding = 'UTF-8';
 				$data2 = $dom->saveXML();
+				if (function_exists('mb_convert_encoding'))
+				{
+					$data2 = mb_convert_encoding($data2, 'UTF-8', 'UTF-8');
+				}
 				if (strlen($data2) > (strlen($data) / 2.0))
 				{
 					$data = $data2;
 				}
+				unset($data2);
 			}
 			catch (Exception $e)
 			{

+ 1 - 1
lib/SimplePie/SimplePie/Rating.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Registry.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/Restriction.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 6 - 2
lib/SimplePie/SimplePie/Sanitize.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon
@@ -267,6 +267,10 @@ class SimplePie_Sanitize
 			if ($type & (SIMPLEPIE_CONSTRUCT_HTML | SIMPLEPIE_CONSTRUCT_XHTML))
 			{
 
+				if (!class_exists('DOMDocument'))
+				{
+					throw new SimplePie_Exception('DOMDocument not found, unable to use sanitizer');
+				}
 				$document = new DOMDocument();
 				$document->encoding = 'UTF-8';
 				$data = $this->preprocess($data, $type);
@@ -339,7 +343,7 @@ class SimplePie_Sanitize
 							}
 							else
 							{
-								$file = $this->registry->create('File', array($img['attribs']['src']['data'], $this->timeout, 5, array('X-FORWARDED-FOR' => $_SERVER['REMOTE_ADDR']), $this->useragent, $this->force_fsockopen));
+								$file = $this->registry->create('File', array($img->getAttribute('src'), $this->timeout, 5, array('X-FORWARDED-FOR' => $_SERVER['REMOTE_ADDR']), $this->useragent, $this->force_fsockopen));
 								$headers = $file->headers;
 
 								if ($file->success && ($file->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($file->status_code === 200 || $file->status_code > 206 && $file->status_code < 300)))

+ 1 - 1
lib/SimplePie/SimplePie/Source.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/XML/Declaration/Parser.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 1 - 1
lib/SimplePie/SimplePie/gzdecode.php

@@ -33,7 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.3.1
+ * @version 1.4-dev
  * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Geoffrey Sneddon

+ 131 - 0
lib/lib_date.php

@@ -0,0 +1,131 @@
+<?php
+/**
+ * Author: Alexandre Alapetite http://alexandre.alapetite.fr
+ * 2014-06-01
+ * License: GNU AGPLv3 http://www.gnu.org/licenses/agpl-3.0.html
+ *
+ * Parser of ISO 8601 time intervals http://en.wikipedia.org/wiki/ISO_8601#Time_intervals
+ *	Examples: "2014-02/2014-04", "2014-02/04", "2014-06", "P1M"
+ */
+
+/*
+example('2014-03');
+example('201403');
+example('2014-03-30');
+example('2014-05-30T13');
+example('2014-05-30T13:30');
+example('2014-02/2014-04');
+example('2014-02--2014-04');
+example('2014-02/04');
+example('2014-02-03/05');
+example('2014-02-03T22:00/22:15');
+example('2014-02-03T22:00/15');
+example('2014-03/');
+example('/2014-03');
+example('2014-03/P1W');
+example('P1W/2014-05-25T23:59:59');
+example('P1Y/');
+example('P1Y');
+example('P2M/');
+example('P3W/');
+example('P4D/');
+example('PT5H/');
+example('PT6M/');
+example('PT7S/');
+example('P1DT1H/');
+
+function example($dateInterval) {
+	$dateIntervalArray = parseDateInterval($dateInterval);
+	echo $dateInterval, "\t=>\t",
+		$dateIntervalArray[0] == null ? 'null' : @date('c', $dateIntervalArray[0]), '/',
+		$dateIntervalArray[1] == null ? 'null' : @date('c', $dateIntervalArray[1]), "\n";
+}
+*/
+
+function _dateFloor($isoDate) {
+	$x = explode('T', $isoDate, 2);
+	$t = isset($x[1]) ? str_pad($x[1], 6, '0') : '000000';
+	return str_pad($x[0], 8, '01') . 'T' . $t;
+}
+
+function _dateCeiling($isoDate) {
+	$x = explode('T', $isoDate, 2);
+	$t = isset($x[1]) && strlen($x[1]) > 1 ? str_pad($x[1], 6, '59') : '235959';
+	switch (strlen($x[0])) {
+		case 4:
+			return $x[0] . '1231T' . $t;
+		case 6:
+			$d = @strtotime($x[0] . '01');
+			return $x[0] . date('t', $d) . 'T' . $t;
+		default:
+			return $x[0] . 'T' . $t;
+	}
+}
+
+function _noDelimit($isoDate) {
+	return $isoDate === null || $isoDate === '' ? null :
+		str_replace(array('-', ':'), '', $isoDate);	//FIXME: Bug with negative time zone
+}
+
+function _dateRelative($d1, $d2) {
+	if ($d2 === null) {
+		return $d1 !== null && $d1[0] !== 'P' ? $d1 : null;
+	} elseif ($d2 !== '' && $d2[0] != 'P' && $d1 !== null && $d1[0] !== 'P') {
+		$y2 = substr($d2, 0, 4);
+		if (strlen($y2) < 4 || !ctype_digit($y2)) {	//Does not start by a year
+			$d2 = _noDelimit($d2);
+			return substr($d1, 0, -strlen($d2)) . $d2;	//Add prefix from $d1
+		}
+	}
+	return _noDelimit($d2);
+}
+
+/**
+ * Parameter $dateInterval is a string containing an ISO 8601 time interval.
+ * Returns an array with the minimum and maximum Unix timestamp of this interval,
+ *  or null if open interval, or false if error.
+ */
+function parseDateInterval($dateInterval) {
+	$dateInterval = trim($dateInterval);
+	$dateInterval = str_replace('--', '/', $dateInterval);
+	$dateInterval = strtoupper($dateInterval);
+	$min = null;
+	$max = null;
+	$x = explode('/', $dateInterval, 2);
+	$d1 = _noDelimit($x[0]);
+	$d2 = _dateRelative($d1, count($x) > 1 ? $x[1] : null);
+	if ($d1 !== null && $d1[0] !== 'P') {
+		$min = @strtotime(_dateFloor($d1));
+	}
+	if ($d2 !== null) {
+		if ($d2[0] === 'P') {
+			try {
+				$di2 = new DateInterval($d2);
+				$dt1 = @date_create();	//new DateTime() would create an Exception if the default time zone is not defined
+				if ($min !== null && $min !== false) {
+					$dt1->setTimestamp($min);
+				}
+				$max = $dt1->add($di2)->getTimestamp() - 1;
+			} catch (Exception $e) {
+				$max = false;
+			}
+		} elseif ($d1 === null || $d1[0] !== 'P') {
+			$max = @strtotime(_dateCeiling($d2));
+		} else {
+			$max = @strtotime($d2);
+		}
+	}
+	if ($d1 !== null && $d1[0] === 'P') {
+		try {
+			$di1 = new DateInterval($d1);
+			$dt2 = @date_create();
+			if ($max !== null && $max !== false) {
+				$dt2->setTimestamp($max);
+			}
+			$min = $dt2->sub($di1)->getTimestamp() + 1;
+		} catch (Exception $e) {
+				$min = false;
+		}
+	}
+	return array($min, $max);
+}

+ 194 - 84
lib/lib_opml.php

@@ -1,23 +1,86 @@
 <?php
-function opml_export ($cats) {
-	$txt = '';
 
-	foreach ($cats as $cat) {
-		$txt .= '<outline text="' . $cat['name'] . '">' . "\n";
-
-		foreach ($cat['feeds'] as $feed) {
-			$txt .= "\t" . '<outline text="' . $feed->name () . '" type="rss" xmlUrl="' . $feed->url () . '" htmlUrl="' . $feed->website () . '" description="' . htmlspecialchars($feed->description(), ENT_COMPAT, 'UTF-8') . '" />' . "\n";
+/* *
+ * lib_opml is a free library to manage OPML format in PHP.
+ * It takes in consideration only version 2.0 (http://dev.opml.org/spec2.html).
+ * Basically it means "text" attribute for outline elements is required.
+ *
+ * lib_opml requires SimpleXML (http://php.net/manual/en/book.simplexml.php)
+ *
+ * Usages:
+ * > include('lib_opml.php');
+ * > $filename = 'my_opml_file.xml';
+ * > $opml_array = libopml_parse_file($filename);
+ * > print_r($opml_array);
+ *
+ * > $opml_string = [...];
+ * > $opml_array = libopml_parse_string($opml_string);
+ * > print_r($opml_array);
+ *
+ * > $opml_array = [...];
+ * > $opml_string = libopml_render($opml_array);
+ * > $opml_object = libopml_render($opml_array, true);
+ * > echo $opml_string;
+ * > print_r($opml_object);
+ *
+ * If parsing fails for any reason (e.g. not an XML string, does not match with
+ * the specifications), a LibOPML_Exception is raised.
+ *
+ * Author: Marien Fressinaud <dev@marienfressinaud.fr>
+ * Url: https://github.com/marienfressinaud/lib_opml
+ * Version: 0.1
+ * Date: 2014-03-29
+ * License: public domain
+ *
+ * */
+
+class LibOPML_Exception extends Exception {}
+
+
+// These elements are optional
+define('HEAD_ELEMENTS', serialize(array(
+	'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail',
+	'ownerId', 'docs', 'expansionState', 'vertScrollState', 'windowTop',
+	'windowLeft', 'windowBottom', 'windowRight'
+)));
+
+
+function libopml_parse_outline($outline_xml) {
+	$outline = array();
+
+	// An outline may contain any kind of attributes but "text" attribute is
+	// required !
+	$text_is_present = false;
+	foreach ($outline_xml->attributes() as $key => $value) {
+		$outline[$key] = (string)$value;
+
+		if ($key === 'text') {
+			$text_is_present = true;
 		}
+	}
 
-		$txt .= '</outline>' . "\n";
+	if (!$text_is_present) {
+		throw new LibOPML_Exception(
+			'Outline does not contain any text attribute'
+		);
 	}
 
-	return $txt;
+	foreach ($outline_xml->children() as $key => $value) {
+		// An outline may contain any number of outline children
+		if ($key === 'outline') {
+			$outline['@outlines'][] = libopml_parse_outline($value);
+		} else {
+			throw new LibOPML_Exception(
+				'Body can contain only outline elements'
+			);
+		}
+	}
+
+	return $outline;
 }
 
-function opml_import ($xml) {
-	$xml = html_only_entity_decode($xml);	//!\ Assume UTF-8
 
+function libopml_parse_string($xml) {
 	$dom = new DOMDocument();
 	$dom->recover = true;
 	$dom->strictErrorChecking = false;
@@ -27,95 +90,142 @@ function opml_import ($xml) {
 	$opml = simplexml_import_dom($dom);
 
 	if (!$opml) {
-		throw new FreshRSS_Opml_Exception ();
+		throw new LibOPML_Exception();
 	}
 
-	$catDAO = new FreshRSS_CategoryDAO();
-	$catDAO->checkDefault();
-	$defCat = $catDAO->getDefault();
+	$array = array(
+		'version' => (string)$opml['version'],
+		'head' => array(),
+		'body' => array()
+	);
+
+	// First, we get all "head" elements. Head is required but its sub-elements
+	// are optional.
+	foreach ($opml->head->children() as $key => $value) {
+		if (in_array($key, unserialize(HEAD_ELEMENTS), true)) {
+			$array['head'][$key] = (string)$value;
+		} else {
+			throw new LibOPML_Exception(
+				$key . 'is not part of OPML format'
+			);
+		}
+	}
 
-	$categories = array ();
-	$feeds = array ();
+	// Then, we get body oulines. Body must contain at least one outline
+	// element.
+	$at_least_one_outline = false;
+	foreach ($opml->body->children() as $key => $value) {
+		if ($key === 'outline') {
+			$at_least_one_outline = true;
+			$array['body'][] = libopml_parse_outline($value);
+		} else {
+			throw new LibOPML_Exception(
+				'Body can contain only outline elements'
+			);
+		}
+	}
+
+	if (!$at_least_one_outline) {
+		throw new LibOPML_Exception(
+			'Body must contain at least one outline element'
+		);
+	}
 
-	foreach ($opml->body->outline as $outline) {
-		if (!isset ($outline['xmlUrl'])) {
-			// Catégorie
-			$title = '';
+	return $array;
+}
 
-			if (isset ($outline['text'])) {
-				$title = (string) $outline['text'];
-			} elseif (isset ($outline['title'])) {
-				$title = (string) $outline['title'];
-			}
 
-			if ($title) {
-				// Permet d'éviter les soucis au niveau des id :
-				// ceux-ci sont générés en fonction de la date,
-				// un flux pourrait être dans une catégorie X avec l'id Y
-				// alors qu'il existe déjà la catégorie X mais avec l'id Z
-				// Y ne sera pas ajouté et le flux non plus vu que l'id
-				// de sa catégorie n'exisera pas
-				$title = htmlspecialchars($title, ENT_COMPAT, 'UTF-8');
-				$catDAO = new FreshRSS_CategoryDAO ();
-				$cat = $catDAO->searchByName ($title);
-				if ($cat === false) {
-					$cat = new FreshRSS_Category ($title);
-					$values = array (
-						'name' => $cat->name (),
-						'color' => $cat->color ()
-					);
-					$cat->_id ($catDAO->addCategory ($values));
-				}
-
-				$feeds = array_merge ($feeds, getFeedsOutline ($outline, $cat->id ()));
+function libopml_parse_file($filename) {
+	$file_content = file_get_contents($filename);
+
+	if ($file_content === false) {
+		throw new LibOPML_Exception(
+			$filename . ' cannot be found'
+		);
+	}
+
+	return libopml_parse_string($file_content);
+}
+
+
+function libopml_render_outline($parent_elt, $outline) {
+	// Outline MUST be an array!
+	if (!is_array($outline)) {
+		throw new LibOPML_Exception(
+			'Outline element must be defined as array'
+		);
+	}
+
+	$outline_elt = $parent_elt->addChild('outline');
+	$text_is_present = false;
+	foreach ($outline as $key => $value) {
+		// Only outlines can be an array and so we consider children are also
+		// outline elements.
+		if ($key === '@outlines' && is_array($value)) {
+			foreach ($value as $outline_child) {
+				libopml_render_outline($outline_elt, $outline_child);
 			}
+		} elseif (is_array($value)) {
+			throw new LibOPML_Exception(
+				'Type of outline elements cannot be array: ' . $key
+			);
 		} else {
-			// Flux rss sans catégorie, on récupère l'ajoute dans la catégorie par défaut
-			$feeds[] = getFeed ($outline, $defCat->id());
+			// Detect text attribute is present, that's good :)
+			if ($key === 'text') {
+				$text_is_present = true;
+			}
+
+			$outline_elt->addAttribute($key, $value);
 		}
 	}
 
-	return array ($categories, $feeds);
+	if (!$text_is_present) {
+		throw new LibOPML_Exception(
+			'You must define at least a text element for all outlines'
+		);
+	}
 }
 
-/**
- * import all feeds of a given outline tag
- */
-function getFeedsOutline ($outline, $cat_id) {
-	$feeds = array ();
 
-	foreach ($outline->children () as $child) {
-		if (isset ($child['xmlUrl'])) {
-			$feeds[] = getFeed ($child, $cat_id);
-		} else {
-			$feeds = array_merge(
-				$feeds,
-				getFeedsOutline ($child, $cat_id)
-			);
+function libopml_render($array, $as_xml_object = false) {
+	$opml = new SimpleXMLElement('<opml version="2.0"></opml>');
+
+	// Create head element. $array['head'] is optional but head element will
+	// exist in the final XML object.
+	$head = $opml->addChild('head');
+	if (isset($array['head'])) {
+		foreach ($array['head'] as $key => $value) {
+			if (in_array($key, unserialize(HEAD_ELEMENTS), true)) {
+				$head->addChild($key, $value);
+			}
 		}
 	}
 
-	return $feeds;
-}
+	// Check body is set and contains at least one element
+	if (!isset($array['body'])) {
+		throw new LibOPML_Exception(
+			'$array must contain a body element'
+		);
+	}
+	if (count($array['body']) <= 0) {
+		throw new LibOPML_Exception(
+			'Body element must contain at least one element (array)'
+		);
+	}
 
-function getFeed ($outline, $cat_id) {
-	$url = (string) $outline['xmlUrl'];
-	$url = htmlspecialchars($url, ENT_COMPAT, 'UTF-8');
-	$title = '';
-	if (isset ($outline['text'])) {
-		$title = (string) $outline['text'];
-	} elseif (isset ($outline['title'])) {
-		$title = (string) $outline['title'];
-	}
-	$title = htmlspecialchars($title, ENT_COMPAT, 'UTF-8');
-	$feed = new FreshRSS_Feed ($url);
-	$feed->_category ($cat_id);
-	$feed->_name ($title);
-	if (isset($outline['htmlUrl'])) {
-		$feed->_website(htmlspecialchars((string)$outline['htmlUrl'], ENT_COMPAT, 'UTF-8'));
-	}
-	if (isset($outline['description'])) {
-		$feed->_description(sanitizeHTML((string)$outline['description']));
-	}
-	return $feed;
+	// Create outline elements
+	$body = $opml->addChild('body');
+	foreach ($array['body'] as $outline) {
+		libopml_render_outline($body, $outline);
+	}
+
+	// And return the final result
+	if ($as_xml_object) {
+		return $opml;
+	} else {
+		$dom = dom_import_simplexml($opml)->ownerDocument;
+		$dom->formatOutput = true;
+		$dom->encoding = 'UTF-8';
+		return $dom->saveXML();
+	}
 }

+ 18 - 3
lib/lib_rss.php

@@ -27,7 +27,7 @@ function classAutoloader($class) {
 				include(APP_PATH . '/Models/' . $components[1] . '.php');
 				return;
 			case 3:	//Controllers, Exceptions
-				include(APP_PATH . '/' . $components[2] . 's/' . $components[1] . $components[2] . '.php');
+				@include(APP_PATH . '/' . $components[2] . 's/' . $components[1] . $components[2] . '.php');
 				return;
 		}
 	} elseif (strpos($class, 'Minz') === 0) {
@@ -214,12 +214,12 @@ function uSecString() {
 }
 
 function invalidateHttpCache() {
-	touch(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log');
 	Minz_Session::_param('touch', uTimeString());
+	return touch(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log');
 }
 
 function usernameFromPath($userPath) {
-	if (preg_match('%/([a-z0-9]{1,16})_user\.php$%', $userPath, $matches)) {
+	if (preg_match('%/([A-Za-z0-9]{1,16})_user\.php$%', $userPath, $matches)) {
 		return $matches[1];
 	} else {
 		return '';
@@ -233,3 +233,18 @@ function listUsers() {
 function httpAuthUser() {
 	return isset($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'] : '';
 }
+
+function cryptAvailable() {
+	if (version_compare(PHP_VERSION, '5.3.3', '>=')) {
+		try {
+			$hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
+			return $hash === @crypt('password', $hash);
+		} catch (Exception $e) {
+		}
+	}
+	return false;
+}
+
+function html_chars_utf8($str) {
+	return htmlspecialchars($str, ENT_COMPAT, 'UTF-8');
+}

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác