Sfoglia il codice sorgente

Merge branch 'dev' into 411-update-system

Conflicts:
	constants.php
Marien Fressinaud 11 anni fa
parent
commit
ef1b35fc43
44 ha cambiato i file con 1221 aggiunte e 377 eliminazioni
  1. 3 0
      CHANGELOG
  2. 101 0
      README.fr.md
  3. 66 64
      README.md
  4. 1 0
      app/Controllers/configureController.php
  5. 123 129
      app/Controllers/importExportController.php
  6. 47 5
      app/Controllers/indexController.php
  7. 5 0
      app/Controllers/statsController.php
  8. 26 5
      app/FreshRSS.php
  9. 7 0
      app/Models/Configuration.php
  10. 2 2
      app/Models/EntryDAO.php
  11. 6 0
      app/Models/Feed.php
  12. 62 0
      app/Models/StatsDAO.php
  13. 326 0
      app/i18n/de.php
  14. 9 0
      app/i18n/en.php
  15. 9 0
      app/i18n/fr.php
  16. 6 2
      app/layout/aside_flux.phtml
  17. 10 6
      app/layout/nav_menu.phtml
  18. 3 0
      app/views/configure/categorize.phtml
  19. 10 0
      app/views/configure/reading.phtml
  20. 15 0
      app/views/configure/shortcut.phtml
  21. 5 2
      app/views/helpers/javascript_vars.phtml
  22. 6 1
      app/views/helpers/view/normal_view.phtml
  23. 3 1
      app/views/importExport/index.phtml
  24. 15 8
      app/views/index/formLogin.phtml
  25. 10 11
      app/views/javascript/actualize.phtml
  26. 69 18
      app/views/stats/repartition.phtml
  27. 1 0
      constants.php
  28. 1 0
      data/tokens/.gitignore
  29. 13 0
      data/tokens/index.html
  30. 14 3
      lib/Minz/Helper.php
  31. 83 49
      lib/Minz/Request.php
  32. 48 26
      lib/Minz/Session.php
  33. 1 1
      lib/SimplePie/SimplePie/Parser.php
  34. 0 4
      lib/lib_rss.php
  35. 1 0
      p/api/greader.php
  36. 1 0
      p/i/.gitignore
  37. 65 11
      p/scripts/main.js
  38. 2 4
      p/themes/Dark/dark.css
  39. 6 5
      p/themes/Flat/flat.css
  40. 11 9
      p/themes/Origine/origine.css
  41. 1 1
      p/themes/Screwdriver/metadata.json
  42. 18 5
      p/themes/Screwdriver/screwdriver.css
  43. 3 0
      p/themes/Screwdriver/template.css
  44. 7 5
      p/themes/base-theme/base.css

+ 3 - 0
CHANGELOG

@@ -11,9 +11,12 @@
 	* Improvements
 * Security
 	* Basic protection against XSRF (Cross-Site Request Forgery) based on HTTP Referer (POST requests only)
+* API
+	* Compatible with lighttpd
 * Misc.
 	* Changed lazyload implementation
 	* Support of HTML5 notifications for new upcoming articles
+	* Add option to stay logged in
 * Bux fixes in export function, add/remove users, keyboard shortcuts, etc.
 
 

+ 101 - 0
README.fr.md

@@ -0,0 +1,101 @@
+* [English version](README.md)
+
+# FreshRSS
+FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](http://projet.idleman.fr/leed/) ou de [Kriss Feed](http://tontof.net/kriss/feed/).
+
+Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
+
+Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme.
+
+* Site officiel : http://freshrss.org
+* Démo : http://demo.freshrss.org/
+* Développeur : Marien Fressinaud <dev@marienfressinaud.fr>
+* 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.
+Je m’engage néanmoins à répondre dans la mesure du possible aux demandes d’évolution si celles-ci me semblent justifiées.
+Privilégiez pour cela des demandes sur GitHub
+(https://github.com/marienfressinaud/FreshRSS/issues) ou par mail (dev@marienfressinaud.fr)
+
+# Pré-requis
+* 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 (recommandé), ou nginx, lighttpd (non testé sur les autres)
+* PHP 5.2.1+ (PHP 5.3.7+ recommandé)
+	* Requis : [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (seulement pour accès API sur platformes < 64 bits)
+	* Recommandés : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip)
+* MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+
+* Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
+	* Fonctionne aussi sur mobile
+
+![Capture d’écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
+
+# Installation
+1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip)
+2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`)
+3. Le serveur Web doit avoir les droits d’écriture dans le répertoire `./data/`
+4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation
+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. 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)
+		* Créer dans ce cas un fichier `./p/i/.htaccess` avec un fichier `.htpasswd` correspondant.
+
+# Rafraîchissement automatique des flux
+* Vous pouvez ajouter une tâche Cron lançant régulièrement le script d’actualisation automatique des flux.
+Consultez la documentation de Cron de votre système d’exploitation ([Debian/Ubuntu](http://doc.ubuntu-fr.org/cron), [Red Hat/Fedora](http://doc.fedora-fr.org/wiki/CRON_:_Configuration_de_t%C3%A2ches_automatis%C3%A9es), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](http://wiki.gentoo.org/wiki/Cron/fr), [Arch Linux](http://wiki.archlinux.fr/Cron)…).
+C’est une bonne idée d’utiliser le même utilisateur que votre serveur Web (souvent “www-data”).
+Par exemple, pour exécuter le script toutes les heures :
+
+```
+7 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+```
+
+# Conseils
+* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
+	* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
+* Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici.
+* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`.
+
+# Sauvegarde
+* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php` et éventuellement `./data/persona/`
+* Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML
+* Pour sauvegarder les articles eux-même, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL :
+
+```bash
+mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
+```
+
+
+# Bibliothèques incluses
+* [SimplePie](http://simplepie.org/)
+* [MINZ](https://github.com/marienfressinaud/MINZ)
+* [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)
+* [phpQuery](http://code.google.com/p/phpquery/)
+
+## Si les fonctions natives ne sont pas disponibles
+* [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198)
+* [password_compat](https://github.com/ircmaxell/password_compat)

+ 66 - 64
README.md

@@ -1,88 +1,90 @@
+* [Version française](README.fr.md)
+
 # FreshRSS
-FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](http://projet.idleman.fr/leed/) ou de [Kriss Feed](http://tontof.net/kriss/feed/).
+FreshRSS is a self-hosted RSS feed agregator like [Leed](http://projet.idleman.fr/leed/) or [Kriss Feed](http://tontof.net/kriss/feed/).
 
-Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
+It is at the same time light-weight, easy to work with, powerful and customizable.
 
-Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme.
+It is a multi-user application with an anonymous reading mode.
 
-* Site officiel : http://freshrss.org
-* Démo : http://demo.freshrss.org/
-* Développeur : Marien Fressinaud <dev@marienfressinaud.fr>
-* Version actuelle : 0.8-dev
-* Date de publication 2014-0x-xx
+* Official website: http://freshrss.org
+* Demo: http://demo.freshrss.org/
+* Developer: Marien Fressinaud <dev@marienfressinaud.fr>
+* Current version: 0.8-dev
+* Publication date: 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)
+![FreshRSS logo](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 :
+# Note on branches
+**This application is still in development!** Please use the branch that suits your needs:
 
-* 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 !
+* Use [the master branch](https://github.com/marienfressinaud/FreshRSS/tree/master/) if you need a stable version.
+* [The beta branch](https://github.com/marienfressinaud/FreshRSS/tree/beta) is the default branch: new features are added on a monthly basis.
+* For developers and tech savvy persons, [the dev branch](https://github.com/marienfressinaud/FreshRSS/tree/dev) is waiting for you!
 
 # 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.
-Je m’engage néanmoins à répondre dans la mesure du possible aux demandes d’évolution si celles-ci me semblent justifiées.
-Privilégiez pour cela des demandes sur GitHub
-(https://github.com/marienfressinaud/FreshRSS/issues) ou par mail (dev@marienfressinaud.fr)
-
-# Pré-requis
-* 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.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), [Zip](http://php.net/zip)
-* MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+ (en bêta)
-* Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
-	* Fonctionne aussi sur mobile
-
-![Capture d’écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
+This application was developed to fulfill personal needs not professional needs.
+There is no guarantee neither on its security nor its proper functioning.
+If there is feature requests which I think are good for the project, I'll do my best to include them.
+The best way is to open issues on GitHub
+(https://github.com/marienfressinaud/FreshRSS/issues) or by email (dev@marienfressinaud.fr)
+
+# Requirements
+* Light server running Linux or Windows
+	* It even works on Raspberry Pi with response time under a second (tested with 150 feeds, 22k articles, or 32Mo of compressed data)
+* A web server: Apache2 (recommanded), nginx, lighttpd (not tested on others)
+* PHP 5.2.1+ (PHP 5.3.7+ recommanded)
+	* Required extensions: [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (only for API access on platforms under 64 bits)
+	* Recommanded extensions : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip)
+* MySQL 5.0.3+ (recommanded) ou SQLite 3.7.4+
+* A recent browser like Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
+	* Works on mobile
+
+![FreshRSS screenshot](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
 
 # Installation
-1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip)
-2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`)
-3. Le serveur Web doit avoir les droits d’écriture dans le répertoire `./data/`
-4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation
-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. 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)
-		* Créer dans ce cas un fichier `./p/i/.htaccess` avec un fichier `.htpasswd` correspondant.
-
-# Rafraîchissement automatique des flux
-* Vous pouvez ajouter une tâche Cron lançant régulièrement le script d’actualisation automatique des flux.
-Consultez la documentation de Cron de votre système d’exploitation ([Debian/Ubuntu](http://doc.ubuntu-fr.org/cron), [Red Hat/Fedora](http://doc.fedora-fr.org/wiki/CRON_:_Configuration_de_t%C3%A2ches_automatis%C3%A9es), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](http://wiki.gentoo.org/wiki/Cron/fr), [Arch Linux](http://wiki.archlinux.fr/Cron)…).
-C’est une bonne idée d’utiliser le même utilisateur que votre serveur Web (souvent “www-data”).
-Par exemple, pour exécuter le script toutes les heures :
+1. Get FreshRSS with git or [by downloading the archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip)
+2. Dump the application on your server (expose only the `./p/` folder)
+3. Add write access on `./data/` folder to the webserver user
+4. Access FreshRSS with your browser and follow the installation process
+5. Every thing should be working :) If you encounter any problem, feel free to contact me.
+
+# Access control
+It is needed for the multi-user mode to limit access to FreshRSS. You can:
+* use form authentication (need JavaScript and PHP 5.3.7+, works with some PHP 5.3.3+)
+* use [Mozilla Persona](https://login.persona.org/about) authentication included in FreshRSS
+* use HTTP authentication supported by your web server
+	* See [Apache documentation](http://httpd.apache.org/docs/trunk/howto/auth.html)
+		* In that case, create a `./p/i/.htaccess` file with a matching `.htpasswd` file.
+
+# Automatic feed update
+* You can add a Cron job to launch the update script.
+Check the Cron documentation related to your distribution ([Debian/Ubuntu](https://help.ubuntu.com/community/CronHowto), [Red Hat/Fedora](https://fedoraproject.org/wiki/Administration_Guide_Draft/Cron), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](https://wiki.gentoo.org/wiki/Cron), [Arch Linux](https://wiki.archlinux.org/index.php/Cron)…).
+It’s a good idea to use the web server user .
+For example, if you want to run the script every hour:
 
 ```
 7 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
 ```
 
-# Conseils
-* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
-	* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
-* Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici.
-* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`.
+# Advices
+* For a better security, expose only the `./p/` folder on the web.
+	* Be aware that the `./data/` folder contain all personal data, so it is a bad idea to expose it.
+* The `./constants.php` file define access to application folder. If you want to customize your installation, every thing happens here.
+* If you encounter some problem, logs are accessibles from the interface or manually in `./data/log/*.log` files.
 
-# Sauvegarde
-* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php` et éventuellement `./data/persona/`
-* Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML
-* Pour sauvegarder les articles eux-même, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL :
+# Backup
+* You need to keep `./data/config.php`, `./data/*_user.php` and `./data/persona/` files
+* You can export your feed list in OPML format from FreshRSS
+* To save articles, you can use [phpMyAdmin](http://www.phpmyadmin.net) or MySQL tools:
 
 ```bash
-mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
+mysqldump -u user -p --databases freshrss > freshrss.sql
 ```
 
 
-# Bibliothèques incluses
+# Included libraries
 * [SimplePie](http://simplepie.org/)
 * [MINZ](https://github.com/marienfressinaud/MINZ)
 * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/)
@@ -90,10 +92,10 @@ mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
 * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/)
 * [flotr2](http://www.humblesoftware.com/flotr2)
 
-## Uniquement pour certaines options
+## Only for some options
 * [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
 * [phpQuery](http://code.google.com/p/phpquery/)
 
-## Si les fonctions natives ne sont pas disponibles
+## If native functions are not available
 * [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198)
 * [password_compat](https://github.com/ircmaxell/password_compat)

+ 1 - 0
app/Controllers/configureController.php

@@ -184,6 +184,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			$this->view->conf->_default_view((int)Minz_Request::param('default_view', FreshRSS_Entry::STATE_ALL));
 			$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->_display_categories(Minz_Request::param('display_categories', false));
 			$this->view->conf->_hide_read_feeds(Minz_Request::param('hide_read_feeds', false));
 			$this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false));
 			$this->view->conf->_lazyload(Minz_Request::param('lazyload', false));

+ 123 - 129
app/Controllers/importExportController.php

@@ -5,7 +5,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		if (!$this->view->loginOk) {
 			Minz_Error::error(
 				403,
-				array('error' => array(Minz_Translate::t('access_denied')))
+				array('error' => array(_t('access_denied')))
 			);
 		}
 
@@ -20,33 +20,51 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$this->view->categories = $this->catDAO->listCategories();
 		$this->view->feeds = $this->feedDAO->listFeeds();
 
-		Minz_View::prependTitle(Minz_Translate::t('import_export') . ' · ');
+		Minz_View::prependTitle(_t('import_export') . ' · ');
 	}
 
 	public function importAction() {
-		if (Minz_Request::isPost() && $_FILES['file']['error'] == 0) {
-			@set_time_limit(300);
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+		}
 
-			$file = $_FILES['file'];
-			$type_file = $this->guessFileType($file['name']);
+		$file = $_FILES['file'];
+		$status_file = $file['error'];
 
-			$list_files = array(
-				'opml' => array(),
-				'json_starred' => array(),
-				'json_feed' => array()
-			);
+		if ($status_file !== 0) {
+			Minz_Log::error('File cannot be uploaded. Error code: ' . $status_file);
+			Minz_Request::bad(_t('file_cannot_be_uploaded'),
+			                  array('c' => 'importExport', 'a' => 'index'));
+		}
 
-			// 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']);
+		@set_time_limit(300);
 
-				while (($zipfile = zip_read($zip)) !== false) {
-					$type_zipfile = $this->guessFileType(
-						zip_entry_name($zipfile)
-					);
+		$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
+		$list = array();
+		if ($type_file === 'zip' && extension_loaded('zip')) {
+			$zip = zip_open($file['tmp_name']);
+
+			if (!is_resource($zip)) {
+				// zip_open cannot open file: something is wrong
+				Minz_Log::error('Zip archive cannot be imported. Error code: ' . $zip);
+				Minz_Request::bad(_t('zip_error'),
+				                  array('c' => 'importExport', 'a' => 'index'));
+			}
+
+			while (($zipfile = zip_read($zip)) !== false) {
+				if (!is_resource($zipfile)) {
+					// zip_entry() can also return an error code!
+					Minz_Log::error('Zip file cannot be imported. Error code: ' . $zipfile);
+				} else {
+					$type_zipfile = $this->guessFileType(zip_entry_name($zipfile));
 					if ($type_file !== 'unknown') {
 						$list_files[$type_zipfile][] = zip_entry_read(
 							$zipfile,
@@ -54,59 +72,37 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 						);
 					}
 				}
-
-				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);
+			zip_close($zip);
+		} elseif ($type_file === 'zip') {
+			// Zip extension is not loaded
+			Minz_Request::bad(_t('no_zip_extension'),
+			                  array('c' => 'importExport', 'a' => 'index'));
+		} elseif ($type_file !== 'unknown') {
+			$list_files[$type_file][] = file_get_contents($file['tmp_name']);
+		}
 
-			Minz_Request::forward(array(
-				'c' => 'index',
-				'a' => 'index'
-			), true);
+		// Import file contents.
+		// 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);
 		}
 
-		// What are you doing? you have to call this controller
-		// with a POST request!
-		Minz_Request::forward(array(
-			'c' => 'importExport',
-			'a' => 'index'
-		));
+		// And finally, we get import status and redirect to the home page
+		Minz_Session::_param('actualize_feeds', true);
+		$content_notif = $error === true ? _t('feeds_imported_with_errors') :
+		                                   _t('feeds_imported');
+		Minz_Request::good($content_notif);
 	}
 
 	private function guessFileType($filename) {
@@ -120,7 +116,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		} elseif (substr_compare($filename, '.opml', -5) === 0 ||
 		          substr_compare($filename, '.xml', -4) === 0) {
 			return 'opml';
-		} elseif (strcmp($filename, 'starred.json') === 0) {
+		} elseif (substr_compare($filename, '.json', -5) === 0 &&
+		          strpos($filename, 'starred') !== false) {
 			return 'json_starred';
 		} elseif (substr_compare($filename, '.json', -5) === 0 &&
 		          strpos($filename, 'feed_') === 0) {
@@ -176,15 +173,15 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		}
 
 		// We get different useful information
-		$url = html_chars_utf8($feed_elt['xmlUrl']);
-		$name = html_chars_utf8($feed_elt['text']);
+		$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
+		$name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text']);
 		$website = '';
 		if (isset($feed_elt['htmlUrl'])) {
-			$website = html_chars_utf8($feed_elt['htmlUrl']);
+			$website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl']);
 		}
 		$description = '';
 		if (isset($feed_elt['description'])) {
-			$description = html_chars_utf8($feed_elt['description']);
+			$description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description']);
 		}
 
 		$error = false;
@@ -210,7 +207,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 	private function addCategoryOpml($cat_elt, $parent_cat) {
 		// Create a new Category object
-		$cat = new FreshRSS_Category(html_chars_utf8($cat_elt['text']));
+		$cat = new FreshRSS_Category(Minz_Helper::htmlspecialchars_utf8($cat_elt['text']));
 
 		$id = $this->catDAO->addCategoryObject($cat);
 		$error = ($id === false);
@@ -287,7 +284,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$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);
@@ -311,61 +308,53 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	}
 
 	public function exportAction() {
-		if (Minz_Request::isPost()) {
-			$this->view->_useLayout(false);
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+		}
 
-			$export_opml = Minz_Request::param('export_opml', false);
-			$export_starred = Minz_Request::param('export_starred', false);
-			$export_feeds = Minz_Request::param('export_feeds', array ());
+		$this->view->_useLayout(false);
 
-			$export_files = array ();
-			if ($export_opml) {
-				$export_files['feeds.opml'] = $this->generateOpml();
-			}
+		$export_opml = Minz_Request::param('export_opml', false);
+		$export_starred = Minz_Request::param('export_starred', false);
+		$export_feeds = Minz_Request::param('export_feeds', array());
 
-			if ($export_starred) {
-				$export_files['starred.json'] = $this->generateArticles('starred');
-			}
+		$export_files = array();
+		if ($export_opml) {
+			$export_files['feeds.opml'] = $this->generateOpml();
+		}
 
-			foreach ($export_feeds as $feed_id) {
-				$feed = $this->feedDAO->searchById($feed_id);
-				if ($feed) {
-					$filename = 'feed_' . $feed->category() . '_'
-					          . $feed->id() . '.json';
-					$export_files[$filename] = $this->generateArticles(
-						'feed', $feed
-					);
-				}
-			}
+		if ($export_starred) {
+			$export_files['starred.json'] = $this->generateArticles('starred');
+		}
 
-			$nb_files = count($export_files);
-			if ($nb_files > 1) {
-				// If there are more than 1 file to export, we need an .zip
-				try {
-					$this->exportZip($export_files);
-				} catch (Exception $e) {
-					# Oops, there is no Zip extension!
-					$notif = array(
-						'type' => 'bad',
-						'content' => _t('export_no_zip_extension')
-					);
-					Minz_Session::_param('notification', $notif);
-					Minz_Request::forward(array('c' => 'importExport'), true);
-				}
-			} elseif ($nb_files === 1) {
-				// Only one file? Guess its type and export it.
-				$filename = key($export_files);
-				$type = null;
-				if (substr_compare($filename, '.opml', -5) === 0) {
-					$type = "text/xml";
-				} elseif (substr_compare($filename, '.json', -5) === 0) {
-					$type = "text/json";
-				}
+		foreach ($export_feeds as $feed_id) {
+			$feed = $this->feedDAO->searchById($feed_id);
+			if ($feed) {
+				$filename = 'feed_' . $feed->category() . '_'
+				          . $feed->id() . '.json';
+				$export_files[$filename] = $this->generateArticles(
+					'feed', $feed
+				);
+			}
+		}
 
-				$this->exportFile($filename, $export_files[$filename], $type);
-			} else {
-				Minz_Request::forward(array('c' => 'importExport'), true);
+		$nb_files = count($export_files);
+		if ($nb_files > 1) {
+			// If there are more than 1 file to export, we need a zip archive.
+			try {
+				$this->exportZip($export_files);
+			} catch (Exception $e) {
+				# Oops, there is no Zip extension!
+				Minz_Request::bad(_t('export_no_zip_extension'),
+				                  array('c' => 'importExport', 'a' => 'index'));
 			}
+		} elseif ($nb_files === 1) {
+			// Only one file? Guess its type and export it.
+			$filename = key($export_files);
+			$type = $this->guessFileType($filename);
+			$this->exportFile('freshrss_' . $filename, $export_files[$filename], $type);
+		} else {
+			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
 		}
 	}
 
@@ -384,7 +373,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$this->view->categories = $this->catDAO->listCategories();
 
 		if ($type == 'starred') {
-			$this->view->list_title = Minz_Translate::t('starred_list');
+			$this->view->list_title = _t('starred_list');
 			$this->view->type = 'starred';
 			$unread_fav = $this->entryDAO->countUnreadReadFavorites();
 			$this->view->entries = $this->entryDAO->listWhere(
@@ -392,9 +381,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 				$unread_fav['all']
 			);
 		} elseif ($type == 'feed' && !is_null($feed)) {
-			$this->view->list_title = Minz_Translate::t(
-				'feed_list', $feed->name()
-			);
+			$this->view->list_title = _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',
@@ -430,11 +417,18 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	}
 
 	private function exportFile($filename, $content, $type) {
-		if (is_null($type)) {
+		if ($type === 'unknown') {
 			return;
 		}
 
-		header('Content-Type: ' . $type . '; charset=utf-8');
+		$content_type = '';
+		if ($type === 'opml') {
+			$content_type = "text/opml";
+		} elseif ($type === 'json_feed' || $type === 'json_starred') {
+			$content_type = "text/json";
+		}
+
+		header('Content-Type: ' . $content_type . '; charset=utf-8');
 		header('Content-disposition: attachment; filename=' . $filename);
 		print($content);
 	}

+ 47 - 5
app/Controllers/indexController.php

@@ -76,14 +76,14 @@ 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);
+		$this->view->state = Minz_Request::param('state', $this->view->conf->default_view);
 		$state_param = Minz_Request::param ('state', null);
 		$filter = Minz_Request::param ('search', '');
 		$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 === FreshRSS_Entry::STATE_NOT_READ) {	//Any unread article in this category at all?
+		if ($this->view->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;
@@ -104,7 +104,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 					break;
 			}
 			if (!$hasUnread && ($state_param === null)) {
-				$this->view->state = $state = FreshRSS_Entry::STATE_ALL;
+				$this->view->state = FreshRSS_Entry::STATE_ALL;
 			}
 		}
 
@@ -117,11 +117,11 @@ 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, true, $keepHistoryDefault);
+			$entries = $entryDAO->listWhere($getType, $getId, $this->view->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 === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) {
+			if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) {
 				Minz_Log::record('Conflicting information about nbNotRead!', Minz_Log::DEBUG);
 				$feedDAO = FreshRSS_Factory::createFeedDao();
 				try {
@@ -132,6 +132,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 				$this->view->state = FreshRSS_Entry::STATE_ALL;
 				$entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter, $date_min, true, $keepHistoryDefault);
 			}
+			Minz_Request::_param('state', $this->view->state);
 
 			if (count($entries) <= $nb) {
 				$this->view->nextId  = '';
@@ -295,6 +296,41 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		Minz_Session::_param('passwordHash');
 	}
 
+	private static function makeLongTermCookie($username, $passwordHash) {
+		do {
+			$token = sha1(Minz_Configuration::salt() . $username . uniqid(mt_rand(), true));
+			$tokenFile = DATA_PATH . '/tokens/' . $token . '.txt';
+		} while (file_exists($tokenFile));
+		if (@file_put_contents($tokenFile, $username . "\t" . $passwordHash) === false) {
+			return false;
+		}
+		$expire = time() + 2629744;	//1 month	//TODO: Use a configuration instead
+		Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
+		Minz_Session::_param('token', $token);
+		return $token;
+	}
+
+	private static function deleteLongTermCookie() {
+		Minz_Session::deleteLongTermCookie('FreshRSS_login');
+		$token = Minz_Session::param('token', null);
+		if (ctype_alnum($token)) {
+			@unlink(DATA_PATH . '/tokens/' . $token . '.txt');
+		}
+		Minz_Session::_param('token');
+		if (rand(0, 10) === 1) {
+			self::purgeTokens();
+		}
+	}
+
+	private static function purgeTokens() {
+		$oldest = time() - 2629744;	//1 month	//TODO: Use a configuration instead
+		foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $fileInfo) {
+			if ($fileInfo->getExtension() === 'txt' && $fileInfo->getMTime() < $oldest) {
+				@unlink($fileInfo->getPathname());
+			}
+		}
+	}
+
 	public function formLoginAction () {
 		if (Minz_Request::isPost()) {
 			$ok = false;
@@ -312,6 +348,11 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 					if ($ok) {
 						Minz_Session::_param('currentUser', $username);
 						Minz_Session::_param('passwordHash', $s);
+						if (Minz_Request::param('keep_logged_in', false)) {
+							self::makeLongTermCookie($username, $s);
+						} else {
+							self::deleteLongTermCookie();
+						}
 					} else {
 						Minz_Log::record('Password mismatch for user ' . $username . ', nonce=' . $nonce . ', c=' . $c, Minz_Log::WARNING);
 					}
@@ -371,6 +412,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		Minz_Session::_param('currentUser');
 		Minz_Session::_param('mail');
 		Minz_Session::_param('passwordHash');
+		self::deleteLongTermCookie();
 		Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
 	}
 }

+ 5 - 0
app/Controllers/statsController.php

@@ -58,15 +58,20 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 
 	public function repartitionAction() {
 		$statsDAO = FreshRSS_Factory::createStatsDAO();
+		$categoryDAO = new FreshRSS_CategoryDAO();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
 		$id = Minz_Request::param ('id', null);
+		$this->view->categories = $categoryDAO->listCategories();
 		$this->view->feed = $feedDAO->searchById($id);
 		$this->view->days = $statsDAO->getDays();
 		$this->view->months = $statsDAO->getMonths();
 		$this->view->repartitionHour = $statsDAO->calculateEntryRepartitionPerFeedPerHour($id);
+		$this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id);
 		$this->view->repartitionDayOfWeek = $statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id);
+		$this->view->averageDayOfWeek = $statsDAO->calculateEntryAveragePerFeedPerDayOfWeek($id);
 		$this->view->repartitionMonth = $statsDAO->calculateEntryRepartitionPerFeedPerMonth($id);
+		$this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id);
 	}
 
 	public function firstAction() {

+ 26 - 5
app/FreshRSS.php

@@ -6,8 +6,7 @@ class FreshRSS extends Minz_FrontController {
 		}
 		$loginOk = $this->accessControl(Minz_Session::param('currentUser', ''));
 		$this->loadParamsView();
-		if (Minz_Request::isPost() && (empty($_SERVER['HTTP_REFERER']) ||
-			Minz_Request::getDomainName() !== parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST))) {
+		if (Minz_Request::isPost() && !Minz_Request::isRefererFromSameDomain()) {
 			$loginOk = false;	//Basic protection against XSRF attacks
 			Minz_Error::error(
 				403,
@@ -20,13 +19,35 @@ class FreshRSS extends Minz_FrontController {
 		$this->loadNotifications();
 	}
 
+	private static function getCredentialsFromLongTermCookie() {
+		$token = Minz_Session::getLongTermCookie('FreshRSS_login');
+		if (!ctype_alnum($token)) {
+			return array();
+		}
+		$tokenFile = DATA_PATH . '/tokens/' . $token . '.txt';
+		$mtime = @filemtime($tokenFile);
+		if ($mtime + 2629744 < time()) {	//1 month	//TODO: Use a configuration instead
+			@unlink($tokenFile);
+			return array(); 	//Expired or token does not exist
+		}
+		$credentials = @file_get_contents($tokenFile);
+		return $credentials === false ? array() : explode("\t", $credentials, 2);
+	}
+
 	private function accessControl($currentUser) {
 		if ($currentUser == '') {
 			switch (Minz_Configuration::authType()) {
 				case 'form':
-					$currentUser = Minz_Configuration::defaultUser();
-					Minz_Session::_param('passwordHash');
-					$loginOk = false;
+					$credentials = self::getCredentialsFromLongTermCookie();
+					if (isset($credentials[1])) {
+						$currentUser = trim($credentials[0]);
+						Minz_Session::_param('passwordHash', trim($credentials[1]));
+					}
+					$loginOk = $currentUser != '';
+					if (!$loginOk) {
+						$currentUser = Minz_Configuration::defaultUser();
+						Minz_Session::_param('passwordHash');
+					}
 					break;
 				case 'http_auth':
 					$currentUser = httpAuthUser();

+ 7 - 0
app/Models/Configuration.php

@@ -17,6 +17,7 @@ class FreshRSS_Configuration {
 		'default_view' => FreshRSS_Entry::STATE_NOT_READ,
 		'auto_load_more' => true,
 		'display_posts' => false,
+		'display_categories' => false,
 		'hide_read_feeds' => true,
 		'onread_jump_next' => true,
 		'lazyload' => true,
@@ -44,6 +45,8 @@ class FreshRSS_Configuration {
 			'load_more' => 'm',
 			'auto_share' => 's',
 			'focus_search' => 'a',
+			'user_filter' => 'u',
+			'help' => 'f1',
 		),
 		'topline_read' => true,
 		'topline_favorite' => true,
@@ -60,6 +63,7 @@ class FreshRSS_Configuration {
 	);
 
 	private $available_languages = array(
+		'de' => 'Deutsch',
 		'en' => 'English',
 		'fr' => 'Français',
 	);
@@ -142,6 +146,9 @@ class FreshRSS_Configuration {
 	public function _display_posts ($value) {
 		$this->data['display_posts'] = ((bool)$value) && $value !== 'no';
 	}
+	public function _display_categories ($value) {
+		$this->data['display_categories'] = ((bool)$value) && $value !== 'no';
+	}
 	public function _hide_read_feeds($value) {
 		$this->data['hide_read_feeds'] = (bool)$value;
 	}

+ 2 - 2
app/Models/EntryDAO.php

@@ -65,7 +65,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		}
 
 		if (!isset($existingGuids[$entry->guid()]) &&
-				($feedHistory != 0 || $eDate  >= $date_min)) {
+				($feedHistory != 0 || $eDate >= $date_min || $entry->isFavorite())) {
 			$values = $entry->toArray();
 
 			$useDeclaredDate = empty($existingGuids);
@@ -230,7 +230,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		}
 		$this->bd->beginTransaction();
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry`  '
+		$sql = 'UPDATE `' . $this->prefix . 'entry` '
 			 . 'SET is_read=1 '
 			 . 'WHERE id_feed=? AND is_read=0 AND id <= ?';
 		$values = array($id, $idMax);

+ 6 - 0
app/Models/Feed.php

@@ -28,6 +28,12 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 	}
 
+	public static function example() {
+		$f = new FreshRSS_Feed('http://example.net/', false);
+		$f->faviconPrepare();
+		return $f;
+	}
+
 	public function id() {
 		return $this->id;
 	}

+ 62 - 0
app/Models/StatsDAO.php

@@ -151,6 +151,68 @@ SQL;
 		return $this->convertToSerie($repartition);
 	}
 
+	/**
+	 * Calculates the average number of article per hour per feed
+	 *
+	 * @param integer $feed id
+	 * @return integer
+	 */
+	public function calculateEntryAveragePerFeedPerHour($feed = null) {
+		return $this->calculateEntryAveragePerFeedPerPeriod(1/24, $feed);
+	}
+	
+	/**
+	 * Calculates the average number of article per day of week per feed
+	 *
+	 * @param integer $feed id
+	 * @return integer
+	 */
+	public function calculateEntryAveragePerFeedPerDayOfWeek($feed = null) {
+		return $this->calculateEntryAveragePerFeedPerPeriod(7, $feed);
+	}
+
+	/**
+	 * Calculates the average number of article per month per feed
+	 *
+	 * @param integer $feed id
+	 * @return integer
+	 */
+	public function calculateEntryAveragePerFeedPerMonth($feed = null) {
+		return $this->calculateEntryAveragePerFeedPerPeriod(30, $feed);
+	}
+	
+	/**
+	 * Calculates the average number of article per feed
+	 * 
+	 * @param float $period number used to divide the number of day in the period
+	 * @param integer $feed id
+	 * @return integer
+	 */
+	protected function calculateEntryAveragePerFeedPerPeriod($period, $feed = null) {
+		if ($feed) {
+			$restrict = "WHERE e.id_feed = {$feed}";
+		} else {
+			$restrict = '';
+		}
+		$sql = <<<SQL
+SELECT COUNT(1) AS count
+, MIN(date) AS date_min
+, MAX(date) AS date_max
+FROM {$this->prefix}entry AS e
+{$restrict}
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetch(PDO::FETCH_NAMED);
+		$date_min = new \DateTime();
+		$date_min->setTimestamp($res['date_min']);
+		$date_max = new \DateTime();
+		$date_max->setTimestamp($res['date_max']);
+		$interval = $date_max->diff($date_min, true);
+
+		return round($res['count'] / ($interval->format('%a') / ($period)), 2);
+	}
+
 	/**
 	 * Initialize an array for statistics depending on a range
 	 *

+ 326 - 0
app/i18n/de.php

@@ -0,0 +1,326 @@
+<?php
+
+return array (
+	// LAYOUT
+	'login'				=> 'Login',
+	'login_with_persona'		=> 'Login mit Persona',
+	'logout'			=> 'Logout',
+	'search'			=> 'Suche nach Worten oder #tags',
+	'search_short'			=> 'Suche',
+
+	'configuration'			=> 'Konfiguration',
+	'users'				=> 'Nutzer',
+	'categories'			=> 'Kategorien',
+	'category'			=> 'Kategorie',
+	'feed'				=> 'Feed',
+	'feeds'				=> 'Feeds',
+	'shortcuts'			=> 'Shortcuts',
+	'about'				=> '&Uuml;ber',
+	'stats'				=> 'Statistiken',
+
+	'your_rss_feeds'		=> 'Ihre RSS Feeds',
+	'add_rss_feed'			=> 'RSS-Feed hinzuf&uuml;gen',
+	'no_rss_feed'			=> 'Kein RSS Feed',
+	'import_export_opml'		=> 'Import / Export (OPML)',
+
+	'subscription_management'	=> 'Abonnementsverwaltung',
+	'main_stream'			=> 'Haupt-Nachrichtenflu&szlig;',
+	'all_feeds'			=> 'Alle Feeds',
+	'favorite_feeds'		=> 'Favoriten (%d)',
+	'not_read'			=> '%d ungelesen',
+	'not_reads'			=> '%d ungelesen',
+
+	'filter'			=> 'Filter',
+	'see_website'			=> 'Website ansehen',
+	'administration'		=> 'Verwaltung',
+	'actualize'			=> 'Aktualisierung',
+
+	'mark_read'			=> 'Als gelesen markieren',
+	'mark_favorite'			=> 'Als Favoriten markieren',
+	'mark_all_read'			=> 'Alle als gelesen markieren',
+	'mark_feed_read'		=> 'Feed als gelesen markieren',
+	'mark_cat_read'			=> 'Kategorie als gelesen markieren',
+	'before_one_day'		=> 'Vor einem Tag',
+	'before_one_week'		=> 'Vor einer Woche',
+	'display'			    => 'Anzeige',
+	'normal_view'			=> 'Normale Anzeige',
+	'reader_view'			=> 'Leseanzeige-Modus',
+	'global_view'			=> 'Globale Anzeige',
+	'rss_view'			    => 'RSS-Feed',
+	'show_all_articles'		=> 'zeige alle Artikel',
+	'show_not_reads'		=> 'zeige nicht gelesene',
+	'show_read'			    => 'zeige nur gelesene',
+	'show_favorite'			=> 'Favoriten anzeigen',
+	'older_first'			=> '&Auml;lteste zuerst',
+	'newer_first'			=> 'Neuere zuerst',
+
+	// Pagination
+	'first'				=> 'Erste',
+	'previous'			=> 'Vorherige',
+	'next'				=> 'N&auml;chste',
+	'last'				=> 'Letzte',
+
+	// CONTROLLERS
+	'article_published_on'		=> 'Dieser Artikel erschien im Original bei <a href="%s">%s</a>',
+	'article_published_on_author'	=> 'Dieser Artikel erschien im Original bei <a href="%s">%s</a> von %s',
+
+	'access_denied'			=> 'Sie haben nicht die Berechtigung, diese Seite aufzurufen',
+	'page_not_found'		=> 'Sie suchen nach einer Seite, die es nicht gibt',
+	'error_occurred'		=> 'Es gab einen Fehler',
+	'error_occurred_update'	=> 'Es wurde nichts ge&auml;ndert',
+
+	'default_category'		=> 'Unkategorisiert',
+	'categories_updated'		=> 'Kategorien wurden aktualisiert',
+	'categories_management'		=> 'Kategorienverwaltung',
+	'feed_updated'			=> 'Der Feed wurde aktualisiert',
+	'rss_feed_management'		=> 'Verwaltung der RSS Feeds',
+	'configuration_updated'		=> 'Die Konfiguration wurde aktualisiert',
+	'sharing_management'		=> 'Verwaltung der Optionen f&uuml;r das Teilen',
+	'bad_opml_file'			=> 'Ihre OPML-Datei ist ung&uuml;ltig',
+	'shortcuts_updated'		=> 'Shortcuts wurden aktualisiert',
+	'shortcuts_management'		=> 'Verwaltung der Shortcuts',
+	'shortcuts_navigation'		=> 'Navigation',
+	'shortcuts_navigation_help'	=> 'Mit der "Shift" Taste gelten die Navigations-Shortcuts f&uuml;r Feeds.<br/>Mit der "Alt" Taste gelten die Navigations-Shortcuts f&uuml;r Kategorien.',
+	'shortcuts_article_action'	=> 'Artikelaktionen',
+	'shortcuts_other_action'	=> 'Andere Aktionen',
+	'feeds_marked_read'		=> 'Die Feeds wurden als gelesen markiert',
+	'updated'			=> 'Die &Auml;nderungen wurden aktualisiert',
+
+	'already_subscribed'		=> 'Sie haben bereits <em>%s</em> abonniert',
+	'feed_added'			=> 'Der RSS Feed <em>%s</em> wurde hinzugef&uuml;gt',
+	'feed_not_added'		=> '<em>%s</em> konnte nicht hinzugef&uuml;gt werden',
+	'internal_problem_feed'		=> 'Der RSS Feed konnte nicht hinzugef&uuml;gt werden. &uuml;berpr&uuml;fen Sie die Protokolldateien von FressRSS f&uuml;r weitere Informationen.',
+	'invalid_url'			=> 'URL <em>%s</em> ist ung&uuml;ltig',
+	'feed_actualized'		=> '<em>%s</em> wurde aktualisiert',
+	'n_feeds_actualized'		=> '%d Feeds wurden aktualisiert',
+	'feeds_actualized'		=> 'RSS Feeds wurden aktualisiert',
+	'no_feed_actualized'		=> 'Es wurden keine RSS Feeds aktualisiert',
+	'n_entries_deleted'		=> '%d Artikel wurden gel&ouml;scht',
+	'feeds_imported_with_errors'	=> 'Ihre Feeds wurden importiert, es gab aber einige Fehler',
+	'feeds_imported'		=> 'Ihre Feeds wurden importiert und werden jetzt aktualisiert',
+	'category_emptied'		=> 'Die Kategorie wurde geleert',
+	'feed_deleted'			=> 'Der Feed wurde gel&ouml;scht',
+	'feed_validator'		=> '&Üuml;berpr&uuml;fen Sie die G&uuml;ltigkeit des Feeds',
+
+	'optimization_complete'		=> 'Die Optimierung ist beendet',
+
+	'your_rss_feeds'		=> 'Ihre RSS Feeds',
+	'your_favorites'		=> 'Ihre Favoriten',
+	'public'			=> '&Ouml;ffentlich',
+	'invalid_login'			=> 'Das Login ist ung&uuml;ltig',
+
+	// VIEWS
+	'save'				=> 'Speichern',
+	'delete'			=> 'L&ouml;schen',
+	'cancel'			=> 'Abbrechen',
+
+	'back_to_rss_feeds'		=> '← Zur&uuml;ck zu den RSS Feeds gehen',
+	'feeds_moved_category_deleted'	=> 'Wenn Sie eine Kategorie l&ouml;schen, werden deren Feeds automatisch in die Kategorie <em>%s</em> eingef&uuml;gt.',
+	'category_number'		=> 'Kategorie n°%d',
+	'ask_empty'			=> 'Leeren?',
+	'number_feeds'			=> '%d Feeds',
+	'can_not_be_deleted'		=> 'Kann nicht gel&ouml;scht werden',
+	'add_category'			=> 'F&uuml;ge eine Kategorie hinzu',
+	'new_category'			=> 'Neue Kategorie',
+
+	'javascript_for_shortcuts'	=> 'JavaScript muss erm&ouml;glicht werden, wenn Shortcuts verwendet werden sollen',
+	'javascript_should_be_activated'=> 'JavaScript muss erm&ouml;glicht werden',
+	'shift_for_all_read'		=> '+ <code>shift</code> um alle Artikel als gelesen zu markieren',
+	'see_on_website'		=> 'Auf der Originalwebseite anschauen',
+	'next_article'			=> 'Zum n&auml;chsten Artikel springen',
+	'last_article'			=> 'Zum letzten Artikel springen',
+	'previous_article'		=> 'Zum vorherigen Artikel springen',
+	'first_article'			=> 'Zum ersten Artikel springen',
+	'next_page'			=> 'Zur n&auml;chsten Seite springen',
+	'previous_page'			=> 'Zur vorherigen Seite springen',
+	'collapse_article'		=> 'Zusammenfalten',
+	'auto_share'			=> 'Teilen',
+	'auto_share_help'		=> 'Wenn es nur eine Option zum Teilen gibt, wird die verwendet. Ansonsten werden die Optionen &uuml;ber die Nummer ausgew&auml;hlt.',
+
+	'file_to_import'		=> 'Datei zum importieren',
+	'import'			=> 'Import',
+	'export'			=> 'Export',
+	'or'				=> 'oder',
+
+	'informations'			=> 'Information',
+	'damn'				=> 'Verdammt!',
+	'feed_in_error'			=> 'Dieser Feed hat ein Problem verursacht. Bitte stellen Sie sicher, dass er immer lesbar ist und aktualisieren Sie ihn dann.',
+	'feed_empty'			=> 'Dieser Feed ist leer. Bitte stellen Sie sicher, dass er noch gepflegt wird.',
+	'feed_description'		=> 'Beschreibung',
+	'website_url'			=> 'Webseiten-Adresse URL',
+	'feed_url'			=> 'Feed URL',
+	'articles'			=> 'Artikel',
+	'number_articles'		=> 'Anzahl der Artikel',
+	'by_feed'			=> 'per Feed',
+	'by_default'			=> 'Als Vorgabe',
+	'keep_history'			=> 'Kleinste Anzahl der Artikel, die behalten werden',
+	'categorize'			=> 'In einer Kategorie speichern',
+	'truncate'			=> 'Alle Artikel l&ouml;schen',
+	'advanced'			=> 'Erweitert',
+	'show_in_all_flux'		=> 'Im Hauptstrom anzeigen',
+	'yes'				=> 'Ja',
+	'no'				=> 'Nein',
+	'css_path_on_website'		=> 'Pfad zur CSS-Datei des Artikels auf der Original Webseite',
+	'retrieve_truncated_feeds'	=> 'Gek&uuml;rzte RSS Feeds abrufen (Achtung, ben&ouml;tigt mehr Zeit!)',
+	'http_authentication'		=> 'HTTP Authentifizierung',
+	'http_username'			=> 'HTTP Nutzername',
+	'http_password'			=> 'HTTP Passwort',
+	'blank_to_disable'		=> 'Zum Ausschalten frei lassen',
+	'not_yet_implemented'		=> 'Noch nicht implementiert',
+	'access_protected_feeds'	=> 'Die Verbindung erlaubt Zugriff zu HTTP-gesch&uuml;tzten RSS Feeds',
+	'no_selected_feed'		=> 'Kein Feed ausgew&auml;hlt.',
+	'think_to_add'			=> '<a href="./?c=configure&amp;a=feed">Sie k&ouml;nnen Feeds hinzuf&uuml;gen</a>.',
+
+	'current_user'			=> 'Aktuelle Nutzung',
+	'default_user'			=> 'Nutzername des Standardnutzers <small>(maximal 16 Zeichen - alphanumerisch)</small>',
+	'password_form'			=> 'Passwort<br /><small>(f&uuml;r die Anmeldemethode per Webformular)</small>',
+	'persona_connection_email'	=> 'Login E-Mail Adresse<br /><small>(f&uuml;r <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
+	'allow_anonymous'		=> 'Anonymes lesen der Artikel des Standardnutzers (%s) wird erlaubt',
+	'allow_anonymous_refresh'	=> 'Aktualisieren der Artikel wird anonymen Nutzern erlaubt',
+	'auth_token'			=> 'Authentifizierungs-Token',
+	'explain_token'			=> 'Erlaube den Zugriff auf die RSS-Ausgabe des Standardnutzers ohne Authentifizierung.<br /><kbd>%s?output=rss&token=%s</kbd>',
+	'login_configuration'		=> 'Login',
+	'is_admin'			=> 'ist Administrator',
+	'auth_type'			=> 'Authentifizierungsmethode',
+	'auth_none'			=> 'Keine (gef&auml;hrlich)',
+	'auth_form'			=> 'Webformular (traditionell, JavaScript wird ben&ouml;tigt)',
+	'http_auth'			=> 'HTTP (mit HTTPS f&uuml;r erfahrene Nutzer)',
+	'auth_persona'			=> 'Mozilla Persona (modern, JavaScript wird ben&ouml;tigt)',
+	'users_list'			=> 'Liste der Nutzer',
+	'create_user'			=> 'Neuen Nutzer erstellen',
+	'username'			=> 'Nutzername',
+	'password'			=> 'Passwort',
+	'create'			=> 'Erstellen',
+	'user_created'			=> 'Nutzer %s wurde erstellt',
+	'user_deleted'			=> 'Nutzer %s wurde gel&ouml;scht',
+
+	'language'			=> 'Sprache',
+	'month'				=> 'Monate',
+	'archiving_configuration'	=> 'Archivieren',
+	'delete_articles_every'	=> 'Entfernen von Artikeln nach',
+	'purge_now'			=> 'Jetzt bereinigen',
+	'purge_completed'		=> 'Die Bereinigung ist abgeschlossen (%d Artikel wurden gel&ouml;scht)',
+	'archiving_configuration_help'	=> 'Es gibt weitere Optionen bei den Einstellungen der individuellen Nachrichtenstr&ouml;me',
+	'reading_configuration'		=> 'Lesen',
+	'articles_per_page'		=> 'Anzahl der Artikel pro Seite',
+	'default_view'			=> 'Standard-Ansicht',
+	'sort_order'			=> 'Sortierreihenfolge',
+	'auto_load_more'		=> 'Die n&auml;chsten Artikel am Seitenende laden',
+	'display_articles_unfolded'	=> 'Die Artikel als Standard zusammen gefaltet anzeigen',
+	'after_onread'			=> 'Nach “als gelesen markieren”',
+	'jump_next'			=> 'springe zum n&auml;chsten ungelesenen Geschwisterelement (Feed oder Kategorie)',
+	'reading_icons'			=> 'Lese Symbol',
+	'top_line'			=> 'Kopfzeile',
+	'bottom_line'			=> 'Fusszeile',
+	'img_with_lazyload'		=> 'Verwende die "tr&auml;ge laden" Methode zum laden von Bildern',
+	'auto_read_when'		=> 'Artikel als gelesen markieren…',
+	'article_selected'		=> 'wenn der Artikel ausgew&auml;hlt ist',
+	'article_open_on_website'	=> 'wenn der Artikel auf der Originalwebseite ge&ouml;ffnet ist',
+	'scroll'			=> 'w&auml;hrend des Seiten-Scrollens',
+	'upon_reception'		=> 'beim Empfang des Artikels',
+	'your_shaarli'			=> 'Ihr Shaarli',
+	'your_wallabag'			=> 'Ihr wallabag',
+	'your_diaspora_pod'		=> 'Ihr Diaspora* pod',
+	'sharing'			=> 'Teilen',
+	'share'				=> 'teile',
+	'by_email'			=> 'Per E-Mail',
+	'optimize_bdd'			=> 'Datenbank optimieren',
+	'optimize_todo_sometimes'	=> 'Sollte gelegentlich gemacht werden, um die Gr&ouml;ße der Datenbank zu reduzieren',
+	'theme'				=> 'Thema',
+	'more_information'		=> 'Weitere Informationen',
+	'activate_sharing'		=> 'Teilen aktivieren',
+	'shaarli'			=> 'Shaarli',
+	'wallabag'			=> 'wallabag',
+	'diaspora'			=> 'Diaspora*',
+	'twitter'			=> 'Twitter',
+	'g+'				=> 'Google+',
+	'facebook'			=> 'Facebook',
+	'email'				=> 'E-Mail',
+	'print'				=> 'Drucken',
+
+	'article'			=> 'Artikel',
+	'title'				=> 'Titel',
+	'author'			=> 'Autor',
+	'publication_date'		=> 'Datum der Ver&ouml;ffentlichung',
+	'by'				=> 'von',
+
+	'load_more'			=> 'Weitere Artikel laden',
+	'nothing_to_load'		=> 'Es gibt keine weiteren Artikel',
+
+	'rss_feeds_of'			=> 'RSS Feed von %s',
+
+	'refresh'			=> 'Aktualisieren',
+	'no_feed_to_refresh'		=> 'Es gibt keinen Feed zum aktualisieren',
+
+	'today'				=> 'Heute',
+	'yesterday'			=> 'Gestern',
+	'before_yesterday'		=> 'vor Gestern',
+	'new_article'			=> 'Es gibt neue Artikel. Bitte klicken Sie hier, um die Seite erneut zu laden.',
+	'by_author'			=> 'Von <em>%s</em>',
+	'related_tags'			=> 'Verwandte tags',
+	'no_feed_to_display'		=> 'Es gibt keinen Artikel zum anzeigen.',
+
+	'about_freshrss'		=> '&Uuml;ber FreshRSS',
+	'project_website'		=> 'Projekt Webseite',
+	'lead_developer'		=> 'Hauptentwickler',
+	'website'			=> 'Webseite',
+	'bugs_reports'			=> 'Fehlerberichte',
+	'github_or_email'		=> '<a href="https://github.com/marienfressinaud/FreshRSS/issues">auf Github</a> oder <a href="mailto:dev@marienfressinaud.fr">per Mail</a>',
+	'license'			=> 'Lizenz',
+	'agpl3'				=> '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>',
+	'freshrss_description'		=> 'FreshRSS ist ein RSS Feedsaggregator zum selbst hosten wie zum Beispiel <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> oder <a href="http://projet.idleman.fr/leed/">Leed</a>. Es ist leicht und einfach zu handhaben und gleichzeitig ein leistungsstark und konfigurierbares Werkzeug.',
+	'credits'			=> 'Credits',
+	'credits_content'		=> 'Einige Designelemente sind von <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> obwohl FreshRSS dieses Framework nicht nutzt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> sind vom <a href="https://www.gnome.org/">GNOME Projekt</a>. <em>Open Sans</em> Font police wurde von <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson erstellt</a>. Favicons wurden mit <a href="https://getfavicon.appspot.com/">getFavicon API gesammelt</a>. FreshRSS basiert auf <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, einem PHP Framework.',
+	'version'			=> 'Version',
+
+	'logs'				=> 'Protokolle',
+	'logs_empty'			=> 'Die Protokolldatei ist leer',
+	'clear_logs'			=> 'Protokolldateien leeren',
+
+	'forbidden_access'		=> 'Der Zugriff ist verboten!',
+	'login_required'		=> 'Das Login ist n&ouml;tig:',
+
+	'confirm_action'		=> 'Sind Sie sicher, dass Sie diese Aktion durchf&uuml;hren wollen? Die Aktion kann nicht abgebrochen werden!',
+
+	// DATE
+	'january'			=> 'januar',
+	'february'			=> 'februar',
+	'march'				=> 'm&auml;rz',
+	'april'				=> 'april',
+	'may'				=> 'mai',
+	'june'				=> 'juni',
+	'july'				=> 'juli',
+	'august'			=> 'august',
+	'september'			=> 'september',
+	'october'			=> 'oktober',
+	'november'			=> 'november',
+	'december'			=> 'dezember',
+	// special format for date() function
+	'Jan'				=> '\J\a\n\u\a\r',
+	'Feb'				=> '\F\e\b\r\u\a\r',
+	'Mar'				=> '\M\a\e\r\z',
+	'Apr'				=> '\A\p\r\i\l',
+	'May'				=> '\M\a\i',
+	'Jun'				=> '\J\u\n\i',
+	'Jul'				=> '\J\u\l\i',
+	'Aug'				=> '\A\u\g\u\s\t',
+	'Sep'				=> '\S\e\p\t\e\m\b\e\r',
+	'Oct'				=> '\O\k\t\o\b\e\r',
+	'Nov'				=> '\N\o\v\e\m\b\e\r',
+	'Dec'				=> '\D\e\z\e\m\b\e\r',
+	// format for date() function, %s allows to indicate month in letter
+	'format_date'			=> 'd\.\ %s  Y',
+	'format_date_hour'		=> 'd\.\ %s  Y \u\m H\:i',
+	
+	'status_favorites'		=> 'Favoriten',
+	'status_read'			=> 'Gelesen',
+	'status_unread'			=> 'Ungelesen',
+	'status_total'			=> 'Gesamt',
+	
+	'stats_entry_repartition'	=> 'Verteilung der Eintr&auml;ge',
+	'stats_entry_per_day'		=> 'Eintr&auml;ge pro Tag (w&auml;hrend der letzten 30 Tage)',
+	'stats_feed_per_category'	=> 'Feeds pro Kategorie',
+	'stats_entry_per_category'	=> 'Eintr&auml;ge pro Kategorie',
+	'stats_top_feed'		=> 'Top 10 Feeds',
+	'stats_entry_count'		=> 'Z&auml;hler f&uuml;r Eintr&auml;ge',
+);

+ 9 - 0
app/i18n/en.php

@@ -3,6 +3,7 @@
 return array (
 	// LAYOUT
 	'login'				=> 'Login',
+	'keep_logged_in'		=> 'Keep me logged in <small>(1 month)</small>',
 	'login_with_persona'		=> 'Login with Persona',
 	'logout'			=> 'Logout',
 	'search'			=> 'Search words or #tags',
@@ -179,9 +180,16 @@ return array (
 	'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',
+	'user_filter'			=> 'Access user filters',
+	'user_filter_help'		=> 'If there is only one user filter, it is used. Else filters are accessible by their number.',
+	'help'				=> 'Display documentation',
 
 	'file_to_import'		=> 'File to import<br />(OPML, Json or Zip)',
+	'file_to_import_no_zip'		=> 'File to import<br />(OPML or Json)',
 	'import'			=> 'Import',
+	'file_cannot_be_uploaded'	=> 'File cannot be uploaded!',
+	'zip_error'			=> 'An error occured during Zip import.',
+	'no_zip_extension'		=> 'Zip extension is not present on your server.',
 	'export'			=> 'Export',
 	'export_opml'			=> 'Export list of feeds (OPML)',
 	'export_starred'		=> 'Export your favourites',
@@ -263,6 +271,7 @@ return array (
 	'sort_order'			=> 'Sort order',
 	'auto_load_more'		=> 'Load next articles at the page bottom',
 	'display_articles_unfolded'	=> 'Show articles unfolded by default',
+	'display_categories_unfolded'	=> 'Show categories folded by default',
 	'hide_read_feeds'		=> 'Hide categories &amp; feeds with no unread article (only in “unread articles” display mode)',
 	'after_onread'			=> 'After “mark all as read”,',
 	'jump_next'			=> 'jump to next unread sibling (feed or category)',

+ 9 - 0
app/i18n/fr.php

@@ -3,6 +3,7 @@
 return array (
 	// LAYOUT
 	'login'				=> 'Connexion',
+	'keep_logged_in'		=> 'Rester connecté <small>(1 mois)</small>',
 	'login_with_persona'		=> 'Connexion avec Persona',
 	'logout'			=> 'Déconnexion',
 	'search'			=> 'Rechercher des mots ou des #tags',
@@ -179,9 +180,16 @@ return array (
 	'auto_share'			=> 'Partager',
 	'auto_share_help'		=> 'S’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',
+	'user_filter'			=> 'Accéder aux filtres utilisateur',
+	'user_filter_help'		=> 'S’il n’y a qu’un filtre utilisateur, celui ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.',
+	'help'				=> 'Afficher la documentation',
 
 	'file_to_import'		=> 'Fichier à importer<br />(OPML, Json ou Zip)',
+	'file_to_import_no_zip'		=> 'Fichier à importer<br />(OPML ou Json)',
 	'import'			=> 'Importer',
+	'file_cannot_be_uploaded'	=> 'Le fichier ne peut pas être téléchargé!',
+	'zip_error'			=> 'Une erreur est survenue durant l’import du fichier Zip.',
+	'no_zip_extension'		=> 'L’extension Zip n’est pas présente sur votre serveur.',
 	'export'			=> 'Exporter',
 	'export_opml'			=> 'Exporter la liste des flux (OPML)',
 	'export_starred'		=> 'Exporter les favoris',
@@ -263,6 +271,7 @@ return array (
 	'sort_order'			=> 'Ordre de tri',
 	'auto_load_more'		=> 'Charger les articles suivants en bas de page',
 	'display_articles_unfolded'	=> 'Afficher les articles dépliés par défaut',
+	'display_categories_unfolded'	=> 'Afficher les catégories pliées par défaut',
 	'hide_read_feeds'		=> 'Cacher les catégories &amp; flux sans article non-lu (uniquement en affichage “articles non lus”)',
 	'after_onread'			=> 'Après “marquer tout comme lu”,',
 	'jump_next'			=> 'sauter au prochain voisin non lu (flux ou catégorie)',

+ 6 - 2
app/layout/aside_flux.phtml

@@ -42,15 +42,19 @@
 			$feeds = $cat->feeds ();
 			if (!empty ($feeds)) {
 				$c_active = false;
+				$c_show = false;
 				if ($this->get_c == $cat->id ()) {
 					$c_active = true;
+					if (!$this->conf->display_categories || $this->get_f) {
+						$c_show = true;
+					}
 				}
 				?><li data-unread="<?php echo $cat->nbNotRead(); ?>"<?php if ($c_active) echo ' class="active"'; ?>><?php
 				?><div class="category stick<?php echo $c_active ? ' active' : ''; ?>"><?php
 					?><a data-unread="<?php echo formatNumber($cat->nbNotRead()); ?>" class="btn<?php echo $c_active ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 'c_' . $cat->id(); echo Minz_Url::display($arUrl); ?>"><?php echo $cat->name (); ?></a><?php
-					?><a class="btn dropdown-toggle" href="#"><?php echo FreshRSS_Themes::icon($c_active ? 'up' : 'down'); ?></a><?php
+					?><a class="btn dropdown-toggle" href="#"><?php echo FreshRSS_Themes::icon($c_show ? 'up' : 'down'); ?></a><?php
 				?></div><?php
-				?><ul class="feeds<?php echo $c_active ? ' active' : ''; ?>"><?php
+				?><ul class="feeds<?php echo $c_show ? ' active' : ''; ?>"><?php
 				foreach ($feeds as $feed) {
 					$feed_id = $feed->id ();
 					$nbEntries = $feed->nbEntries ();

+ 10 - 6
app/layout/nav_menu.phtml

@@ -96,7 +96,7 @@
 				<li class="dropdown-header"><?php echo Minz_Translate::t('queries'); ?> <a class="no-mobile" href="<?php echo _url('configure', 'queries'); ?>"><?php echo FreshRSS_Themes::icon('configure'); ?></a></li>
 
 				<?php foreach ($this->conf->queries as $query) { ?>
-				<li class="item">
+				<li class="item query">
 					<a href="<?php echo $query['url']; ?>"><?php echo $query['name']; ?></a>
 				</li>
 				<?php } ?>
@@ -164,11 +164,15 @@
 					break;
 			}
 		}
-		if ($this->order === 'ASC') {
-			$idMax = 0;
-		} else {
-			$p = isset($this->entries[0]) ? $this->entries[0] : null;
-			$idMax = $p === null ? '0' : $p->id();
+
+		$p = isset($this->entries[0]) ? $this->entries[0] : null;
+		$idMax = $p === null ? (time() - 1) . '000000' : $p->id();
+
+		if ($this->order === 'ASC') {	//In this case we do not know but we guess idMax
+			$idMax2 = (time() - 1) . '000000';
+			if (strcmp($idMax2, $idMax) > 0) {
+				$idMax = $idMax2;
+			}
 		}
 
 		$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('get' => $get, 'nextGet' => $nextGet, 'idMax' => $idMax));

+ 3 - 0
app/views/configure/categorize.phtml

@@ -18,6 +18,9 @@
 					<input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" />
 
 					<?php if ($cat->nbFeed () > 0) { ?>
+					<a class="btn" href="<?php echo _url('index', 'index', 'get', 'c_' . $cat->id ()); ?>">
+						<?php echo _i('link'); ?>
+					</a>
 					<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>

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

@@ -61,6 +61,16 @@
 			</div>
 		</div>
 
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="display_categories">
+					<input type="checkbox" name="display_categories" id="display_categories" value="1"<?php echo $this->conf->display_categories ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('display_categories_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="sticky_post">

+ 15 - 0
app/views/configure/shortcut.phtml

@@ -103,6 +103,21 @@
 			</div>
 		</div>
 
+		<div class="form-group">
+			<label class="group-name" for="user_filter_shortcut"><?php echo Minz_Translate::t ('user_filter'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="user_filter_shortcut" name="shortcuts[user_filter]" list="keys" value="<?php echo $s['user_filter']; ?>" />
+				<?php echo Minz_Translate::t ('user_filter_help'); ?>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="help_shortcut"><?php echo Minz_Translate::t ('help'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="help_shortcut" name="shortcuts[help]" list="keys" value="<?php echo $s['help']; ?>" />
+			</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>

+ 5 - 2
app/views/helpers/javascript_vars.phtml

@@ -4,7 +4,8 @@ echo '"use strict";', "\n";
 
 $mark = $this->conf->mark_when;
 echo 'var ',
-	'hide_posts=', ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'false' : 'true',
+	'help_url="', FRESHRSS_WIKI, '"',
+	',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',
@@ -25,7 +26,9 @@ echo ',shortcuts={',
 	'collapse_entry:"', $s['collapse_entry'], '",',
 	'load_more:"', $s['load_more'], '",',
 	'auto_share:"', $s['auto_share'], '",',
-	'focus_search:"', $s['focus_search'], '"',
+	'focus_search:"', $s['focus_search'], '",',
+	'user_filter:"', $s['user_filter'], '",',
+	'help:"', $s['help'], '"',
 "},\n";
 
 if (Minz_Request::param ('output') === 'global') {

+ 6 - 1
app/views/helpers/view/normal_view.phtml

@@ -81,7 +81,12 @@ if (!empty($this->entries)) {
 				}
 			}
 			$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);
+			if ($feed == null) {
+				$feed = $item->feed(true);
+				if ($feed == null) {
+					$feed = FreshRSS_Feed::example();
+				}
+			}
 			?><li class="item website"><a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>"><img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" /> <span><?php echo $feed->name(); ?></span></a></li>
 			<li class="item title"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo $item->title (); ?></a></li>
 			<?php if ($topline_date) { ?><li class="item date"><?php echo $item->date (); ?> </li><?php } ?>

+ 3 - 1
app/views/importExport/index.phtml

@@ -6,7 +6,9 @@
 	<form method="post" action="<?php echo _url('importExport', 'import'); ?>" enctype="multipart/form-data">
 		<legend><?php echo _t('import'); ?></legend>
 		<div class="form-group">
-			<label class="group-name" for="file"><?php echo _t('file_to_import'); ?></label>
+			<label class="group-name" for="file">
+				<?php echo extension_loaded('zip') ? _t('file_to_import') : _t('file_to_import_no_zip'); ?>
+			</label>
 			<div class="group-controls">
 				<input type="file" name="file" id="file" />
 			</div>

+ 15 - 8
app/views/index/formLogin.phtml

@@ -1,32 +1,39 @@
 <div class="prompt">
-	<h1><?php echo Minz_Translate::t('login'); ?></h1><?php
+	<h1><?php echo _t('login'); ?></h1><?php
 
 	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>
+			<label for="username"><?php echo _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>
+			<label for="passwordPlain"><?php echo _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>
+				<noscript><strong><?php echo _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>
+			<label class="checkbox" for="keep_logged_in">
+				<input type="checkbox" name="keep_logged_in" id="keep_logged_in" value="1" />
+				<?php echo _t('keep_logged_in'); ?>
+			</label>
+			<br />
+		</div>
+		<div>
+			<button id="loginButton" type="submit" class="btn btn-important"><?php echo _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_with_persona'); ?></a>
+			<?php echo _i('login'); ?>
+			<a class="signin" href="#"><?php echo _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 _t('about_freshrss'); ?></a></p>
 </div>

+ 10 - 11
app/views/javascript/actualize.phtml

@@ -1,25 +1,24 @@
 "use strict";
-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";
-	}
-	?>],
+var feeds = [<?php foreach ($this->feeds as $feed) { ?>{<?php
+	?>url: "<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'); ?>",<?php
+	?>title: "<?php echo $feed->name(); ?>"<?php
+?>},<?php } ?>],
 	feed_processed = 0,
 	feed_count = feeds.length;
 
 function initProgressBar(init) {
 	if (init) {
 		$("body").after("\<div id=\"actualizeProgress\" class=\"notification good\">\
-			<?php echo _t('refresh'); ?> <span class=\"progress\">0 / " + feed_count + "</span><br />\
-			<progress id=\"actualizeProgressBar\" value=\"0\" max=\"" + feed_count + "\"></progress>\
+			<?php echo _t('refresh'); ?><br /><span class=\"title\">/</span><br />\
+			<span class=\"progress\">0 / " + feed_count + "</span>\
 		</div>");
 	} else {
 		window.location.reload();
 	}
 }
-function updateProgressBar(i) {
-	$("#actualizeProgressBar").val(i);
+function updateProgressBar(i, title_feed) {
 	$("#actualizeProgress .progress").html(i + " / " + feed_count);
+	$("#actualizeProgress .title").html(title_feed);
 }
 
 function updateFeeds() {
@@ -43,10 +42,10 @@ function updateFeed() {
 
 	$.ajax({
 		type: 'POST',
-		url: feed,
+		url: feed['url'],
 	}).complete(function (data) {
 		feed_processed++;
-		updateProgressBar(feed_processed);
+		updateProgressBar(feed_processed, feed['title']);
 
 		if (feed_processed === feed_count) {
 			initProgressBar(false);

+ 69 - 18
app/views/stats/repartition.phtml

@@ -2,23 +2,38 @@
 
 <div class="post content">
 	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a>
-	
+
+	<h1><?php echo _t('stats_repartition'); ?></h1>
+
+	<select id="feed_select">
+		<option data-url="<?php echo _url('stats', 'repartition')?>"><?php echo _t('all_feeds')?></option>
+	<?php foreach ($this->categories as $category) {
+		$feeds = $category->feeds();
+		if (!empty($feeds)) {
+			echo '<optgroup label="', $category->name(), '">';
+			foreach ($feeds as $feed) {
+				if ($this->feed && $feed->id() == $this->feed->id()){
+					echo '<option value="', $feed->id(), '" selected="selected" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
+				} else {
+					echo '<option value="', $feed->id(), '" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
+				}
+			}
+			echo '</optgroup>';
+		}
+	}?>
+	</select>
+
 	<?php if ($this->feed) {?>
-		<h1>
-			<?php echo _t('stats_repartition'), " - "; ?>
-			<a href="<?php echo _url('configure', 'feed', 'id', $this->feed->id()); ?>">
-				<?php echo $this->feed->name(); ?>
-			</a>
-		</h1>
-	<?php } else {?>
-		<h1><?php echo _t('stats_repartition'); ?></h1>
+		<a href="<?php echo _url('configure', 'feed', 'id', $this->feed->id()); ?>">
+			<?php echo _t('administration'); ?>
+		</a>
 	<?php }?>
-	
+
 	<div class="stat">
 		<h2><?php echo _t('stats_entry_per_hour'); ?></h2>
 		<div id="statsEntryPerHour" style="height: 300px"></div>
 	</div>
-	
+
 	<div class="stat">
 		<h2><?php echo _t('stats_entry_per_day_of_week'); ?></h2>
 		<div id="statsEntryPerDayOfWeek" style="height: 300px"></div>
@@ -41,11 +56,22 @@ function initStats() {
 		return;
 	}
 	// Entry per hour
+	var avg_h = [];
+	for (var i = -1; i <= 24; i++) {
+		avg_h.push([i, <?php echo $this->averageHour?>]);
+	}
 	Flotr.draw(document.getElementById('statsEntryPerHour'),
-		[<?php echo $this->repartitionHour ?>],
+		[{
+			data: <?php echo $this->repartitionHour ?>,
+			bars: {horizontal: false, show: true}
+		}, {
+			data: avg_h,
+			lines: {show: true},
+			label: <?php echo $this->averageHour?>,
+			yaxis: 2
+		}],
 		{
 			grid: {verticalLines: false},
-			bars: {horizontal: false, show: true},
 			xaxis: {noTicks: 23,
 				tickFormatter: function(x) {
 					var x = parseInt(x);
@@ -55,14 +81,26 @@ function initStats() {
 				max: 23.9,
 				tickDecimals: 0},
 			yaxis: {min: 0},
+			y2axis: {showLabels: false},
 			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}}
 		});
 	// Entry per day of week
+	var avg_dow = [];
+	for (var i = -1; i <= 7; i++) {
+		avg_dow.push([i, <?php echo $this->averageDayOfWeek?>]);
+	}
 	Flotr.draw(document.getElementById('statsEntryPerDayOfWeek'),
-		[<?php echo $this->repartitionDayOfWeek ?>],
+		[{
+			data: <?php echo $this->repartitionDayOfWeek ?>,
+			bars: {horizontal: false, show: true}
+		}, {
+			data: avg_dow,
+			lines: {show: true},
+			label: <?php echo $this->averageDayOfWeek?>,
+			yaxis: 2
+		}],
 		{
 			grid: {verticalLines: false},
-			bars: {horizontal: false, show: true},
 			xaxis: {noTicks: 6,
 				tickFormatter: function(x) {
 					var x = parseInt(x),
@@ -73,14 +111,26 @@ function initStats() {
 				max: 6.9,
 				tickDecimals: 0},
 			yaxis: {min: 0},
+			y2axis: {showLabels: false},
 			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}}
 		});
 	// Entry per month
+	var avg_m = [];
+	for (var i = 0; i <= 13; i++) {
+		avg_m.push([i, <?php echo $this->averageMonth?>]);
+	}
 	Flotr.draw(document.getElementById('statsEntryPerMonth'),
-		[<?php echo $this->repartitionMonth ?>],
+		[{
+			data: <?php echo $this->repartitionMonth ?>,
+			bars: {horizontal: false, show: true}
+		}, {
+			data: avg_m,
+			lines: {show: true},
+			label: <?php echo $this->averageMonth?>,
+			yaxis: 2
+		}],
 		{
 			grid: {verticalLines: false},
-			bars: {horizontal: false, show: true},
 			xaxis: {noTicks: 12,
 				tickFormatter: function(x) {
 					var x = parseInt(x),
@@ -91,9 +141,10 @@ function initStats() {
 				max: 12.9,
 				tickDecimals: 0},
 			yaxis: {min: 0},
+			y2axis: {showLabels: false},
 			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}}
 		});
-	
+
 }
 initStats();
 </script>

+ 1 - 0
constants.php

@@ -2,6 +2,7 @@
 define('FRESHRSS_VERSION', '0.8-dev');
 define('FRESHRSS_WEBSITE', 'http://freshrss.org');
 define('FRESHRSS_UPDATE_WEBSITE', 'https://update.freshrss.org?v=' . FRESHRSS_VERSION);
+define('FRESHRSS_WIKI', 'http://doc.freshrss.org');
 
 // PHP text output compression http://php.net/ob_gzhandler (better to do it at Web server level)
 define('PHP_COMPRESSION', false);

+ 1 - 0
data/tokens/.gitignore

@@ -0,0 +1 @@
+*.txt

+ 13 - 0
data/tokens/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">
+<head>
+<meta charset="UTF-8" />
+<meta http-equiv="Refresh" content="0; url=/" />
+<title>Redirection</title>
+<meta name="robots" content="noindex" />
+</head>
+
+<body>
+<p><a href="/">Redirection</a></p>
+</body>
+</html>

+ 14 - 3
lib/Minz/Helper.php

@@ -12,11 +12,22 @@ class Minz_Helper {
 	 * Annule les effets des magic_quotes pour une variable donnée
 	 * @param $var variable à traiter (tableau ou simple variable)
 	 */
-	public static function stripslashes_r ($var) {
-		if (is_array ($var)){
-			return array_map (array ('Helper', 'stripslashes_r'), $var);
+	public static function stripslashes_r($var) {
+		if (is_array($var)){
+			return array_map(array('Minz_Helper', 'stripslashes_r'), $var);
 		} else {
 			return stripslashes($var);
 		}
 	}
+
+	/**
+	 * Wrapper for htmlspecialchars.
+	 * Force UTf-8 value and can be used on array too.
+	 */
+	public static function htmlspecialchars_utf8($var) {
+		if (is_array($var)) {
+			return array_map(array('Minz_Helper', 'htmlspecialchars_utf8'), $var);
+		}
+		return htmlspecialchars($var, ENT_COMPAT, 'UTF-8');
+	}
 }

+ 83 - 49
lib/Minz/Request.php

@@ -10,7 +10,7 @@
 class Minz_Request {
 	private static $controller_name = '';
 	private static $action_name = '';
-	private static $params = array ();
+	private static $params = array();
 
 	private static $default_controller_name = 'index';
 	private static $default_action_name = 'index';
@@ -18,59 +18,53 @@ class Minz_Request {
 	/**
 	 * Getteurs
 	 */
-	public static function controllerName () {
+	public static function controllerName() {
 		return self::$controller_name;
 	}
-	public static function actionName () {
+	public static function actionName() {
 		return self::$action_name;
 	}
-	public static function params () {
+	public static function params() {
 		return self::$params;
 	}
-	static function htmlspecialchars_utf8 ($p) {
-		if (is_array($p)) {
-			return array_map('self::htmlspecialchars_utf8', $p);
-		}
-		return htmlspecialchars($p, ENT_COMPAT, 'UTF-8');
-	}
-	public static function param ($key, $default = false, $specialchars = false) {
-		if (isset (self::$params[$key])) {
+	public static function param($key, $default = false, $specialchars = false) {
+		if (isset(self::$params[$key])) {
 			$p = self::$params[$key];
-			if(is_object($p) || $specialchars) {
+			if (is_object($p) || $specialchars) {
 				return $p;
 			} else {
-				return self::htmlspecialchars_utf8($p);
+				return Minz_Helper::htmlspecialchars_utf8($p);
 			}
 		} else {
 			return $default;
 		}
 	}
-	public static function defaultControllerName () {
+	public static function defaultControllerName() {
 		return self::$default_controller_name;
 	}
-	public static function defaultActionName () {
+	public static function defaultActionName() {
 		return self::$default_action_name;
 	}
 
 	/**
 	 * Setteurs
 	 */
-	public static function _controllerName ($controller_name) {
+	public static function _controllerName($controller_name) {
 		self::$controller_name = $controller_name;
 	}
-	public static function _actionName ($action_name) {
+	public static function _actionName($action_name) {
 		self::$action_name = $action_name;
 	}
-	public static function _params ($params) {
+	public static function _params($params) {
 		if (!is_array($params)) {
-			$params = array ($params);
+			$params = array($params);
 		}
 
 		self::$params = $params;
 	}
-	public static function _param ($key, $value = false) {
+	public static function _param($key, $value = false) {
 		if ($value === false) {
-			unset (self::$params[$key]);
+			unset(self::$params[$key]);
 		} else {
 			self::$params[$key] = $value;
 		}
@@ -79,22 +73,36 @@ class Minz_Request {
 	/**
 	 * Initialise la Request
 	 */
-	public static function init () {
-		self::magicQuotesOff ();
+	public static function init() {
+		self::magicQuotesOff();
 	}
 
 	/**
 	 * Retourn le nom de domaine du site
 	 */
-	public static function getDomainName () {
+	public static function getDomainName() {
 		return $_SERVER['HTTP_HOST'];
 	}
 
+	public static function isRefererFromSameDomain() {
+		if (empty($_SERVER['HTTP_REFERER'])) {
+			return false;
+		}
+		$host = parse_url(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https://' : 'http://') .
+			(empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST']));
+		$referer = parse_url($_SERVER['HTTP_REFERER']);
+		if (empty($host['scheme']) || empty($referer['scheme']) || $host['scheme'] !== $referer['scheme'] ||
+		    empty($host['host']) || empty($referer['host']) || $host['host'] !== $referer['host']) {
+			return false;
+		}
+		return (isset($host['port']) ? $host['port'] : 0) === (isset($referer['port']) ? $referer['port'] : 0);
+	}
+
 	/**
 	 * Détermine la base de l'url
 	 * @return la base de l'url
 	 */
-	public static function getBaseUrl () {
+	public static function getBaseUrl() {
 		$defaultBaseUrl = Minz_Configuration::baseUrl();
 		if (!empty($defaultBaseUrl)) {
 			return $defaultBaseUrl;
@@ -109,13 +117,13 @@ class Minz_Request {
 	 * Récupère l'URI de la requête
 	 * @return l'URI
 	 */
-	public static function getURI () {
-		if (isset ($_SERVER['REQUEST_URI'])) {
-			$base_url = self::getBaseUrl ();
+	public static function getURI() {
+		if (isset($_SERVER['REQUEST_URI'])) {
+			$base_url = self::getBaseUrl();
 			$uri = $_SERVER['REQUEST_URI'];
 
-			$len_base_url = strlen ($base_url);
-			$real_uri = substr ($uri, $len_base_url);
+			$len_base_url = strlen($base_url);
+			$real_uri = substr($uri, $len_base_url);
 		} else {
 			$real_uri = '';
 		}
@@ -129,16 +137,16 @@ class Minz_Request {
 	 * @param $redirect si vrai, force la redirection http
 	 *                > sinon, le dispatcher recharge en interne
 	 */
-	public static function forward ($url = array (), $redirect = false) {
-		$url = Minz_Url::checkUrl ($url);
+	public static function forward($url = array(), $redirect = false) {
+		$url = Minz_Url::checkUrl($url);
 
 		if ($redirect) {
-			header ('Location: ' . Minz_Url::display ($url, 'php'));
-			exit ();
+			header('Location: ' . Minz_Url::display($url, 'php'));
+			exit();
 		} else {
-			self::_controllerName ($url['c']);
-			self::_actionName ($url['a']);
-			self::_params (array_merge (
+			self::_controllerName($url['c']);
+			self::_actionName($url['a']);
+			self::_params(array_merge(
 				self::$params,
 				$url['params']
 			));
@@ -146,6 +154,31 @@ class Minz_Request {
 		}
 	}
 
+
+	/**
+	 * Wrappers good notifications + redirection
+	 * @param $msg notification content
+	 * @param $url url array to where we should be forwarded
+	 */
+	public static function good($msg, $url = array()) {
+		Minz_Session::_param('notification', array(
+			'type' => 'good',
+			'content' => $msg
+		));
+
+		Minz_Request::forward($url, true);
+	}
+
+	public static function bad($msg, $url = array()) {
+		Minz_Session::_param('notification', array(
+			'type' => 'bad',
+			'content' => $msg
+		));
+
+		Minz_Request::forward($url, true);
+	}
+
+
 	/**
 	 * Permet de récupérer une variable de type $_GET
 	 * @param $param nom de la variable
@@ -154,10 +187,10 @@ class Minz_Request {
 	 *         $_GET si $param = false
 	 *         $default si $_GET[$param] n'existe pas
 	 */
-	public static function fetchGET ($param = false, $default = false) {
+	public static function fetchGET($param = false, $default = false) {
 		if ($param === false) {
 			return $_GET;
-		} elseif (isset ($_GET[$param])) {
+		} elseif (isset($_GET[$param])) {
 			return $_GET[$param];
 		} else {
 			return $default;
@@ -172,10 +205,10 @@ class Minz_Request {
 	 *         $_POST si $param = false
 	 *         $default si $_POST[$param] n'existe pas
 	 */
-	public static function fetchPOST ($param = false, $default = false) {
+	public static function fetchPOST($param = false, $default = false) {
 		if ($param === false) {
 			return $_POST;
-		} elseif (isset ($_POST[$param])) {
+		} elseif (isset($_POST[$param])) {
 			return $_POST[$param];
 		} else {
 			return $default;
@@ -188,15 +221,16 @@ class Minz_Request {
 	 *   $_POST
 	 *   $_COOKIE
 	 */
-	private static function magicQuotesOff () {
-		if (get_magic_quotes_gpc ()) {
-			$_GET = Minz_Helper::stripslashes_r ($_GET);
-			$_POST = Minz_Helper::stripslashes_r ($_POST);
-			$_COOKIE = Minz_Helper::stripslashes_r ($_COOKIE);
+	private static function magicQuotesOff() {
+		if (get_magic_quotes_gpc()) {
+			$_GET = Minz_Helper::stripslashes_r($_GET);
+			$_POST = Minz_Helper::stripslashes_r($_POST);
+			$_COOKIE = Minz_Helper::stripslashes_r($_COOKIE);
 		}
 	}
 
-	public static function isPost () {
-		return $_SERVER['REQUEST_METHOD'] === 'POST';
+	public static function isPost() {
+		return isset($_SERVER['REQUEST_METHOD']) &&
+			$_SERVER['REQUEST_METHOD'] === 'POST';
 	}
 }

+ 48 - 26
lib/Minz/Session.php

@@ -2,28 +2,20 @@
 
 /**
  * La classe Session gère la session utilisateur
- * C'est un singleton
  */
 class Minz_Session {
-	/**
-	 * $session stocke les variables de session
-	 */
-	private static $session = array ();	//TODO: Try to avoid having another local copy
-
 	/**
 	 * Initialise la session, avec un nom
-	 * Le nom de session est utilisé comme nom pour les cookies et les URLs (i.e. PHPSESSID).
+	 * Le nom de session est utilisé comme nom pour les cookies et les URLs(i.e. PHPSESSID).
 	 * Il ne doit contenir que des caractères alphanumériques ; il doit être court et descriptif
 	 */
-	public static function init ($name) {
-		// démarre la session
-		session_name ($name);
-		session_set_cookie_params (0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
-		session_start ();
+	public static function init($name) {
+		$cookie = session_get_cookie_params();
+		self::keepCookie($cookie['lifetime']);
 
-		if (isset ($_SESSION)) {
-			self::$session = $_SESSION;
-		}
+		// démarre la session
+		session_name($name);
+		session_start();
 	}
 
 
@@ -32,8 +24,8 @@ class Minz_Session {
 	 * @param $p le paramètre à récupérer
 	 * @return la valeur de la variable de session, false si n'existe pas
 	 */
-	public static function param ($p, $default = false) {
-		return isset(self::$session[$p]) ? self::$session[$p] : $default;
+	public static function param($p, $default = false) {
+		return isset($_SESSION[$p]) ? $_SESSION[$p] : $default;
 	}
 
 
@@ -42,13 +34,11 @@ class Minz_Session {
 	 * @param $p le paramètre à créer ou modifier
 	 * @param $v la valeur à attribuer, false pour supprimer
 	 */
-	public static function _param ($p, $v = false) {
+	public static function _param($p, $v = false) {
 		if ($v === false) {
-			unset ($_SESSION[$p]);
-			unset (self::$session[$p]);
+			unset($_SESSION[$p]);
 		} else {
 			$_SESSION[$p] = $v;
-			self::$session[$p] = $v;
 		}
 	}
 
@@ -57,15 +47,47 @@ class Minz_Session {
 	 * Permet d'effacer une session
 	 * @param $force si à false, n'efface pas le paramètre de langue
 	 */
-	public static function unset_session ($force = false) {
-		$language = self::param ('language');
+	public static function unset_session($force = false) {
+		$language = self::param('language');
 
 		session_destroy();
-		self::$session = array ();
+		$_SESSION = array();
 
 		if (!$force) {
-			self::_param ('language', $language);
-			Minz_Translate::reset ();
+			self::_param('language', $language);
+			Minz_Translate::reset();
 		}
 	}
+
+
+	/**
+	 * Spécifie la durée de vie des cookies
+	 * @param $l la durée de vie
+	 */
+	public static function keepCookie($l) {
+		$cookie_dir = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI'];
+		session_set_cookie_params($l, $cookie_dir, '', false, true);
+	}
+
+
+	/**
+	 * Régénère un id de session.
+	 * Utile pour appeler session_set_cookie_params après session_start()
+	 */
+	public static function regenerateID() {
+		session_regenerate_id(true);
+	}
+
+	public static function deleteLongTermCookie($name) {
+		setcookie($name, '', 1, '', '', false, true);
+	}
+
+	public static function setLongTermCookie($name, $value, $expire) {
+		setcookie($name, $value, $expire, '', '', false, true);
+	}
+
+	public static function getLongTermCookie($name) {
+		return isset($_COOKIE[$name]) ? $_COOKIE[$name] : null;
+	}
+
 }

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

@@ -142,7 +142,7 @@ class SimplePie_Parser
 				$dom = new DOMDocument();
 				$dom->recover = true;
 				$dom->strictErrorChecking = false;
-				$dom->loadXML($data);
+				@$dom->loadXML($data);
 				$this->encoding = $encoding = $dom->encoding = 'UTF-8';
 				$data2 = $dom->saveXML();
 				if (function_exists('mb_convert_encoding'))

+ 0 - 4
lib/lib_rss.php

@@ -230,7 +230,3 @@ function cryptAvailable() {
 	}
 	return false;
 }
-
-function html_chars_utf8($str) {
-	return htmlspecialchars($str, ENT_COMPAT, 'UTF-8');
-}

+ 1 - 0
p/api/greader.php

@@ -135,6 +135,7 @@ function checkCompatibility() {
 	}
 	if ((!array_key_exists('HTTP_AUTHORIZATION', $_SERVER)) &&	//Apache mod_rewrite trick should be fine
 		(empty($_SERVER['SERVER_SOFTWARE']) || (stripos($_SERVER['SERVER_SOFTWARE'], 'nginx') === false)) &&	//nginx should be fine
+		(empty($_SERVER['SERVER_SOFTWARE']) || (stripos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') === false)) &&	//lighttpd should be fine
 		((!function_exists('getallheaders')) || (stripos(php_sapi_name(), 'cgi') !== false))) {	//Main problem is Apache/CGI mode
 		die('FAIL getallheaders! (probably)');
 	}

+ 1 - 0
p/i/.gitignore

@@ -0,0 +1 @@
+.htaccess

+ 65 - 11
p/scripts/main.js

@@ -297,7 +297,7 @@ function next_entry() {
 function prev_feed() {
 	var active_feed = $("#aside_flux .feeds li.active");
 	if (active_feed.length > 0) {
-		active_feed.prev().find('a.feed').each(function(){this.click();});
+		active_feed.prevAll(':visible:first').find('a.feed').each(function(){this.click();});
 	} else {
 		last_feed();
 	}
@@ -306,21 +306,21 @@ function prev_feed() {
 function next_feed() {
 	var active_feed = $("#aside_flux .feeds li.active");
 	if (active_feed.length > 0) {
-		active_feed.next().find('a.feed').each(function(){this.click();});
+		active_feed.nextAll(':visible:first').find('a.feed').each(function(){this.click();});
 	} else {
 		first_feed();
 	}
 }
 
 function first_feed() {
-	var feed = $("#aside_flux .feeds.active li:first");
+	var feed = $("#aside_flux .feeds.active li:visible:first");
 	if (feed.length > 0) {
 		feed.find('a')[1].click();
 	}
 }
 
 function last_feed() {
-	var feed = $("#aside_flux .feeds.active li:last");
+	var feed = $("#aside_flux .feeds.active li:visible:last");
 	if (feed.length > 0) {
 		feed.find('a')[1].click();
 	}
@@ -330,7 +330,7 @@ function prev_category() {
 	var active_cat = $("#aside_flux .category.stick.active");
 
 	if (active_cat.length > 0) {
-		var prev_cat = active_cat.parent('li').prev().find('.category.stick a.btn');
+		var prev_cat = active_cat.parent('li').prevAll(':visible:first').find('.category.stick a.btn');
 		if (prev_cat.length > 0) {
 			prev_cat[0].click();
 		}
@@ -344,7 +344,7 @@ function next_category() {
 	var active_cat = $("#aside_flux .category.stick.active");
 
 	if (active_cat.length > 0) {
-		var next_cat = active_cat.parent('li').next().find('.category.stick a.btn');
+		var next_cat = active_cat.parent('li').nextAll(':visible:first').find('.category.stick a.btn');
 		if (next_cat.length > 0) {
 			next_cat[0].click();
 		}
@@ -355,14 +355,14 @@ function next_category() {
 }
 
 function first_category() {
-	var cat = $("#aside_flux .category.stick:first");
+	var cat = $("#aside_flux .category.stick:visible:first");
 	if (cat.length > 0) {
 		cat.find('a.btn')[0].click();
 	}
 }
 
 function last_category() {
-	var cat = $("#aside_flux .category.stick:last");
+	var cat = $("#aside_flux .category.stick:visible:last");
 	if (cat.length > 0) {
 		cat.find('a.btn')[0].click();
 	}
@@ -373,11 +373,41 @@ function collapse_entry() {
 
 	var flux_current = $(".flux.current");
 	flux_current.toggleClass("active");
-	if (isCollapsed) {
+	if (isCollapsed && auto_mark_article) {
 		mark_read(flux_current, true);
 	}
 }
 
+function user_filter(key) {
+	console.log('user filter');
+	console.warn(key);
+	var filter = $('#dropdown-query');
+	var filters = filter.siblings('.dropdown-menu').find('.item.query a');
+	if (typeof key === "undefined") {
+		if (!filter.length) {
+			return;
+		}
+		// Display the filter div
+		window.location.hash = filter.attr('id');
+		// Force scrolling to the filter div
+		var scroll = needsScroll($('.header'));
+		if (scroll !== 0) {
+			$('html,body').scrollTop(scroll);
+		}
+		// Force the key value if there is only one action, so we can trigger it automatically
+		if (filters.length === 1) {
+			key = 1;
+		} else {
+			return;
+		}
+	}
+	// Trigger selected share action
+	key = parseInt(key);
+	if (key <= filters.length) {
+		filters[key - 1].click();
+	}
+}
+
 function auto_share(key) {
 	var share = $(".flux.current.active").find('.dropdown-target[id^="dropdown-share"]');
 	var shares = share.siblings('.dropdown-menu').find('.item a');
@@ -531,9 +561,19 @@ function init_shortcuts() {
 	}, {
 		'disable_in_input': true
 	});
+
+	shortcut.add(shortcuts.user_filter, function () {
+		user_filter();
+	}, {
+		'disable_in_input': true
+	});
 	for(var i = 1; i < 10; i++){
 		shortcut.add(i.toString(), function (e) {
-			auto_share(String.fromCharCode(e.keyCode));
+			if ($('#dropdown-query').siblings('.dropdown-menu').is(':visible')) {
+				user_filter(String.fromCharCode(e.keyCode));
+			} else {
+				auto_share(String.fromCharCode(e.keyCode));
+			}
 		}, {
 			'disable_in_input': true
 		});
@@ -618,6 +658,13 @@ function init_shortcuts() {
 	}, {
 		'disable_in_input': true
 	});
+
+	shortcut.add(shortcuts.help, function () {
+		redirect(help_url, true);
+	}, {
+		'disable_in_input': true
+	});
+
 }
 
 function init_stream(divStream) {
@@ -663,7 +710,7 @@ function init_stream(divStream) {
 
 	if (auto_mark_site) {
 		divStream.on('click', '.flux .link > a', function () {
-			mark_read($(this).parent().parent().parent(), true);
+			mark_read($(this).parents(".flux"), true);
 		});
 	}
 }
@@ -1063,6 +1110,12 @@ function init_share_observers() {
 	});
 }
 
+function init_stats_observers() {
+	$('#feed_select').on('change', function(e) {
+		redirect($(this).find(':selected').data('url'));
+	});
+}
+
 function init_remove_observers() {
 	$('.post').on('click', 'a.remove', function(e) {
 		var remove_what = $(this).attr('data-remove');
@@ -1177,6 +1230,7 @@ function init_all() {
 		init_remove_observers();
 		init_feed_observers();
 		init_password_observers();
+		init_stats_observers();
 	}
 
 	if (window.console) {

+ 2 - 4
p/themes/Dark/dark.css

@@ -515,15 +515,13 @@ a.btn {
 .categories .feeds .item.empty.active {
 	background: #c95;
 }
-.categories .feeds .item.empty.active .feed {
-	color: #fff;
-}
 .categories .feeds .item.error .feed {
 	color: #a44;
 }
 .categories .feeds .item.error.active {
 	background: #a44;
 }
+.categories .feeds .item.empty.active .feed,
 .categories .feeds .item.error.active .feed {
 	color: #fff;
 }
@@ -570,7 +568,7 @@ a.btn {
 }
 .prompt form {
 	margin: 10px auto 20px auto;
-	width: 180px;
+	width: 200px;
 }
 .prompt input {
 	margin: 5px auto;

+ 6 - 5
p/themes/Flat/flat.css

@@ -492,10 +492,6 @@ a.btn {
 .categories .feeds .item.active {
 	background: #2980b9;
 }
-.categories .feeds .item.active .feed,
-.categories .feeds .item.empty.active .feed {
-	color: #fff;
-}
 .categories .feeds .item.empty.active {
 	background: #f39c12;
 }
@@ -508,6 +504,11 @@ a.btn {
 .categories .feeds .item.error .feed {
 	color: #bd362f;
 }
+.categories .feeds .item.active .feed,
+.categories .feeds .item.empty.active .feed,
+.categories .feeds .item.error.active .feed {
+	color: #fff;
+}
 .categories .feeds .item .feed {
 	margin: 0;
 	width: 165px;
@@ -551,7 +552,7 @@ a.btn {
 }
 .prompt form {
 	margin: 10px auto 20px auto;
-	width: 180px;
+	width: 200px;
 }
 .prompt input {
 	margin: 5px auto;

+ 11 - 9
p/themes/Origine/origine.css

@@ -540,21 +540,23 @@ a.btn {
 .categories .feeds .item.active {
 	background: #0062BE;
 }
-.categories .feeds .item.active .feed {
-	color: #fff;
-}
-.categories .feeds .item.empty .feed {
-	color: #e67e22;
-}
 .categories .feeds .item.empty.active {
 	background: #e67e22;
 }
-.categories .feeds .item.empty.active .feed {
-	color: #fff;
+.categories .feeds .item.error.active {
+	background: #BD362F;
+}
+.categories .feeds .item.empty .feed {
+	color: #e67e22;
 }
 .categories .feeds .item.error .feed {
 	color: #BD362F;
 }
+.categories .feeds .item.active .feed,
+.categories .feeds .item.empty.active .feed,
+.categories .feeds .item.error.active .feed {
+	color: #fff;
+}
 .categories .feeds .item .feed {
 	margin: 0;
 	width: 165px;
@@ -598,7 +600,7 @@ a.btn {
 }
 .prompt form {
 	margin: 10px auto 20px auto;
-	width: 180px;
+	width: 200px;
 }
 .prompt input {
 	margin: 5px auto;

+ 1 - 1
p/themes/Screwdriver/metadata.json

@@ -2,6 +2,6 @@
   "name": "Screwdriver",
   "author": "Mister aiR",
   "description": "C'est un cocktail ! C'est chaud mais « fresh » à la fois. Ce thème tue du chaton.",
-  "version": 1.0,
+  "version": 1.1,
   "files": ["template.css","screwdriver.css"]
 }

+ 18 - 5
p/themes/Screwdriver/screwdriver.css

@@ -206,6 +206,10 @@ a.btn {
 	background: linear-gradient(180deg, #EDE7DE 0%, #FFF 100%) #EDE7DE;
 	background: -webkit-linear-gradient(top, #EDE7DE 0%, #FFF 100%);
 }
+#loginButton.btn{
+	border:none;
+	box-shadow: 0px 1px rgba(255, 255, 255, 0.08) inset,0 -1px #171717,0px 1px rgba(255, 255, 255, 0.08);
+}
 .nav_menu .btn.active, .nav_menu .btn:active, .nav_menu .dropdown-target:target ~ .btn.dropdown-toggle{
 	box-shadow: 0px 2px #E2972A;
 	border-radius: 0;
@@ -333,7 +337,7 @@ a.btn {
 .nav-head {
 	margin: 0;
 	background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE;
-	background: -webkit-linear-gradient(0deg, #EDE7DE 0%, #FFF 100%);
+	background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%);
 	text-align: right;
 }
 .nav-head .item {
@@ -674,6 +678,15 @@ ul.feeds.active{
 .prompt p {
 	margin: 20px 0;
 }
+.prompt input#username,.prompt input#passwordPlain{
+	border:none;
+	box-shadow: 0px 1px rgba(255, 255, 255, 0.08) inset,0 -1px #171717,0px 1px rgba(255, 255, 255, 0.08);
+	background:#EDE7DE;
+}
+.prompt input#username:focus,.prompt input#passwordPlain:focus{
+	border: solid 1px #E7AB34;
+	box-shadow: 0 0 3px #E7AB34;
+}
 
 /*=== New article notification */
 #new-article {
@@ -755,13 +768,13 @@ ul.feeds.active{
 }
 
 .flux .item.title {
-text-decoration: line-through;
+opacity: 0.35;
 }
 .flux.favorite .item.title {
-text-decoration: none;
+opacity: 1;
 }
 .flux.not_read .item.title {
-text-decoration: none;
+opacity: 1;
 }
 .flux.current .item.title a {
 	color: #0f0f0f;
@@ -1084,7 +1097,7 @@ text-decoration: none;
 		text-align: center;
 		background: #171717;
 		box-shadow: 0 1px rgba(255,255,255,0.08);
-		border-radius: 0 0 0 5px;
+		border-radius: 0 8px 0 8px;
 	}
 	.aside .btn-important {
 		display: inline-block;

+ 3 - 0
p/themes/Screwdriver/template.css

@@ -309,6 +309,9 @@ a.btn {
 	list-style: none;
 	margin: 0;
 }
+.state_unread li:not(.active)[data-unread="0"] {
+	display: none;
+}
 .category {
 	display: block;
 	overflow: hidden;

+ 7 - 5
p/themes/base-theme/base.css

@@ -390,16 +390,18 @@ a.btn {
 /*=== Aside main page (feeds) */
 .categories .feeds .item.active {
 }
-.categories .feeds .item.active .feed {
-}
-.categories .feeds .item.empty .feed {
-}
 .categories .feeds .item.empty.active {
 }
-.categories .feeds .item.empty.active .feed {
+.categories .feeds .item.error.active {
+}
+.categories .feeds .item.empty .feed {
 }
 .categories .feeds .item.error .feed {
 }
+.categories .feeds .item.active .feed,
+.categories .feeds .item.empty.active .feed,
+.categories .feeds .item.error.active .feed {
+}
 .categories .feeds .item .feed {
 	margin: 0;
 	width: 165px;