Browse Source

Merge branch 'master' into hotfixes

Marien Fressinaud 11 năm trước cách đây
mục cha
commit
f97d4b3b6c
100 tập tin đã thay đổi với 10116 bổ sung4445 xóa
  1. 276 37
      CHANGELOG
  2. 101 0
      README.fr.md
  3. 92 35
      README.md
  4. 0 28
      actualize_script.php
  5. 3 0
      app/.htaccess
  6. 0 78
      app/App_FrontController.php
  7. 522 0
      app/Controllers/configureController.php
  8. 167 0
      app/Controllers/entryController.php
  9. 38 0
      app/Controllers/errorController.php
  10. 438 0
      app/Controllers/feedController.php
  11. 447 0
      app/Controllers/importExportController.php
  12. 498 0
      app/Controllers/indexController.php
  13. 46 0
      app/Controllers/javascriptController.php
  14. 129 0
      app/Controllers/statsController.php
  15. 129 0
      app/Controllers/updateController.php
  16. 203 0
      app/Controllers/usersController.php
  17. 6 0
      app/Exceptions/BadUrlException.php
  18. 7 0
      app/Exceptions/EntriesGetterException.php
  19. 1 2
      app/Exceptions/FeedException.php
  20. 182 0
      app/FreshRSS.php
  21. 73 0
      app/Models/Category.php
  22. 257 0
      app/Models/CategoryDAO.php
  23. 335 0
      app/Models/Configuration.php
  24. 1 1
      app/Models/Days.php
  25. 192 0
      app/Models/Entry.php
  26. 566 0
      app/Models/EntryDAO.php
  27. 129 0
      app/Models/EntryDAOSQLite.php
  28. 32 0
      app/Models/Factory.php
  29. 331 0
      app/Models/Feed.php
  30. 388 0
      app/Models/FeedDAO.php
  31. 19 0
      app/Models/FeedDAOSQLite.php
  32. 26 0
      app/Models/Log.php
  33. 25 0
      app/Models/LogDAO.php
  34. 44 0
      app/Models/Share.php
  35. 404 0
      app/Models/StatsDAO.php
  36. 64 0
      app/Models/StatsDAOSQLite.php
  37. 121 0
      app/Models/Themes.php
  38. 56 0
      app/Models/UserDAO.php
  39. 61 0
      app/SQL/install.sql.mysql.php
  40. 59 0
      app/SQL/install.sql.sqlite.php
  41. 54 0
      app/actualize_script.php
  42. 0 1
      app/configuration/.gitignore
  43. 0 343
      app/controllers/configureController.php
  44. 0 115
      app/controllers/entryController.php
  45. 0 26
      app/controllers/errorController.php
  46. 0 351
      app/controllers/feedController.php
  47. 0 228
      app/controllers/indexController.php
  48. 0 15
      app/controllers/javascriptController.php
  49. 262 111
      app/i18n/en.php
  50. 277 126
      app/i18n/fr.php
  51. 69 0
      app/i18n/install.en.php
  52. 68 0
      app/i18n/install.fr.php
  53. 13 0
      app/index.html
  54. 879 0
      app/install.php
  55. 30 10
      app/layout/aside_configure.phtml
  56. 32 15
      app/layout/aside_feed.phtml
  57. 82 85
      app/layout/aside_flux.phtml
  58. 12 0
      app/layout/aside_stats.phtml
  59. 83 44
      app/layout/header.phtml
  60. 51 18
      app/layout/layout.phtml
  61. 4 4
      app/layout/nav_entries.phtml
  62. 266 128
      app/layout/nav_menu.phtml
  63. 0 68
      app/layout/persona.phtml
  64. 0 325
      app/models/Category.php
  65. 0 156
      app/models/EntriesGetter.php
  66. 0 584
      app/models/Entry.php
  67. 0 19
      app/models/Exception/FeedException.php
  68. 0 541
      app/models/Feed.php
  69. 0 47
      app/models/Log.php
  70. 0 321
      app/models/RSSConfiguration.php
  71. 0 29
      app/models/RSSPaginator.php
  72. 0 47
      app/models/RSSThemes.php
  73. 79 0
      app/views/configure/archiving.phtml
  74. 27 15
      app/views/configure/categorize.phtml
  75. 71 147
      app/views/configure/display.phtml
  76. 113 50
      app/views/configure/feed.phtml
  77. 0 37
      app/views/configure/importExport.phtml
  78. 97 0
      app/views/configure/queries.phtml
  79. 158 0
      app/views/configure/reading.phtml
  80. 59 0
      app/views/configure/sharing.phtml
  81. 80 16
      app/views/configure/shortcut.phtml
  82. 211 0
      app/views/configure/users.phtml
  83. 9 8
      app/views/entry/bookmark.phtml
  84. 9 8
      app/views/entry/read.phtml
  85. 2 3
      app/views/error/index.phtml
  86. 1 0
      app/views/feed/actualize.phtml
  87. 91 0
      app/views/feed/add.phtml
  88. 0 5
      app/views/helpers/confirm_action_script.phtml
  89. 47 0
      app/views/helpers/export/articles.phtml
  90. 28 0
      app/views/helpers/export/opml.phtml
  91. 61 0
      app/views/helpers/javascript_vars.phtml
  92. 8 8
      app/views/helpers/logs_pagination.phtml
  93. 28 11
      app/views/helpers/pagination.phtml
  94. 28 17
      app/views/helpers/view/global_view.phtml
  95. 156 138
      app/views/helpers/view/normal_view.phtml
  96. 19 23
      app/views/helpers/view/reader_view.phtml
  97. 7 8
      app/views/helpers/view/rss_view.phtml
  98. 0 0
      app/views/importExport/export.phtml
  99. 61 0
      app/views/importExport/index.phtml
  100. 16 13
      app/views/index/about.phtml

+ 276 - 37
CHANGELOG

@@ -1,69 +1,307 @@
-# Changelog
-## 2013-10-12 changes with FreshRSS 0.5.0
-* Possibilité d'interdire la lecture anonyme
-* Option pour garder l'historique d'un flux
-* Lors d'un clic sur "Marquer tous les articles comme lus", FreshRSS peut désormais sauter à la prochaine catégorie / prochain flux avec des articles non lus.
-* Ajout d'un token pour accéder aux flux RSS générés par FreshRSS sans nécessiter de connexion
+# Journal des modifications
+
+## 2014-09-26 FreshRSS 0.8.0 / 0.9.0 (beta)
+
+* UI
+	* New interface for statistics
+	* Fix filter buttons
+	* Number of articles divided by 2 in reading view
+	* Redesign of bigMarkAsRead
+* Features
+	* New automatic update system
+	* New reset auth system
+* Security
+	* "Mark as read" requires POST requests for several articles
+	* Test HTTP REFERER in install.php
+* Configuration
+	* New "Show all articles" / "Show only unread" / "Adjust viewing" option
+	* New notification timeout option
+* Misc.
+	* Improve coding style + comments
+	* Fix SQLite bug "ON DELETE CASCADE"
+	* Improve performance when importing articles
+
+
+## 2014-08-24 FreshRSS 0.7.4
+
+* UI
+	* Hide categories/feeds with unread articles when showing only unread articles
+	* Dynamic favicon showing the number of unread articles
+	* New theme: Screwdriver by Mister aiR
+* Statistics
+	* New page with article repartition
+	* 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.
+
+
+## 2014-07-21 FreshRSS 0.7.3
+
+* New options
+	* Add system of user queries which are shortcuts to filter the view
+	* New TTL option to limit the frequency at which feeds are refreshed (by cron or manual refresh button).
+		It is still possible to manually refresh an individual feed at a higher frequency.
+* SQL
+	* Add support for SQLite (beta) in addition to MySQL
+* SimplePie
+	* Complies with HTTP "301 Moved Permanently" responses by automatically updating the URL of feeds that have changed address.
+* Themes
+	* Flat and Dark designs are based on same template file as Origine
+* Statistics
+	* Refactor code
+	* Add an idle feed page
+* Misc
+	* Several bug fixes
+	* Add confirmation option when marking all articles as read
+	* Fix some typo
+
+
+## 2014-06-13 FreshRSS 0.7.2
+
+* API compatible with Google Reader API level 2
+	* FreshRSS can now be used from e.g.:
+		* (Android) News+ https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader
+		* (Android) EasyRSS https://github.com/Alkarex/EasyRSS
+* Basic support for audio and video podcasts
+* Searching
+	* New search filters date: and pubdate: accepting ISO 8601 date intervals such as `date:2013-2014` or `pubdate:P1W`
+	* Possibility to combine search filters, e.g. `date:2014-05 intitle:FreshRSS intitle:Open great reader #Internet`
+* Change nav menu with more buttons instead of dropdown menus and add some filters
+* New system of import / export
+	* Support OPML, Json (like Google Reader) and Zip archives
+	* Can export and import articles (specific option for favorites)
+* Refactor "Origine" theme
+	* Some improvements
+	* Based on a template file (other themes will use it too)
+
+
+## 2014-02-19 FreshRSS 0.7.1
+
+* Mise à jour des flux plus rapide grâce à une meilleure utilisation du cache
+	* Utilisation d’une signature MD5 du contenu intéressant pour les flux n’implémentant pas les requêtes conditionnelles
+* Modification des raccourcis
+	* "s" partage directement si un seul moyen de partage
+	* Moyens de partage accessibles par "1", "2", "3", etc.
+	* Premier article : Home ; Dernier article : End
+	* Ajout du déplacement au sein des catégories / flux (via modificateurs shift et alt)
+* UI
+	* Séparation des descriptions des raccourcis par groupes
+	* Revue rapide de la page de connexion
+	* Amélioration de l'affichage des notifications sur mobile
+* Revue du système de rafraîchissement des flux
+	* Meilleure gestion de la file de flux à rafraîchir en JSON
+	* Rafraîchissement uniquement pour les flux non rafraîchis récemment
+	* Possibilité donnée aux anonymes de rafraîchir les flux
+* SimplePie
+	* Mise à jour de la lib
+	* Corrige fuite de mémoire
+	* Meilleure tolérance aux flux invalides
+* Corrections divers
+	* Ne déplie plus l'article lors du clic sur l'icône lien externe
+	* Ne boucle plus à la fin de la navigation dans les articles
+	* Suppression du champ category.color inutile
+	* Corrige bug redirection infinie (Persona)
+	* Amélioration vérification de la requête POST
+	* Ajout d'un verrou lorsqu'une action mark_read ou mark_favorite est en cours
+
+
+## 2014-01-29 FreshRSS 0.7
+
+* Nouveau mode multi-utilisateur
+	* L’utilisateur par défaut (administrateur) peut créer et supprimer d’autres utilisateurs
+	* Nécessite un contrôle d’accès, soit :
+		* par le nouveau mode de connexion par formulaire (nom d’utilisateur + mot de passe)
+			* relativement sûr même sans HTTPS (le mot de passe n’est pas transmis en clair)
+			* requiert JavaScript et PHP 5.3+
+		* par HTTP (par exemple sous Apache en créant un fichier ./p/i/.htaccess et .htpasswd)
+			* le nom d’utilisateur HTTP doit correspondre au nom d’utilisateur FreshRSS
+		* par Mozilla Persona, en renseignant l’adresse courriel des utilisateurs
+* Installateur supportant les mises à jour :
+	* Depuis une v0.6, placer application.ini et Configuration.array.php dans le nouveau répertoire “./data/”
+		(voir réorganisation ci-dessous)
+	* Pour les versions suivantes, juste garder le répertoire “./data/”
+* Rafraîchissement automatique du nombre d’articles non lus toutes les deux minutes (utilise le cache HTTP à bon escient)
+	* Permet aussi de conserver la session valide, surtout dans le cas de Persona
+* Nouvelle page de statistiques (nombres d’articles par jour / catégorie)
+* Importation OPML instantanée et plus tolérante
+* Nouvelle gestion des favicons avec téléchargement en parallèle
+* Nouvelles options
+	* Réorganisation des options
+	* Gestion des utilisateurs
+	* Améliorations partage vers Shaarli, Poche, Diaspora*, Facebook, Twitter, Google+, courriel
+		* Raccourci ‘s’ par défaut
+	* Permet la suppression de tous les articles d’un flux
+	* Option pour marquer les articles comme lus dès la réception
+	* Permet de configurer plus finement le nombre d’articles minimum à conserver par flux
+	* Permet de modifier la description et l’adresse d’un flux RSS ainsi que le site Web associé
+	* Nouveau raccourci pour ouvrir/fermer un article (‘c’ par défaut)
+	* Boutons pour effacer les logs et pour purger les vieux articles
+	* Nouveaux filtres d’affichage : seulement les articles favoris, et seulement les articles lus
+* SQL :
+	* Nouveau moteur de recherche, aussi accessible depuis la vue mobile
+		* Mots clefs de recherche “intitle:”, “inurl:”, “author:”
+	* Les articles sont triés selon la date de leur ajout dans FreshRSS plutôt que la date déclarée (souvent erronée)
+		* Permet de marquer tout comme lu sans affecter les nouveaux articles arrivés en cours de lecture
+		* Permet une pagination efficace
+	* Refactorisation
+		* Les tables sont préfixées avec le nom d’utilisateur afin de permettre le mode multi-utilisateurs
+		* Amélioration des performances
+		* Tolère un beaucoup plus grand nombre d’articles
+		* Compression des données côté MySQL plutôt que côté PHP
+		* Incompatible avec la version 0.6 (nécessite une mise à jour grâce à l’installateur) 
+	* Affichage de la taille de la base de données dans FreshRSS
+	* Correction problème de marquage de tous les favoris comme lus
+* HTML5 :
+	* Support des balises HTML5 audio, video, et éléments associés
+		* Utilisation de preload="none", et réécriture correcte des adresses, aussi en HTTPS
+	* Protection HTML5 des iframe (sandbox="allow-scripts allow-same-origin")
+	* Filtrage des object et embed
+	* Chargement différé HTML5 (postpone="") pour iframe et video
+	* Chargement différé JavaScript pour iframe
+* CSS :
+	* Nouveau thème sombre
+		* Chargement plus robuste des thèmes
+	* Meilleur support des longs titres d’articles sur des écrans étroits
+	* Meilleure accessibilité
+		* FreshRSS fonctionne aussi en mode dégradé sans images (alternatives Unicode) et/ou sans CSS
+	* Diverses améliorations
+* PHP :
+	* Encore plus tolérant pour les flux comportant des erreurs
+	* Mise à jour automatique de l’URL du flux (en base de données) lorsque SimplePie découvre qu’elle a changé
+	* Meilleure gestion des caractères spéciaux dans différents cas
+	* Compatibilité PHP 5.5+ avec OPcache
+	* Amélioration des performances
+	* Chargement automatique des classes
+	* Alternative dans le cas d’absence de librairie JSON
+	* Pour le développement, le cache HTTP peut être désactivé en créant un fichier “./data/no-cache.txt”
+* Réorganisation des fichiers et répertoires, en particulier :
+	* Tous les fichiers utilisateur sont dans “./data/” (y compris “cache”, “favicons”, et “log”)
+	* Déplacement de “./app/configuration/application.ini” vers “./data/config.php”
+		* Meilleure sécurité et compatibilité
+	* Déplacement de “./public/data/Configuration.array.php” vers “./data/*_user.php”
+	* Déplacement de “./public/” vers “./p/”
+		* Déplacement de “./public/index.php” vers “./p/i/index.php” (voir cookie ci-dessous)
+	* Déplacement de “./actualize_script.php” vers “./app/actualize_script.php” (pour une meilleure sécurité)
+		* Pensez à mettre à jour votre Cron !
+* Divers :
+	* Nouvelle politique de cookie de session (témoin de connexion)
+		* Utilise un nom poli “FreshRSS” (évite des problèmes avec certains filtres)
+		* Se limite au répertoire “./FreshRSS/p/i/” pour de meilleures performances HTTP
+			* Les images, CSS, scripts sont servis sans cookie
+		* Utilise “HttpOnly” pour plus de sécurité
+	* Nouvel “agent utilisateur” exposé lors du téléchargement des flux, par exemple :
+		* “FreshRSS/0.7 (Linux; http://freshrss.org) SimplePie/1.3.1”
+	* Script d’actualisation avec plus de messages
+		* Sur la sortie standard, ainsi que dans le log système (syslog)
+	* Affichage du numéro de version dans "À propos"
+
+
+## 2013-11-21 FreshRSS 0.6.1
+
+* Corrige bug chargement du JavaScript
+* Affiche un message d’erreur plus explicite si fichier de configuration inaccessible
+
+
+## 2013-11-17 FreshRSS 0.6
+
+* Nettoyage du code JavaScript + optimisations
+* Utilisation d’adresses relatives
+* Amélioration des performances coté client
+* Mise à jour automatique du nombre d’articles non lus
+* Corrections traductions
+* Mise en cache de FreshRSS
+* Amélioration des retours utilisateur lorsque la configuration n’est pas bonne
+* Actualisation des flux après une importation OPML
+* Meilleure prise en charge des flux RSS invalides
+* Amélioration de la vue globale
+* Possibilité de personnaliser les icônes de lecture
+* Suppression de champs lors de l’installation (base_url et sel)
+* Correction bugs divers
+
+
+## 2013-10-15 FreshRSS 0.5.1
+
+* Correction bug des catégories disparues
+* Correction traduction i18n/fr et i18n/en
+* Suppression de certains appels à la feuille de style fallback.css
+
+
+## 2013-10-12 FreshRSS 0.5.0
+
+* Possibilité d’interdire la lecture anonyme
+* Option pour garder l’historique d’un flux
+* Lors d’un clic sur “Marquer tous les articles comme lus”, FreshRSS peut désormais sauter à la prochaine catégorie / prochain flux avec des articles non lus.
+* Ajout d’un token pour accéder aux flux RSS générés par FreshRSS sans nécessiter de connexion
 * Possibilité de partager vers Facebook, Twitter et Google+
 * Possibilité de changer de thème
 * Le menu de navigation (article précédent / suivant / haut de page) a été ajouté à la vue non mobile
 * La police OpenSans est désormais appliquée
 * Amélioration de la page de configuration
-* Une meilleure sortie pour l'imprimante
+* Une meilleure sortie pour limprimante
 * Quelques retouches du design par défaut
-* Les vidéos ne dépassent plus du cadre de l'écran
+* Les vidéos ne dépassent plus du cadre de lécran
 * Nouveau logo
-* Possibilité d'ajouter un préfixe aux tables lors de l'installation
-* Ajout d'un champ en base de données keep_history à la table feed
-* Si possible, création automatique de la base de données si elle n'existe pas lors de l'installation
-* L'utilisation d'UTF-8 est forcée
+* Possibilité d’ajouter un préfixe aux tables lors de l’installation
+* Ajout dun champ en base de données keep_history à la table feed
+* Si possible, création automatique de la base de données si elle n’existe pas lors de l’installation
+* L’utilisation d’UTF-8 est forcée
 * Le marquage automatique au défilement de la page a été amélioré
 * La vue globale a été énormément améliorée et est beaucoup plus utile
 * Amélioration des requêtes SQL
-* Amélioration du Javascript
+* Amélioration du JavaScript
 * Correction bugs divers
 
-## 2013-07-02 changes with FreshRSS 0.4.0
 
-* Correction bug et ajout notification lors de la phase d'installation
-* Affichage d'erreur si fichier OPML invalide
+## 2013-07-02 FreshRSS 0.4.0
+
+* Correction bug et ajout notification lors de la phase d’installation
+* Affichage d’erreur si fichier OPML invalide
 * Les tags sont maintenant cliquables pour filtrer dessus
-* Amélioration vue mobile (boutons plus gros et ajout d'une barre de navigation)
-* Possibilité d'ajouter directement un flux dans une catégorie dès son ajout
+* Amélioration vue mobile (boutons plus gros et ajout dune barre de navigation)
+* Possibilité dajouter directement un flux dans une catégorie dès son ajout
 * Affichage des flux en erreur (injoignable par exemple) en rouge pour les différencier
-* Possiblité de changer les noms des flux
-* Ajout d'une option (désactivable donc) pour charger les images en lazyload permettant de ne pas charger toutes les images d'un coup
-* Le framework Minz est maintenant directement inclus dans l'archive (plus besoin de passer par ./build.sh)
+* Possibilité de changer les noms des flux
+* Ajout dune option (désactivable donc) pour charger les images en lazyload permettant de ne pas charger toutes les images dun coup
+* Le framework Minz est maintenant directement inclus dans larchive (plus besoin de passer par ./build.sh)
 * Amélioration des performances pour la récupération des flux tronqués
-* Possibilité d'importer des flux sans catégorie lors de l'import OPML
-* Suppression de "l'API" (qui était de toutes façons très basique) et de la fonctionnalité de "notes"
-* Amélioration de la recherche (garde en mémoire si l'on a sélectionné une catégorie) par exemple
+* Possibilité d’importer des flux sans catégorie lors de l’import OPML
+* Suppression de “l’API” (qui était de toute façon très basique) et de la fonctionnalité de “notes”
+* Amélioration de la recherche (garde en mémoire si lon a sélectionné une catégorie) par exemple
 * Modification apparence des balises hr et pre
 * Meilleure vérification des champs de formulaire
-* Remise en place du mode "endless" (permettant de simplement charger les articles qui suivent plutôt que de charger une nouvelle page)
-* Ajout d'une page de visualisation des logs
-* Ajout d'une option pour optimiser la BDD (diminue sa taille)
+* Remise en place du mode “endless” (permettant de simplement charger les articles qui suivent plutôt que de charger une nouvelle page)
+* Ajout dune page de visualisation des logs
+* Ajout dune option pour optimiser la BDD (diminue sa taille)
 * Ajout des vues lecture et globale (assez basique)
-* Les vidéos Youtube ne débordent plus du cadre sur les petits écrans
-* Ajout d'une option pour marquer les articles comme lus lors du défilement (et suppression de celle au chargement de la page)
+* Les vidéos YouTube ne débordent plus du cadre sur les petits écrans
+* Ajout d’une option pour marquer les articles comme lus lors du défilement (et suppression de celle au chargement de la page)
+
 
-## 2013-05-05 changes with FreshRSS 0.3.0
+## 2013-05-05 FreshRSS 0.3.0
 
 * Fallback pour les icônes SVG (utilisation de PNG à la place)
 * Fallback pour les propriétés CSS3 (utilisation de préfixes)
 * Affichage des tags associés aux articles
-* Internationalisation de l'application (gestion des langues anglaise et française)
+* Internationalisation de lapplication (gestion des langues anglaise et française)
 * Gestion des flux protégés par authentification HTTP
 * Mise en cache des favicons
-* Création d'un logo *temporaire*
+* Création dun logo *temporaire*
 * Affichage des vidéos dans les articles
 * Gestion de la recherche et filtre par tags pleinement fonctionnels
-* Création d'un vrai script CRON permettant de mettre tous les flux à jour
+* Création dun vrai script CRON permettant de mettre tous les flux à jour
 * Correction bugs divers
 
-## 2013-04-17 changes with FreshRSS 0.2.0
 
-* Création d'un installateur
+## 2013-04-17 FreshRSS 0.2.0
+
+* Création d’un installateur
 * Actualisation des flux en Ajax
 * Partage par mail et Shaarli ajouté
 * Export par flux RSS
@@ -78,6 +316,7 @@
 * Flux sans auteurs gérés normalement
 * Correction bugs divers
 
-## 2013-04-08 changes with FreshRSS 0.1.0
 
-* "Première" version
+## 2013-04-08 FreshRSS 0.1.0
+
+* “Première” version

+ 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.0
+* Date de publication 2014-09-26
+* 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)

+ 92 - 35
README.md

@@ -1,44 +1,101 @@
+* [Version française](README.fr.md)
+
 # FreshRSS
-FreshRSS est un agrégateur de flux RSS à auto-héberger à l'image de [Selfoss](http://selfoss.aditu.de/), [TinyTinyRSS](http://tt-rss.org/redmine/projects/tt-rss/wiki), [Leed](http://projet.idleman.fr/leed/) our encore [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.
+FreshRSS is a self-hosted RSS feed agregator like [Leed](http://projet.idleman.fr/leed/) or [Kriss Feed](http://tontof.net/kriss/feed/).
+
+It is at the same time light-weight, easy to work with, powerful and customizable.
+
+It is a multi-user application with an anonymous reading mode.
+
+* Official website: http://freshrss.org
+* Demo: http://demo.freshrss.org/
+* Developer: Marien Fressinaud <dev@marienfressinaud.fr>
+* Current version: 0.8.0
+* Publication date: 2014-09-26
+* License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
+
+![FreshRSS logo](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png)
 
-* Site officiel : http://marienfressinaud.github.io/FreshRSS/
-* Démo : http://marienfressinaud.fr/projets/freshrss/
-* Développeur : Marien Fressinaud <dev@marienfressinaud.fr>
-* Version actuelle : 0.5.0
-* Date de publication 2013-10-12
-* License AGPL3
+# Note on branches
+**This application is still in development!** Please use the branch that suits your needs:
 
-![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png)
+* 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 à mes besoins personnels.
-Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement
-sur un autre serveur que le mien. 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 Apache ou Nginx (non testé sur les autres)
-* PHP 5.3 (il me faudrait des retours sur d'autres versions antérieures)
-* libxml pour PHP
-* cURL
-* PDO et MySQL
-
-![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. Déplacez l'application où vous voulez sur votre serveur (attention, la partie accessible se trouve dans le répertoire `./public`)
-3. Accédez à FreshRSS à travers votre navigateur web et suivez les instructions d'installation
-4. Tout devrait fonctionner :) En cas de problème, n'hésitez pas à me contacter.
-
-# Sécurité et conseils
-1. Pour une meilleure sécurité, faites en sorte que seul le répertoire `./public` soit accessible par le navigateur. Faites pointer un sous-domaine sur le répertoire `./public` par exemple
-2. Dans tous les cas, assurez-vous que `./app/configuration/application.ini` ne puisse pas être téléchargé !
-3. Le fichier de log peut être utile à lire si vous avez des soucis
-4. Le fichier `./public/index.php` défini les chemins d'accès aux répertoires clés de l'application. Si vous les bougez, tout se passe ici.
-5. Vous pouvez ajouter une tâche CRON sur le script d'actualisation des flux. Il s'agit d'un script PHP à exécuter avec la commande `php`. 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
 ```
-0 * * * * php /chemin/vers/freshrss/actualize_script.php >/dev/null 2>&1
+
+# 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.
+
+# 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 user -p --databases freshrss > freshrss.sql
 ```
+
+
+# Included libraries
+* [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)
+
+## Only for some options
+* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
+* [phpQuery](http://code.google.com/p/phpquery/)
+
+## 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)

+ 0 - 28
actualize_script.php

@@ -1,28 +0,0 @@
-<?php
-
-// Constantes de chemins
-define ('PUBLIC_PATH', realpath (dirname (__FILE__) . '/public'));
-define ('LIB_PATH', realpath (PUBLIC_PATH . '/../lib'));
-define ('APP_PATH', realpath (PUBLIC_PATH . '/../app'));
-define ('LOG_PATH', realpath (PUBLIC_PATH . '/../log'));
-define ('CACHE_PATH', realpath (PUBLIC_PATH . '/../cache'));
-
-$_GET['c'] = 'feed';
-$_GET['a'] = 'actualize';
-$_GET['force'] = true;
-$_SERVER['HTTP_HOST'] = '';
-
-set_include_path (get_include_path ()
-		 . PATH_SEPARATOR
-		 . LIB_PATH
-		 . PATH_SEPARATOR
-		 . LIB_PATH . '/minz'
-		 . PATH_SEPARATOR
-		 . APP_PATH);
-
-require (APP_PATH . '/App_FrontController.php');
-
-$front_controller = new App_FrontController ();
-$front_controller->init ();
-Session::_param('mail', true); // permet de se passer de la phase de connexion
-$front_controller->run ();

+ 3 - 0
app/.htaccess

@@ -0,0 +1,3 @@
+Order	Allow,Deny
+Deny	from all
+Satisfy	all

+ 0 - 78
app/App_FrontController.php

@@ -1,78 +0,0 @@
-<?php
-/** 
- * MINZ - Copyright 2011 Marien Fressinaud
- * Sous licence AGPL3 <http://www.gnu.org/licenses/>
-*/
-require ('FrontController.php');
-
-class App_FrontController extends FrontController {
-	public function init () {
-		$this->loadLibs ();
-		$this->loadModels ();
-
-		Session::init ();
-		RSSThemes::init ();
-		Translate::init ();
-
-		$this->loadParamsView ();
-		$this->loadStylesAndScripts ();
-		$this->loadNotifications ();
-	}
-
-	private function loadLibs () {
-		require (LIB_PATH . '/lib_phpQuery.php');
-		require (LIB_PATH . '/lib_rss.php');
-		require (LIB_PATH . '/SimplePie_autoloader.php');
-		require (LIB_PATH . '/lib_text.php');
-	}
-
-	private function loadModels () {
-		include (APP_PATH . '/models/Exception/FeedException.php');
-		include (APP_PATH . '/models/Exception/EntriesGetterException.php');
-		include (APP_PATH . '/models/RSSConfiguration.php');
-		include (APP_PATH . '/models/RSSThemes.php');
-		include (APP_PATH . '/models/Days.php');
-		include (APP_PATH . '/models/Category.php');
-		include (APP_PATH . '/models/Feed.php');
-		include (APP_PATH . '/models/Entry.php');
-		include (APP_PATH . '/models/EntriesGetter.php');
-		include (APP_PATH . '/models/RSSPaginator.php');
-		include (APP_PATH . '/models/Log.php');
-	}
-
-	private function loadParamsView () {
-		$this->conf = Session::param ('conf', new RSSConfiguration ());
-		View::_param ('conf', $this->conf);
-
-		$entryDAO = new EntryDAO ();
-		View::_param ('nb_not_read', $entryDAO->countNotRead ());
-
-		Session::_param ('language', $this->conf->language ());
-	}
-
-	private function loadStylesAndScripts () {
-		$theme = RSSThemes::get_infos($this->conf->theme());
-		if ($theme) {
-			foreach($theme["files"] as $file) {
-				View::appendStyle (Url::display ('/themes/' . $theme['path'] . '/' . $file));
-			}
-		}
-		View::appendStyle (Url::display ('/themes/printer/style.css'), 'print');
-		if (login_is_conf ($this->conf)) {
-			View::appendScript ('https://login.persona.org/include.js');
-		}
-		View::appendScript (Url::display ('/scripts/jquery.min.js'));
-		if ($this->conf->lazyload () === 'yes') {
-			View::appendScript (Url::display ('/scripts/jquery.lazyload.min.js'));
-		}
-		View::appendScript (Url::display ('/scripts/notification.js'));
-	}
-
-	private function loadNotifications () {
-		$notif = Session::param ('notification');
-		if ($notif) {
-			View::_param ('notification', $notif);
-			Session::_param ('notification');
-		}
-	}
-}

+ 522 - 0
app/Controllers/configureController.php

@@ -0,0 +1,522 @@
+<?php
+
+/**
+ * Controller to handle every configuration options.
+ */
+class FreshRSS_configure_Controller extends Minz_ActionController {
+	/**
+	 * This action is called before every other action in that class. It is
+	 * the common boiler plate for every action. It is triggered by the
+	 * underlying framework.
+	 *
+	 * @todo see if the category default configuration is needed here or if
+	 *       we can move it to the categorize action
+	 */
+	public function firstAction() {
+		if (!$this->view->loginOk) {
+			Minz_Error::error(
+				403,
+				array('error' => array(_t('access_denied')))
+			);
+		}
+
+		$catDAO = new FreshRSS_CategoryDAO();
+		$catDAO->checkDefault();
+	}
+
+	/**
+	 * This action handles the category configuration page
+	 *
+	 * It displays the category configuration page.
+	 * If this action is reached through a POST request, it loops through
+	 * every category to check for modification then add a new category if
+	 * needed then sends a notification to the user.
+	 * If a category name is emptied, the category is deleted and all
+	 * related feeds are moved to the default category. Related user queries
+	 * are deleted too.
+	 * If a category name is changed, it is updated.
+	 */
+	public function categorizeAction() {
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$catDAO = new FreshRSS_CategoryDAO();
+		$defaultCategory = $catDAO->getDefault();
+		$defaultId = $defaultCategory->id();
+
+		if (Minz_Request::isPost()) {
+			$cats = Minz_Request::param('categories', array());
+			$ids = Minz_Request::param('ids', array());
+			$newCat = trim(Minz_Request::param('new_category', ''));
+
+			foreach ($cats as $key => $name) {
+				if (strlen($name) > 0) {
+					$cat = new FreshRSS_Category($name);
+					$values = array(
+						'name' => $cat->name(),
+					);
+					$catDAO->updateCategory($ids[$key], $values);
+				} elseif ($ids[$key] != $defaultId) {
+					$feedDAO->changeCategory($ids[$key], $defaultId);
+					$catDAO->deleteCategory($ids[$key]);
+
+					// Remove related queries.
+					$this->view->conf->remove_query_by_get('c_' . $ids[$key]);
+					$this->view->conf->save();
+				}
+			}
+
+			if ($newCat != '') {
+				$cat = new FreshRSS_Category($newCat);
+				$values = array(
+					'id' => $cat->id(),
+					'name' => $cat->name(),
+				);
+
+				if ($catDAO->searchByName($newCat) == null) {
+					$catDAO->addCategory($values);
+				}
+			}
+			invalidateHttpCache();
+
+			Minz_Request::good(_t('categories_updated'),
+			                   array('c' => 'configure', 'a' => 'categorize'));
+		}
+
+		$this->view->categories = $catDAO->listCategories(false);
+		$this->view->defaultCategory = $catDAO->getDefault();
+		$this->view->feeds = $feedDAO->listFeeds();
+
+		Minz_View::prependTitle(_t('categories_management') . ' · ');
+	}
+
+	/**
+	 * This action handles the feed configuration page.
+	 *
+	 * It displays the feed configuration page.
+	 * If this action is reached through a POST request, it stores all new
+	 * configuraiton values then sends a notification to the user.
+	 *
+	 * The options available on the page are:
+	 *   - name
+	 *   - description
+	 *   - website URL
+	 *   - feed URL
+	 *   - category id (default: default category id)
+	 *   - CSS path to article on website
+	 *   - display in main stream (default: 0)
+	 *   - HTTP authentication
+	 *   - number of article to retain (default: -2)
+	 *   - refresh frequency (default: -2)
+	 * Default values are empty strings unless specified.
+	 */
+	public function feedAction() {
+		$catDAO = new FreshRSS_CategoryDAO();
+		$this->view->categories = $catDAO->listCategories(false);
+
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$this->view->feeds = $feedDAO->listFeeds();
+
+		$id = Minz_Request::param('id');
+		if ($id == false && !empty($this->view->feeds)) {
+			$id = current($this->view->feeds)->id();
+		}
+
+		$this->view->flux = false;
+		if ($id != false) {
+			$this->view->flux = $this->view->feeds[$id];
+
+			if (!$this->view->flux) {
+				Minz_Error::error(
+					404,
+					array('error' => array(_t('page_not_found')))
+				);
+			} else {
+				if (Minz_Request::isPost() && $this->view->flux) {
+					$user = Minz_Request::param('http_user', '');
+					$pass = Minz_Request::param('http_pass', '');
+
+					$httpAuth = '';
+					if ($user != '' || $pass != '') {
+						$httpAuth = $user . ':' . $pass;
+					}
+
+					$cat = intval(Minz_Request::param('category', 0));
+
+					$values = array(
+						'name' => Minz_Request::param('name', ''),
+						'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
+						'website' => Minz_Request::param('website', ''),
+						'url' => Minz_Request::param('url', ''),
+						'category' => $cat,
+						'pathEntries' => Minz_Request::param('path_entries', ''),
+						'priority' => intval(Minz_Request::param('priority', 0)),
+						'httpAuth' => $httpAuth,
+						'keep_history' => intval(Minz_Request::param('keep_history', -2)),
+						'ttl' => intval(Minz_Request::param('ttl', -2)),
+					);
+
+					if ($feedDAO->updateFeed($id, $values)) {
+						$this->view->flux->_category($cat);
+						$this->view->flux->faviconPrepare();
+						$notif = array(
+							'type' => 'good',
+							'content' => _t('feed_updated')
+						);
+					} else {
+						$notif = array(
+							'type' => 'bad',
+							'content' => _t('error_occurred_update')
+						);
+					}
+					invalidateHttpCache();
+
+					Minz_Session::_param('notification', $notif);
+					Minz_Request::forward(array('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true);
+				}
+
+				Minz_View::prependTitle(_t('rss_feed_management') . ' — ' . $this->view->flux->name() . ' · ');
+			}
+		} else {
+			Minz_View::prependTitle(_t('rss_feed_management') . ' · ');
+		}
+	}
+
+	/**
+	 * This action handles the display configuration page.
+	 *
+	 * It displays the display configuration page.
+	 * If this action is reached through a POST request, it stores all new
+	 * configuration values then sends a notification to the user.
+	 *
+	 * The options available on the page are:
+	 *   - language (default: en)
+	 *   - theme (default: Origin)
+	 *   - content width (default: thin)
+	 *   - display of read action in header
+	 *   - display of favorite action in header
+	 *   - display of date in header
+	 *   - display of open action in header
+	 *   - display of read action in footer
+	 *   - display of favorite action in footer
+	 *   - display of sharing action in footer
+	 *   - display of tags in footer
+	 *   - display of date in footer
+	 *   - display of open action in footer
+	 *   - html5 notification timeout (default: 0)
+	 * Default values are false unless specified.
+	 */
+	public function displayAction() {
+		if (Minz_Request::isPost()) {
+			$this->view->conf->_language(Minz_Request::param('language', 'en'));
+			$this->view->conf->_theme(Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme));
+			$this->view->conf->_content_width(Minz_Request::param('content_width', 'thin'));
+			$this->view->conf->_topline_read(Minz_Request::param('topline_read', false));
+			$this->view->conf->_topline_favorite(Minz_Request::param('topline_favorite', false));
+			$this->view->conf->_topline_date(Minz_Request::param('topline_date', false));
+			$this->view->conf->_topline_link(Minz_Request::param('topline_link', false));
+			$this->view->conf->_bottomline_read(Minz_Request::param('bottomline_read', false));
+			$this->view->conf->_bottomline_favorite(Minz_Request::param('bottomline_favorite', false));
+			$this->view->conf->_bottomline_sharing(Minz_Request::param('bottomline_sharing', false));
+			$this->view->conf->_bottomline_tags(Minz_Request::param('bottomline_tags', false));
+			$this->view->conf->_bottomline_date(Minz_Request::param('bottomline_date', false));
+			$this->view->conf->_bottomline_link(Minz_Request::param('bottomline_link', false));
+			$this->view->conf->_html5_notif_timeout(Minz_Request::param('html5_notif_timeout', 0));
+			$this->view->conf->save();
+
+			Minz_Session::_param('language', $this->view->conf->language);
+			Minz_Translate::reset();
+			invalidateHttpCache();
+
+			Minz_Request::good(_t('configuration_updated'),
+			                   array('c' => 'configure', 'a' => 'display'));
+		}
+
+		$this->view->themes = FreshRSS_Themes::get();
+
+		Minz_View::prependTitle(_t('display_configuration') . ' · ');
+	}
+
+	/**
+	 * This action handles the reading configuration page.
+	 *
+	 * It displays the reading configuration page.
+	 * If this action is reached through a POST request, it stores all new
+	 * configuration values then sends a notification to the user.
+	 *
+	 * The options available on the page are:
+	 *   - number of posts per page (default: 10)
+	 *   - view mode (default: normal)
+	 *   - default article view (default: all)
+	 *   - load automatically articles
+	 *   - display expanded articles
+	 *   - display expanded categories
+	 *   - hide categories and feeds without unread articles
+	 *   - jump on next category or feed when marked as read
+	 *   - image lazy loading
+	 *   - stick open articles to the top
+	 *   - display a confirmation when reading all articles
+	 *   - article order (default: DESC)
+	 *   - mark articles as read when:
+	 *       - displayed
+	 *       - opened on site
+	 *       - scrolled
+	 *       - received
+	 * Default values are false unless specified.
+	 */
+	public function readingAction() {
+		if (Minz_Request::isPost()) {
+			$this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10));
+			$this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal'));
+			$this->view->conf->_default_view((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));
+			$this->view->conf->_sticky_post(Minz_Request::param('sticky_post', false));
+			$this->view->conf->_reading_confirm(Minz_Request::param('reading_confirm', false));
+			$this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC'));
+			$this->view->conf->_mark_when(array(
+				'article' => Minz_Request::param('mark_open_article', false),
+				'site' => Minz_Request::param('mark_open_site', false),
+				'scroll' => Minz_Request::param('mark_scroll', false),
+				'reception' => Minz_Request::param('mark_upon_reception', false),
+			));
+			$this->view->conf->save();
+
+			Minz_Session::_param('language', $this->view->conf->language);
+			Minz_Translate::reset();
+			invalidateHttpCache();
+
+			Minz_Request::good(_t('configuration_updated'),
+			                   array('c' => 'configure', 'a' => 'reading'));
+		}
+
+		Minz_View::prependTitle(_t('reading_configuration') . ' · ');
+	}
+
+	/**
+	 * This action handles the sharing configuration page.
+	 *
+	 * It displays the sharing configuration page.
+	 * If this action is reached through a POST request, it stores all
+	 * configuration values then sends a notification to the user.
+	 */
+	public function sharingAction() {
+		if (Minz_Request::isPost()) {
+			$params = Minz_Request::params();
+			$this->view->conf->_sharing($params['share']);
+			$this->view->conf->save();
+			invalidateHttpCache();
+
+			Minz_Request::good(_t('configuration_updated'),
+			                   array('c' => 'configure', 'a' => 'sharing'));
+		}
+
+		Minz_View::prependTitle(_t('sharing') . ' · ');
+	}
+
+	/**
+	 * This action handles the shortcut configuration page.
+	 *
+	 * It displays the shortcut configuration page.
+	 * If this action is reached through a POST request, it stores all new
+	 * configuration values then sends a notification to the user.
+	 *
+	 * The authorized values for shortcuts are letters (a to z), numbers (0
+	 * to 9), function keys (f1 to f12), backspace, delete, down, end, enter,
+	 * escape, home, insert, left, page down, page up, return, right, space,
+	 * tab and up.
+	 */
+	public function shortcutAction() {
+		$list_keys = array('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter',
+		                    'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left',
+		                    'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right',
+		                    's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y',
+		                    'z', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9',
+		                    'f10', 'f11', 'f12');
+		$this->view->list_keys = $list_keys;
+
+		if (Minz_Request::isPost()) {
+			$shortcuts = Minz_Request::param('shortcuts');
+			$shortcuts_ok = array();
+
+			foreach ($shortcuts as $key => $value) {
+				if (in_array($value, $list_keys)) {
+					$shortcuts_ok[$key] = $value;
+				}
+			}
+
+			$this->view->conf->_shortcuts($shortcuts_ok);
+			$this->view->conf->save();
+			invalidateHttpCache();
+
+			Minz_Request::good(_t('shortcuts_updated'),
+			                   array('c' => 'configure', 'a' => 'shortcut'));
+		}
+
+		Minz_View::prependTitle(_t('shortcuts') . ' · ');
+	}
+
+	/**
+	 * This action display the user configuration page
+	 *
+	 * @todo move that action in the user controller
+	 */
+	public function usersAction() {
+		Minz_View::prependTitle(_t('users') . ' · ');
+	}
+
+	/**
+	 * This action handles the archive configuration page.
+	 *
+	 * It displays the archive configuration page.
+	 * If this action is reached through a POST request, it stores all new
+	 * configuration values then sends a notification to the user.
+	 *
+	 * The options available on that page are:
+	 *   - duration to retain old article (default: 3)
+	 *   - number of article to retain per feed (default: 0)
+	 *   - refresh frequency (default: -2)
+	 *
+	 * @todo explain why the default value is -2 but this value does not
+	 *       exist in the drop-down list
+	 */
+	public function archivingAction() {
+		if (Minz_Request::isPost()) {
+			$this->view->conf->_old_entries(Minz_Request::param('old_entries', 3));
+			$this->view->conf->_keep_history_default(Minz_Request::param('keep_history_default', 0));
+			$this->view->conf->_ttl_default(Minz_Request::param('ttl_default', -2));
+			$this->view->conf->save();
+			invalidateHttpCache();
+
+			Minz_Request::good(_t('configuration_updated'),
+			                   array('c' => 'configure', 'a' => 'archiving'));
+		}
+
+		Minz_View::prependTitle(_t('archiving_configuration') . ' · ');
+
+		$entryDAO = FreshRSS_Factory::createEntryDao();
+		$this->view->nb_total = $entryDAO->count();
+		$this->view->size_user = $entryDAO->size();
+
+		if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+			$this->view->size_total = $entryDAO->size(true);
+		}
+	}
+
+	/**
+	 * This action handles the user queries configuration page.
+	 *
+	 * If this action is reached through a POST request, it stores all new
+	 * configuration values then sends a notification to the user then
+	 * redirect to the same page.
+	 * If this action is not reached through a POST request, it displays the
+	 * configuration page and verifies that every user query is runable by
+	 * checking if categories and feeds are still in use.
+	 */
+	public function queriesAction() {
+		if (Minz_Request::isPost()) {
+			$queries = Minz_Request::param('queries', array());
+
+			foreach ($queries as $key => $query) {
+				if (!$query['name']) {
+					$query['name'] = _t('query_number', $key + 1);
+				}
+			}
+			$this->view->conf->_queries($queries);
+			$this->view->conf->save();
+
+			Minz_Request::good(_t('configuration_updated'),
+			                   array('c' => 'configure', 'a' => 'queries'));
+		} else {
+			$this->view->query_get = array();
+			$cat_dao = new FreshRSS_CategoryDAO();
+			$feed_dao = FreshRSS_Factory::createFeedDao();
+			foreach ($this->view->conf->queries as $key => $query) {
+				if (!isset($query['get'])) {
+					continue;
+				}
+
+				switch ($query['get'][0]) {
+				case 'c':
+					$category = $cat_dao->searchById(substr($query['get'], 2));
+
+					$deprecated = true;
+					$cat_name = '';
+					if ($category) {
+						$cat_name = $category->name();
+						$deprecated = false;
+					}
+
+					$this->view->query_get[$key] = array(
+						'type' => 'category',
+						'name' => $cat_name,
+						'deprecated' => $deprecated,
+					);
+					break;
+				case 'f':
+					$feed = $feed_dao->searchById(substr($query['get'], 2));
+
+					$deprecated = true;
+					$feed_name = '';
+					if ($feed) {
+						$feed_name = $feed->name();
+						$deprecated = false;
+					}
+
+					$this->view->query_get[$key] = array(
+						'type' => 'feed',
+						'name' => $feed_name,
+						'deprecated' => $deprecated,
+					);
+					break;
+				case 's':
+					$this->view->query_get[$key] = array(
+						'type' => 'favorite',
+						'name' => 'favorite',
+						'deprecated' => false,
+					);
+					break;
+				case 'a':
+					$this->view->query_get[$key] = array(
+						'type' => 'all',
+						'name' => 'all',
+						'deprecated' => false,
+					);
+					break;
+				}
+			}
+		}
+
+		Minz_View::prependTitle(_t('queries') . ' · ');
+	}
+
+	/**
+	 * This action handles the creation of a user query.
+	 *
+	 * It gets the GET parameters and stores them in the configuration query
+	 * storage. Before it is saved, the unwanted parameters are unset to keep
+	 * lean data.
+	 */
+	public function addQueryAction() {
+		$whitelist = array('get', 'order', 'name', 'search', 'state');
+		$queries = $this->view->conf->queries;
+		$query = Minz_Request::params();
+		$query['name'] = _t('query_number', count($queries) + 1);
+		foreach ($query as $key => $value) {
+			if (!in_array($key, $whitelist)) {
+				unset($query[$key]);
+			}
+		}
+		if (!empty($query['state']) && $query['state'] & FreshRSS_Entry::STATE_STRICT) {
+			$query['state'] -= FreshRSS_Entry::STATE_STRICT;
+		}
+		$queries[] = $query;
+		$this->view->conf->_queries($queries);
+		$this->view->conf->save();
+
+		Minz_Request::good(_t('query_created', $query['name']),
+		                   array('c' => 'configure', 'a' => 'queries'));
+	}
+}

+ 167 - 0
app/Controllers/entryController.php

@@ -0,0 +1,167 @@
+<?php
+
+class FreshRSS_entry_Controller extends Minz_ActionController {
+	public function firstAction () {
+		if (!$this->view->loginOk) {
+			Minz_Error::error (
+				403,
+				array ('error' => array (Minz_Translate::t ('access_denied')))
+			);
+		}
+
+		$this->params = array ();
+		$output = Minz_Request::param('output', '');
+		if (($output != '') && ($this->view->conf->view_mode !== $output)) {
+			$this->params['output'] = $output;
+		}
+
+		$this->redirect = false;
+		$ajax = Minz_Request::param ('ajax');
+		if ($ajax) {
+			$this->view->_useLayout (false);
+		}
+	}
+
+	public function lastAction () {
+		$ajax = Minz_Request::param ('ajax');
+		if (!$ajax && $this->redirect) {
+			Minz_Request::forward (array (
+				'c' => 'index',
+				'a' => 'index',
+				'params' => $this->params
+			), true);
+		} else {
+			Minz_Request::_param ('ajax');
+		}
+	}
+
+	public function readAction () {
+		$this->redirect = true;
+
+		$id = Minz_Request::param ('id');
+		$get = Minz_Request::param ('get');
+		$nextGet = Minz_Request::param ('nextGet', $get); 
+		$idMax = Minz_Request::param ('idMax', 0);
+
+		$entryDAO = FreshRSS_Factory::createEntryDao();
+		if ($id == false) {
+			if (!Minz_Request::isPost()) {
+				return;
+			}
+
+			if (!$get) {
+				$entryDAO->markReadEntries ($idMax);
+			} else {
+				$typeGet = $get[0];
+				$get = substr ($get, 2);
+				switch ($typeGet) {
+					case 'c':
+						$entryDAO->markReadCat ($get, $idMax);
+						break;
+					case 'f':
+						$entryDAO->markReadFeed ($get, $idMax);
+						break;
+					case 's':
+						$entryDAO->markReadEntries ($idMax, true);
+						break;
+					case 'a':
+						$entryDAO->markReadEntries ($idMax);
+						break;
+				}
+				if ($nextGet !== 'a') {
+					$this->params['get'] = $nextGet;
+				}
+			}
+
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('feeds_marked_read')
+			);
+			Minz_Session::_param ('notification', $notif);
+		} else {
+			$is_read = (bool)(Minz_Request::param ('is_read', true));
+			$entryDAO->markRead ($id, $is_read);
+		}
+	}
+
+	public function bookmarkAction () {
+		$this->redirect = true;
+
+		$id = Minz_Request::param ('id');
+		if ($id) {
+			$entryDAO = FreshRSS_Factory::createEntryDao();
+			$entryDAO->markFavorite ($id, (bool)(Minz_Request::param ('is_favorite', true)));
+		}
+	}
+
+	public function optimizeAction() {
+		if (Minz_Request::isPost()) {
+			@set_time_limit(300);
+
+			// La table des entrées a tendance à grossir énormément
+			// Cette action permet d'optimiser cette table permettant de grapiller un peu de place
+			// Cette fonctionnalité n'est à appeler qu'occasionnellement
+			$entryDAO = FreshRSS_Factory::createEntryDao();
+			$entryDAO->optimizeTable();
+
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$feedDAO->updateCachedValues();
+
+			invalidateHttpCache();
+
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('optimization_complete')
+			);
+			Minz_Session::_param ('notification', $notif);
+		}
+
+		Minz_Request::forward(array(
+			'c' => 'configure',
+			'a' => 'archiving'
+		), true);
+	}
+
+	public function purgeAction() {
+		@set_time_limit(300);
+
+		$nb_month_old = max($this->view->conf->old_entries, 1);
+		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
+
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$feeds = $feedDAO->listFeeds();
+		$nbTotal = 0;
+
+		invalidateHttpCache();
+
+		foreach ($feeds as $feed) {
+			$feedHistory = $feed->keepHistory();
+			if ($feedHistory == -2) {	//default
+				$feedHistory = $this->view->conf->keep_history_default;
+			}
+			if ($feedHistory >= 0) {
+				$nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, $feedHistory);
+				if ($nb > 0) {
+					$nbTotal += $nb;
+					Minz_Log::record($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG);
+					//$feedDAO->updateLastUpdate($feed->id());
+				}
+			}
+		}
+
+		$feedDAO->updateCachedValues();
+
+		invalidateHttpCache();
+
+		$notif = array(
+			'type' => 'good',
+			'content' => Minz_Translate::t('purge_completed', $nbTotal)
+		);
+		Minz_Session::_param('notification', $notif);
+
+		Minz_Request::forward(array(
+			'c' => 'configure',
+			'a' => 'archiving'
+		), true);
+	}
+}

+ 38 - 0
app/Controllers/errorController.php

@@ -0,0 +1,38 @@
+<?php
+
+class FreshRSS_error_Controller extends Minz_ActionController {
+	public function indexAction() {
+		switch (Minz_Request::param('code')) {
+			case 403:
+				$this->view->code = 'Error 403 - Forbidden';
+				break;
+			case 404:
+				$this->view->code = 'Error 404 - Not found';
+				break;
+			case 500:
+				$this->view->code = 'Error 500 - Internal Server Error';
+				break;
+			case 503:
+				$this->view->code = 'Error 503 - Service Unavailable';
+				break;
+			default:
+				$this->view->code = 'Error 404 - Not found';
+		}
+
+		$errors = Minz_Request::param('logs', array());
+		$this->view->errorMessage = trim(implode($errors));
+		if ($this->view->errorMessage == '') {
+			switch(Minz_Request::param('code')) {
+				case 403:
+					$this->view->errorMessage = Minz_Translate::t('forbidden_access');
+					break;
+				case 404:
+				default:
+					$this->view->errorMessage = Minz_Translate::t('page_not_found');
+					break;
+			}
+		}
+
+		Minz_View::prependTitle($this->view->code . ' · ');
+	}
+}

+ 438 - 0
app/Controllers/feedController.php

@@ -0,0 +1,438 @@
+<?php
+
+class FreshRSS_feed_Controller extends Minz_ActionController {
+	public function firstAction () {
+		if (!$this->view->loginOk) {
+			// Token is useful in the case that anonymous refresh is forbidden
+			// and CRON task cannot be used with php command so the user can
+			// set a CRON task to refresh his feeds by using token inside url
+			$token = $this->view->conf->token;
+			$token_param = Minz_Request::param ('token', '');
+			$token_is_ok = ($token != '' && $token == $token_param);
+			$action = Minz_Request::actionName ();
+			if (!(($token_is_ok || Minz_Configuration::allowAnonymousRefresh()) &&
+				$action === 'actualize')
+			) {
+				Minz_Error::error (
+					403,
+					array ('error' => array (Minz_Translate::t ('access_denied')))
+				);
+			}
+		}
+	}
+
+	public function addAction () {
+		$url = Minz_Request::param('url_rss', false);
+
+		if ($url === false) {
+			Minz_Request::forward(array(
+				'c' => 'configure',
+				'a' => 'feed'
+			), true);
+		}
+
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$this->catDAO = new FreshRSS_CategoryDAO ();
+		$this->catDAO->checkDefault ();
+
+		if (Minz_Request::isPost()) {
+			@set_time_limit(300);
+
+
+			$cat = Minz_Request::param ('category', false);
+			if ($cat === 'nc') {
+				$new_cat = Minz_Request::param ('new_category');
+				if (empty($new_cat['name'])) {
+					$cat = false;
+				} else {
+					$cat = $this->catDAO->addCategory($new_cat);
+				}
+			}
+			if ($cat === false) {
+				$def_cat = $this->catDAO->getDefault ();
+				$cat = $def_cat->id ();
+			}
+
+			$user = Minz_Request::param ('http_user');
+			$pass = Minz_Request::param ('http_pass');
+			$params = array ();
+
+			$transactionStarted = false;
+			try {
+				$feed = new FreshRSS_Feed ($url);
+				$feed->_category ($cat);
+
+				$httpAuth = '';
+				if ($user != '' || $pass != '') {
+					$httpAuth = $user . ':' . $pass;
+				}
+				$feed->_httpAuth ($httpAuth);
+
+				$feed->load(true);
+
+				$values = array (
+					'url' => $feed->url (),
+					'category' => $feed->category (),
+					'name' => $feed->name (),
+					'website' => $feed->website (),
+					'description' => $feed->description (),
+					'lastUpdate' => time (),
+					'httpAuth' => $feed->httpAuth (),
+				);
+
+				if ($feedDAO->searchByUrl ($values['url'])) {
+					// on est déjà abonné à ce flux
+					$notif = array (
+						'type' => 'bad',
+						'content' => Minz_Translate::t ('already_subscribed', $feed->name ())
+					);
+					Minz_Session::_param ('notification', $notif);
+				} else {
+					$id = $feedDAO->addFeed ($values);
+					if (!$id) {
+						// problème au niveau de la base de données
+						$notif = array (
+							'type' => 'bad',
+							'content' => Minz_Translate::t ('feed_not_added', $feed->name ())
+						);
+						Minz_Session::_param ('notification', $notif);
+					} else {
+						$feed->_id ($id);
+						$feed->faviconPrepare();
+
+						$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
+
+						$entryDAO = FreshRSS_Factory::createEntryDao();
+						$entries = array_reverse($feed->entries());	//We want chronological order and SimplePie uses reverse order
+
+						// on calcule la date des articles les plus anciens qu'on accepte
+						$nb_month_old = $this->view->conf->old_entries;
+						$date_min = time () - (3600 * 24 * 30 * $nb_month_old);
+
+						//MySQL: http://docs.oracle.com/cd/E17952_01/refman-5.5-en/optimizing-innodb-transaction-management.html
+						//SQLite: http://stackoverflow.com/questions/1711631/how-do-i-improve-the-performance-of-sqlite
+						$preparedStatement = $entryDAO->addEntryPrepare();
+						$transactionStarted = true;
+						$feedDAO->beginTransaction();
+						// on ajoute les articles en masse sans vérification
+						foreach ($entries as $entry) {
+							$values = $entry->toArray();
+							$values['id_feed'] = $feed->id();
+							$values['id'] = min(time(), $entry->date(true)) . uSecString();
+							$values['is_read'] = $is_read;
+							$entryDAO->addEntry($values, $preparedStatement);
+						}
+						$feedDAO->updateLastUpdate($feed->id());
+						if ($transactionStarted) {
+							$feedDAO->commit();
+						}
+						$transactionStarted = false;
+
+						// ok, ajout terminé
+						$notif = array (
+							'type' => 'good',
+							'content' => Minz_Translate::t ('feed_added', $feed->name ())
+						);
+						Minz_Session::_param ('notification', $notif);
+
+						// permet de rediriger vers la page de conf du flux
+						$params['id'] = $feed->id ();
+					}
+				}
+			} catch (FreshRSS_BadUrl_Exception $e) {
+				Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
+				$notif = array (
+					'type' => 'bad',
+					'content' => Minz_Translate::t ('invalid_url', $url)
+				);
+				Minz_Session::_param ('notification', $notif);
+			} catch (FreshRSS_Feed_Exception $e) {
+				Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
+				$notif = array (
+					'type' => 'bad',
+					'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs')))
+				);
+				Minz_Session::_param ('notification', $notif);
+			} catch (Minz_FileNotExistException $e) {
+				// Répertoire de cache n'existe pas
+				Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
+				$notif = array (
+					'type' => 'bad',
+					'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs')))
+				);
+				Minz_Session::_param ('notification', $notif);
+			}
+			if ($transactionStarted) {
+				$feedDAO->rollBack ();
+			}
+
+			Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true);
+		} else {
+
+			// GET request so we must ask confirmation to user
+			Minz_View::prependTitle(Minz_Translate::t('add_rss_feed') . ' · ');
+			$this->view->categories = $this->catDAO->listCategories();
+			$this->view->feed = new FreshRSS_Feed($url);
+			try {
+				// We try to get some more information about the feed
+				$this->view->feed->load(true);
+				$this->view->load_ok = true;
+			} catch (Exception $e) {
+				$this->view->load_ok = false;
+			}
+
+			$feed = $feedDAO->searchByUrl($this->view->feed->url());
+			if ($feed) {
+				// Already subscribe so we redirect to the feed configuration page
+				$notif = array(
+					'type' => 'bad',
+					'content' => Minz_Translate::t(
+						'already_subscribed', $feed->name()
+					)
+				);
+				Minz_Session::_param('notification', $notif);
+
+				Minz_Request::forward(array(
+					'c' => 'configure',
+					'a' => 'feed',
+					'params' => array(
+						'id' => $feed->id()
+					)
+				), true);
+			}
+		}
+	}
+
+	public function truncateAction () {
+		if (Minz_Request::isPost ()) {
+			$id = Minz_Request::param ('id');
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$n = $feedDAO->truncate($id);
+			$notif = array(
+				'type' => $n === false ? 'bad' : 'good',
+				'content' => Minz_Translate::t ('n_entries_deleted', $n)
+			);
+			Minz_Session::_param ('notification', $notif);
+			invalidateHttpCache();
+			Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true);
+		}
+	}
+
+	public function actualizeAction () {
+		@set_time_limit(300);
+
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$entryDAO = FreshRSS_Factory::createEntryDao();
+
+		Minz_Session::_param('actualize_feeds', false);
+		$id = Minz_Request::param ('id');
+		$force = Minz_Request::param ('force', false);
+
+		// on créé la liste des flux à mettre à actualiser
+		// si on veut mettre un flux à jour spécifiquement, on le met
+		// dans la liste, mais seul (permet d'automatiser le traitement)
+		$feeds = array ();
+		if ($id) {
+			$feed = $feedDAO->searchById ($id);
+			if ($feed) {
+				$feeds = array ($feed);
+			}
+		} else {
+			$feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default);
+		}
+
+		// on calcule la date des articles les plus anciens qu'on accepte
+		$nb_month_old = max($this->view->conf->old_entries, 1);
+		$date_min = time () - (3600 * 24 * 30 * $nb_month_old);
+
+		$i = 0;
+		$flux_update = 0;
+		$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
+		foreach ($feeds as $feed) {
+			if (!$feed->lock()) {
+				Minz_Log::record('Feed already being actualized: ' . $feed->url(), Minz_Log::NOTICE);
+				continue;
+			}
+			try {
+				$url = $feed->url();
+				$feedHistory = $feed->keepHistory();
+
+				$feed->load(false);
+				$entries = array_reverse($feed->entries());	//We want chronological order and SimplePie uses reverse order
+				$hasTransaction = false;
+
+				if (count($entries) > 0) {
+					//For this feed, check last n entry GUIDs already in database
+					$existingGuids = array_fill_keys ($entryDAO->listLastGuidsByFeed ($feed->id (), count($entries) + 10), 1);
+					$useDeclaredDate = empty($existingGuids);
+
+					if ($feedHistory == -2) {	//default
+						$feedHistory = $this->view->conf->keep_history_default;
+					}
+
+					$preparedStatement = $entryDAO->addEntryPrepare();
+					$hasTransaction = true;
+					$feedDAO->beginTransaction();
+
+					// On ne vérifie pas strictement que l'article n'est pas déjà en BDD
+					// La BDD refusera l'ajout car (id_feed, guid) doit être unique
+					foreach ($entries as $entry) {
+						$eDate = $entry->date(true);
+						if ((!isset($existingGuids[$entry->guid()])) &&
+							(($feedHistory != 0) || ($eDate  >= $date_min))) {
+							$values = $entry->toArray();
+							//Use declared date at first import, otherwise use discovery date
+							$values['id'] = ($useDeclaredDate || $eDate < $date_min) ?
+								min(time(), $eDate) . uSecString() :
+								uTimeString();
+							$values['is_read'] = $is_read;
+							$entryDAO->addEntry($values, $preparedStatement);
+						}
+					}
+				}
+
+				if (($feedHistory >= 0) && (rand(0, 30) === 1)) {
+					if (!$hasTransaction) {
+						$feedDAO->beginTransaction();
+					}
+					$nb = $feedDAO->cleanOldEntries ($feed->id (), $date_min, max($feedHistory, count($entries) + 10));
+					if ($nb > 0) {
+						Minz_Log::record ($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG);
+					}
+				}
+
+				// on indique que le flux vient d'être mis à jour en BDD
+				$feedDAO->updateLastUpdate ($feed->id (), 0, $hasTransaction);
+				if ($hasTransaction) {
+					$feedDAO->commit();
+				}
+				$flux_update++;
+				if (($feed->url() !== $url)) {	//HTTP 301 Moved Permanently
+					Minz_Log::record('Feed ' . $url . ' moved permanently to ' . $feed->url(), Minz_Log::NOTICE);
+					$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
+				}
+			} catch (FreshRSS_Feed_Exception $e) {
+				Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
+				$feedDAO->updateLastUpdate ($feed->id (), 1);
+			}
+
+			$feed->faviconPrepare();
+			$feed->unlock();
+			unset($feed);
+
+			// On arrête à 10 flux pour ne pas surcharger le serveur
+			// sauf si le paramètre $force est à vrai
+			$i++;
+			if ($i >= 10 && !$force) {
+				break;
+			}
+		}
+
+		$url = array ();
+		if ($flux_update === 1) {
+			// on a mis un seul flux à jour
+			$feed = reset ($feeds);
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('feed_actualized', $feed->name ())
+			);
+		} elseif ($flux_update > 1) {
+			// plusieurs flux on été mis à jour
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('n_feeds_actualized', $flux_update)
+			);
+		} else {
+			// aucun flux n'a été mis à jour, oups
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('no_feed_to_refresh')
+			);
+		}
+
+		if ($i === 1) {
+			// Si on a voulu mettre à jour qu'un flux
+			// on filtre l'affichage par ce flux
+			$feed = reset ($feeds);
+			$url['params'] = array ('get' => 'f_' . $feed->id ());
+		}
+
+		if (Minz_Request::param ('ajax', 0) === 0) {
+			Minz_Session::_param ('notification', $notif);
+			Minz_Request::forward ($url, true);
+		} else {
+			// Une requête Ajax met un seul flux à jour.
+			// Comme en principe plusieurs requêtes ont lieu,
+			// on indique que "plusieurs flux ont été mis à jour".
+			// Cela permet d'avoir une notification plus proche du
+			// ressenti utilisateur
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('feeds_actualized')
+			);
+			Minz_Session::_param ('notification', $notif);
+			// et on désactive le layout car ne sert à rien
+			$this->view->_useLayout (false);
+		}
+	}
+
+	public function deleteAction () {
+		if (Minz_Request::isPost ()) {
+			$type = Minz_Request::param ('type', 'feed');
+			$id = Minz_Request::param ('id');
+
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			if ($type == 'category') {
+				// List feeds to remove then related user queries.
+				$feeds = $feedDAO->listByCategory($id);
+
+				if ($feedDAO->deleteFeedByCategory ($id)) {
+					// Remove related queries
+					foreach ($feeds as $feed) {
+						$this->view->conf->remove_query_by_get('f_' . $feed->id());
+					}
+					$this->view->conf->save();
+
+					$notif = array (
+						'type' => 'good',
+						'content' => Minz_Translate::t ('category_emptied')
+					);
+					//TODO: Delete old favicons
+				} else {
+					$notif = array (
+						'type' => 'bad',
+						'content' => Minz_Translate::t ('error_occured')
+					);
+				}
+			} else {
+				if ($feedDAO->deleteFeed ($id)) {
+					// Remove related queries
+					$this->view->conf->remove_query_by_get('f_' . $id);
+					$this->view->conf->save();
+
+					$notif = array (
+						'type' => 'good',
+						'content' => Minz_Translate::t ('feed_deleted')
+					);
+					//TODO: Delete old favicon
+				} else {
+					$notif = array (
+						'type' => 'bad',
+						'content' => Minz_Translate::t ('error_occured')
+					);
+				}
+			}
+
+			Minz_Session::_param ('notification', $notif);
+
+			$redirect_url = Minz_Request::param('r', false, true);
+			if ($redirect_url) {
+				Minz_Request::forward($redirect_url);
+			} elseif ($type == 'category') {
+				Minz_Request::forward(array ('c' => 'configure', 'a' => 'categorize'), true);
+			} else {
+				Minz_Request::forward(array ('c' => 'configure', 'a' => 'feed'), true);
+			}
+		}
+	}
+}

+ 447 - 0
app/Controllers/importExportController.php

@@ -0,0 +1,447 @@
+<?php
+
+class FreshRSS_importExport_Controller extends Minz_ActionController {
+	public function firstAction() {
+		if (!$this->view->loginOk) {
+			Minz_Error::error(
+				403,
+				array('error' => array(_t('access_denied')))
+			);
+		}
+
+		require_once(LIB_PATH . '/lib_opml.php');
+
+		$this->catDAO = new FreshRSS_CategoryDAO();
+		$this->entryDAO = FreshRSS_Factory::createEntryDao();
+		$this->feedDAO = FreshRSS_Factory::createFeedDao();
+	}
+
+	public function indexAction() {
+		$this->view->categories = $this->catDAO->listCategories();
+		$this->view->feeds = $this->feedDAO->listFeeds();
+
+		Minz_View::prependTitle(_t('import_export') . ' · ');
+	}
+
+	public function importAction() {
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+		}
+
+		$file = $_FILES['file'];
+		$status_file = $file['error'];
+
+		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'));
+		}
+
+		@set_time_limit(300);
+
+		$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,
+							zip_entry_filesize($zipfile)
+						);
+					}
+				}
+			}
+
+			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']);
+		}
+
+		// 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);
+		}
+
+		// 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) {
+		// A *very* basic guess file type function. Only based on filename
+		// That's could be improved but should be enough, at least for a first
+		// implementation.
+
+		if (substr_compare($filename, '.zip', -4) === 0) {
+			return 'zip';
+		} elseif (substr_compare($filename, '.opml', -5) === 0 ||
+		          substr_compare($filename, '.xml', -4) === 0) {
+			return 'opml';
+		} elseif (substr_compare($filename, '.json', -5) === 0 &&
+		          strpos($filename, 'starred') !== false) {
+			return 'json_starred';
+		} elseif (substr_compare($filename, '.json', -5) === 0) {
+			return 'json_feed';
+		} else {
+			return 'unknown';
+		}
+	}
+
+	private function importOpml($opml_file) {
+		$opml_array = array();
+		try {
+			$opml_array = libopml_parse_string($opml_file);
+		} catch (LibOPML_Exception $e) {
+			Minz_Log::warning($e->getMessage());
+			return true;
+		}
+
+		$this->catDAO->checkDefault();
+
+		return $this->addOpmlElements($opml_array['body']);
+	}
+
+	private function addOpmlElements($opml_elements, $parent_cat = null) {
+		$error = false;
+		foreach ($opml_elements as $elt) {
+			$res = false;
+			if (isset($elt['xmlUrl'])) {
+				$res = $this->addFeedOpml($elt, $parent_cat);
+			} else {
+				$res = $this->addCategoryOpml($elt, $parent_cat);
+			}
+
+			if (!$error && $res) {
+				// oops: there is at least one error!
+				$error = $res;
+			}
+		}
+
+		return $error;
+	}
+
+	private function addFeedOpml($feed_elt, $parent_cat) {
+		if (is_null($parent_cat)) {
+			// This feed has no parent category so we get the default one
+			$parent_cat = $this->catDAO->getDefault()->name();
+		}
+
+		$cat = $this->catDAO->searchByName($parent_cat);
+
+		if (!$cat) {
+			return true;
+		}
+
+		// We get different useful information
+		$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
+		$name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text']);
+		$website = '';
+		if (isset($feed_elt['htmlUrl'])) {
+			$website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl']);
+		}
+		$description = '';
+		if (isset($feed_elt['description'])) {
+			$description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description']);
+		}
+
+		$error = false;
+		try {
+			// Create a Feed object and add it in DB
+			$feed = new FreshRSS_Feed($url);
+			$feed->_category($cat->id());
+			$feed->_name($name);
+			$feed->_website($website);
+			$feed->_description($description);
+
+			// addFeedObject checks if feed is already in DB so nothing else to
+			// check here
+			$id = $this->feedDAO->addFeedObject($feed);
+			$error = ($id === false);
+		} catch (FreshRSS_Feed_Exception $e) {
+			Minz_Log::warning($e->getMessage());
+			$error = true;
+		}
+
+		return $error;
+	}
+
+	private function addCategoryOpml($cat_elt, $parent_cat) {
+		// Create a new Category object
+		$cat = new FreshRSS_Category(Minz_Helper::htmlspecialchars_utf8($cat_elt['text']));
+
+		$id = $this->catDAO->addCategoryObject($cat);
+		$error = ($id === false);
+
+		if (isset($cat_elt['@outlines'])) {
+			// Our cat_elt contains more categories or more feeds, so we
+			// add them recursively.
+			// Note: FreshRSS does not support yet category arborescence
+			$res = $this->addOpmlElements($cat_elt['@outlines'], $cat->name());
+			if (!$error && $res) {
+				$error = true;
+			}
+		}
+
+		return $error;
+	}
+
+	private function importArticles($article_file, $starred = false) {
+		$article_object = json_decode($article_file, true);
+		if (is_null($article_object)) {
+			Minz_Log::warning('Try to import a non-JSON file');
+			return true;
+		}
+
+		$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
+
+		$google_compliant = (
+			strpos($article_object['id'], 'com.google') !== false
+		);
+
+		$error = false;
+		$article_to_feed = array();
+
+		// First, we check feeds of articles are in DB (and add them if needed).
+		foreach ($article_object['items'] as $item) {
+			$feed = $this->addFeedArticles($item['origin'], $google_compliant);
+			if (is_null($feed)) {
+				$error = true;
+			} else {
+				$article_to_feed[$item['id']] = $feed->id();
+			}
+		}
+
+		// Then, articles are imported.
+		$prepared_statement = $this->entryDAO->addEntryPrepare();
+		$this->entryDAO->beginTransaction();
+		foreach ($article_object['items'] as $item) {
+			if (!isset($article_to_feed[$item['id']])) {
+				continue;
+			}
+
+			$feed_id = $article_to_feed[$item['id']];
+			$author = isset($item['author']) ? $item['author'] : '';
+			$key_content = ($google_compliant && !isset($item['content'])) ?
+			               'summary' : 'content';
+			$tags = $item['categories'];
+			if ($google_compliant) {
+				$tags = array_filter($tags, function($var) {
+					return strpos($var, '/state/com.google') === false;
+				});
+			}
+
+			$entry = new FreshRSS_Entry(
+				$feed_id, $item['id'], $item['title'], $author,
+				$item[$key_content]['content'], $item['alternate'][0]['href'],
+				$item['published'], $is_read, $starred
+			);
+			$entry->_id(min(time(), $entry->date(true)) . uSecString());
+			$entry->_tags($tags);
+
+			$values = $entry->toArray();
+			$id = $this->entryDAO->addEntry($values, $prepared_statement);
+
+			if (!$error && ($id === false)) {
+				$error = true;
+			}
+		}
+		$this->entryDAO->commit();
+
+		return $error;
+	}
+
+	private function addFeedArticles($origin, $google_compliant) {
+		$default_cat = $this->catDAO->getDefault();
+
+		$return = null;
+		$key = $google_compliant ? 'htmlUrl' : 'feedUrl';
+		$url = $origin[$key];
+		$name = $origin['title'];
+		$website = $origin['htmlUrl'];
+
+		try {
+			// Create a Feed object and add it in DB
+			$feed = new FreshRSS_Feed($url);
+			$feed->_category($default_cat->id());
+			$feed->_name($name);
+			$feed->_website($website);
+
+			// addFeedObject checks if feed is already in DB so nothing else to
+			// check here
+			$id = $this->feedDAO->addFeedObject($feed);
+
+			if ($id !== false) {
+				$feed->_id($id);
+				$return = $feed;
+			}
+		} catch (FreshRSS_Feed_Exception $e) {
+			Minz_Log::warning($e->getMessage());
+		}
+
+		return $return;
+	}
+
+	public function exportAction() {
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+		}
+
+		$this->view->_useLayout(false);
+
+		$export_opml = Minz_Request::param('export_opml', false);
+		$export_starred = Minz_Request::param('export_starred', false);
+		$export_feeds = Minz_Request::param('export_feeds', array());
+
+		$export_files = array();
+		if ($export_opml) {
+			$export_files['feeds.opml'] = $this->generateOpml();
+		}
+
+		if ($export_starred) {
+			$export_files['starred.json'] = $this->generateArticles('starred');
+		}
+
+		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
+				);
+			}
+		}
+
+		$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);
+		}
+	}
+
+	private function generateOpml() {
+		$list = array();
+		foreach ($this->catDAO->listCategories() as $key => $cat) {
+			$list[$key]['name'] = $cat->name();
+			$list[$key]['feeds'] = $this->feedDAO->listByCategory($cat->id());
+		}
+
+		$this->view->categories = $list;
+		return $this->view->helperToString('export/opml');
+	}
+
+	private function generateArticles($type, $feed = NULL) {
+		$this->view->categories = $this->catDAO->listCategories();
+
+		if ($type == 'starred') {
+			$this->view->list_title = _t('starred_list');
+			$this->view->type = 'starred';
+			$unread_fav = $this->entryDAO->countUnreadReadFavorites();
+			$this->view->entries = $this->entryDAO->listWhere(
+				's', '', FreshRSS_Entry::STATE_ALL, 'ASC',
+				$unread_fav['all']
+			);
+		} elseif ($type == 'feed' && !is_null($feed)) {
+			$this->view->list_title = _t('feed_list', $feed->name());
+			$this->view->type = 'feed/' . $feed->id();
+			$this->view->entries = $this->entryDAO->listWhere(
+				'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
+				$this->view->conf->posts_per_page
+			);
+			$this->view->feed = $feed;
+		}
+
+		return $this->view->helperToString('export/articles');
+	}
+
+	private function exportZip($files) {
+		if (!extension_loaded('zip')) {
+			throw new Exception();
+		}
+
+		// From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly
+		$zip_file = tempnam('tmp', 'zip');
+		$zip = new ZipArchive();
+		$zip->open($zip_file, ZipArchive::OVERWRITE);
+
+		foreach ($files as $filename => $content) {
+			$zip->addFromString($filename, $content);
+		}
+
+		// Close and send to user
+		$zip->close();
+		header('Content-Type: application/zip');
+		header('Content-Length: ' . filesize($zip_file));
+		header('Content-Disposition: attachment; filename="freshrss_export.zip"');
+		readfile($zip_file);
+		unlink($zip_file);
+	}
+
+	private function exportFile($filename, $content, $type) {
+		if ($type === 'unknown') {
+			return;
+		}
+
+		$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);
+	}
+}

+ 498 - 0
app/Controllers/indexController.php

@@ -0,0 +1,498 @@
+<?php
+
+class FreshRSS_index_Controller extends Minz_ActionController {
+	private $nb_not_read_cat = 0;
+
+	public function indexAction () {
+		$output = Minz_Request::param ('output');
+		$token = $this->view->conf->token;
+
+		// check if user is logged in
+		if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) {
+			$token_param = Minz_Request::param ('token', '');
+			$token_is_ok = ($token != '' && $token === $token_param);
+			if ($output === 'rss' && !$token_is_ok) {
+				Minz_Error::error (
+					403,
+					array ('error' => array (Minz_Translate::t ('access_denied')))
+				);
+				return;
+			} elseif ($output !== 'rss') {
+				// "hard" redirection is not required, just ask dispatcher to
+				// forward to the login form without 302 redirection
+				Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'));
+				return;
+			}
+		}
+
+		$params = Minz_Request::params ();
+		if (isset ($params['search'])) {
+			$params['search'] = urlencode ($params['search']);
+		}
+
+		$this->view->url = array (
+			'c' => 'index',
+			'a' => 'index',
+			'params' => $params
+		);
+
+		if ($output === 'rss') {
+			// no layout for RSS output
+			$this->view->_useLayout (false);
+			header('Content-Type: application/rss+xml; charset=utf-8');
+		} elseif ($output === 'global') {
+			Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
+		}
+
+		$catDAO = new FreshRSS_CategoryDAO();
+		$entryDAO = FreshRSS_Factory::createEntryDao();
+
+		$this->view->cat_aside = $catDAO->listCategories ();
+		$this->view->nb_favorites = $entryDAO->countUnreadReadFavorites ();
+		$this->view->nb_not_read = FreshRSS_CategoryDAO::CountUnreads($this->view->cat_aside, 1);
+		$this->view->currentName = '';
+
+		$this->view->get_c = '';
+		$this->view->get_f = '';
+
+		$get = Minz_Request::param ('get', 'a');
+		$getType = $get[0];
+		$getId = substr ($get, 2);
+		if (!$this->checkAndProcessType ($getType, $getId)) {
+			Minz_Log::record ('Not found [' . $getType . '][' . $getId . ']', Minz_Log::DEBUG);
+			Minz_Error::error (
+				404,
+				array ('error' => array (Minz_Translate::t ('page_not_found')))
+			);
+			return;
+		}
+
+		// mise à jour des titres
+		$this->view->rss_title = $this->view->currentName . ' | ' . Minz_View::title();
+		Minz_View::prependTitle(
+			($this->nb_not_read_cat > 0 ? '(' . formatNumber($this->nb_not_read_cat) . ') ' : '') .
+			$this->view->currentName .
+			' · '
+		);
+
+		// On récupère les différents éléments de filtrage
+		$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', '');
+
+		$ajax_request = Minz_Request::param('ajax', false);
+		if ($output === 'reader') {
+			$nb = max(1, round($nb / 2));
+		}
+
+		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;
+					break;
+				case 's':
+					// This is deprecated. The favorite button does not exist anymore
+					$hasUnread = $this->view->nb_favorites['unread'] > 0;
+					break;
+				case 'c':
+					$hasUnread = (!isset($this->view->cat_aside[$getId])) || ($this->view->cat_aside[$getId]->nbNotRead() > 0);
+					break;
+				case 'f':
+					$myFeed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
+					$hasUnread = ($myFeed === null) || ($myFeed->nbNotRead() > 0);
+					break;
+				default:
+					$hasUnread = true;
+					break;
+			}
+			if (!$hasUnread && ($state_param === null)) {
+				$this->view->state = FreshRSS_Entry::STATE_ALL;
+			}
+		}
+
+		$today = @strtotime('today');
+		$this->view->today = $today;
+
+		// on calcule la date des articles les plus anciens qu'on affiche
+		$nb_month_old = $this->view->conf->old_entries;
+		$date_min = $today - (3600 * 24 * 30 * $nb_month_old);	//Do not use a fast changing value such as time() to allow SQL caching
+		$keepHistoryDefault = $this->view->conf->keep_history_default;
+
+		try {
+			$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 ($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 {
+					$feedDAO->updateCachedValues();
+				} catch (Exception $ex) {
+					Minz_Log::record('Failed to automatically correct nbNotRead! ' + $ex->getMessage(), Minz_Log::NOTICE);
+				}
+				$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  = '';
+			} else {	//We have more elements for pagination
+				$lastEntry = array_pop($entries);
+				$this->view->nextId  = $lastEntry->id();
+			}
+
+			$this->view->entries = $entries;
+		} catch (FreshRSS_EntriesGetter_Exception $e) {
+			Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
+			Minz_Error::error (
+				404,
+				array ('error' => array (Minz_Translate::t ('page_not_found')))
+			);
+		}
+	}
+
+	/*
+	 * Vérifie que la catégorie / flux sélectionné existe
+	 * + Initialise correctement les variables de vue get_c et get_f
+	 * + Met à jour la variable $this->nb_not_read_cat
+	 */
+	private function checkAndProcessType ($getType, $getId) {
+		switch ($getType) {
+			case 'a':
+				$this->view->currentName = Minz_Translate::t ('your_rss_feeds');
+				$this->nb_not_read_cat = $this->view->nb_not_read;
+				$this->view->get_c = $getType;
+				return true;
+			case 's':
+				$this->view->currentName = Minz_Translate::t ('your_favorites');
+				$this->nb_not_read_cat = $this->view->nb_favorites['unread'];
+				$this->view->get_c = $getType;
+				return true;
+			case 'c':
+				$cat = isset($this->view->cat_aside[$getId]) ? $this->view->cat_aside[$getId] : null;
+				if ($cat === null) {
+					$catDAO = new FreshRSS_CategoryDAO();
+					$cat = $catDAO->searchById($getId);
+				}
+				if ($cat) {
+					$this->view->currentName = $cat->name ();
+					$this->nb_not_read_cat = $cat->nbNotRead ();
+					$this->view->get_c = $getId;
+					return true;
+				} else {
+					return false;
+				}
+			case 'f':
+				$feed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
+				if (empty($feed)) {
+					$feedDAO = FreshRSS_Factory::createFeedDao();
+					$feed = $feedDAO->searchById($getId);
+				}
+				if ($feed) {
+					$this->view->currentName = $feed->name ();
+					$this->nb_not_read_cat = $feed->nbNotRead ();
+					$this->view->get_f = $getId;
+					$this->view->get_c = $feed->category ();
+					return true;
+				} else {
+					return false;
+				}
+			default:
+				return false;
+		}
+	}
+	
+	public function aboutAction () {
+		Minz_View::prependTitle (Minz_Translate::t ('about') . ' · ');
+	}
+
+	public function logsAction () {
+		if (!$this->view->loginOk) {
+			Minz_Error::error (
+				403,
+				array ('error' => array (Minz_Translate::t ('access_denied')))
+			);
+		}
+
+		Minz_View::prependTitle (Minz_Translate::t ('logs') . ' · ');
+
+		if (Minz_Request::isPost ()) {
+			FreshRSS_LogDAO::truncate();
+		}
+
+		$logs = FreshRSS_LogDAO::lines();	//TODO: ask only the necessary lines
+
+		//gestion pagination
+		$page = Minz_Request::param ('page', 1);
+		$this->view->logsPaginator = new Minz_Paginator ($logs);
+		$this->view->logsPaginator->_nbItemsPerPage (50);
+		$this->view->logsPaginator->_currentPage ($page);
+	}
+
+	public function loginAction () {
+		$this->view->_useLayout (false);
+
+		$url = 'https://verifier.login.persona.org/verify';
+		$assert = Minz_Request::param ('assertion');
+		$params = 'assertion=' . $assert . '&audience=' .
+			  urlencode (Minz_Url::display (null, 'php', true));
+		$ch = curl_init ();
+		$options = array (
+			CURLOPT_URL => $url,
+			CURLOPT_RETURNTRANSFER => TRUE,
+			CURLOPT_POST => 2,
+			CURLOPT_POSTFIELDS => $params
+		);
+		curl_setopt_array ($ch, $options);
+		$result = curl_exec ($ch);
+		curl_close ($ch);
+
+		$res = json_decode ($result, true);
+
+		$loginOk = false;
+		$reason = '';
+		if ($res['status'] === 'okay') {
+			$email = filter_var($res['email'], FILTER_VALIDATE_EMAIL);
+			if ($email != '') {
+				$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
+				if (($currentUser = @file_get_contents($personaFile)) !== false) {
+					$currentUser = trim($currentUser);
+					if (ctype_alnum($currentUser)) {
+						try {
+							$this->conf = new FreshRSS_Configuration($currentUser);
+							$loginOk = strcasecmp($email, $this->conf->mail_login) === 0;
+						} catch (Minz_Exception $e) {
+							$reason = 'Invalid configuration for user [' . $currentUser . ']! ' . $e->getMessage();	//Permission denied or conf file does not exist
+						}
+					} else {
+						$reason = 'Invalid username format [' . $currentUser . ']!';
+					}
+				}
+			} else {
+				$reason = 'Invalid email format [' . $res['email'] . ']!';
+			}
+		}
+		if ($loginOk) {
+			Minz_Session::_param('currentUser', $currentUser);
+			Minz_Session::_param ('mail', $email);
+			$this->view->loginOk = true;
+			invalidateHttpCache();
+		} else {
+			$res = array ();
+			$res['status'] = 'failure';
+			$res['reason'] = $reason == '' ? Minz_Translate::t ('invalid_login') : $reason;
+			Minz_Log::record ('Persona: ' . $res['reason'], Minz_Log::WARNING);
+		}
+
+		header('Content-Type: application/json; charset=UTF-8');
+		$this->view->res = json_encode ($res);
+	}
+
+	public function logoutAction () {
+		$this->view->_useLayout(false);
+		invalidateHttpCache();
+		Minz_Session::_param('currentUser');
+		Minz_Session::_param('mail');
+		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 ($this->view->loginOk) {
+			Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+		}
+
+		if (Minz_Request::isPost()) {
+			$ok = false;
+			$nonce = Minz_Session::param('nonce');
+			$username = Minz_Request::param('username', '');
+			$c = Minz_Request::param('challenge', '');
+			if (ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce)) {
+				if (!function_exists('password_verify')) {
+					include_once(LIB_PATH . '/password_compat.php');
+				}
+				try {
+					$conf = new FreshRSS_Configuration($username);
+					$s = $conf->passwordHash;
+					$ok = password_verify($nonce . $s, $c);
+					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);
+					}
+				} catch (Minz_Exception $me) {
+					Minz_Log::record('Login failure: ' . $me->getMessage(), Minz_Log::WARNING);
+				}
+			} else {
+				Minz_Log::record('Invalid credential parameters: user=' . $username . ' challenge=' . $c . ' nonce=' . $nonce, Minz_Log::DEBUG);
+			}
+			if (!$ok) {
+				$notif = array(
+					'type' => 'bad',
+					'content' => Minz_Translate::t('invalid_login')
+				);
+				Minz_Session::_param('notification', $notif);
+			}
+			$this->view->_useLayout(false);
+			Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+		} elseif (Minz_Configuration::unsafeAutologinEnabled() && isset($_GET['u']) && isset($_GET['p'])) {
+			Minz_Session::_param('currentUser');
+			Minz_Session::_param('mail');
+			Minz_Session::_param('passwordHash');
+			$username = ctype_alnum($_GET['u']) ? $_GET['u'] : '';
+			$passwordPlain = $_GET['p'];
+			Minz_Request::_param('p');	//Discard plain-text password ASAP
+			$_GET['p'] = '';
+			if (!function_exists('password_verify')) {
+				include_once(LIB_PATH . '/password_compat.php');
+			}
+			try {
+				$conf = new FreshRSS_Configuration($username);
+				$s = $conf->passwordHash;
+				$ok = password_verify($passwordPlain, $s);
+				unset($passwordPlain);
+				if ($ok) {
+					Minz_Session::_param('currentUser', $username);
+					Minz_Session::_param('passwordHash', $s);
+				} else {
+					Minz_Log::record('Unsafe password mismatch for user ' . $username, Minz_Log::WARNING);
+				}
+			} catch (Minz_Exception $me) {
+				Minz_Log::record('Unsafe login failure: ' . $me->getMessage(), Minz_Log::WARNING);
+			}
+			Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+		} elseif (!Minz_Configuration::canLogIn()) {
+			Minz_Error::error (
+				403,
+				array ('error' => array (Minz_Translate::t ('access_denied')))
+			);
+		}
+		invalidateHttpCache();
+	}
+
+	public function formLogoutAction () {
+		$this->view->_useLayout(false);
+		invalidateHttpCache();
+		Minz_Session::_param('currentUser');
+		Minz_Session::_param('mail');
+		Minz_Session::_param('passwordHash');
+		self::deleteLongTermCookie();
+		Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+	}
+
+	public function resetAuthAction() {
+		Minz_View::prependTitle(_t('auth_reset') . ' · ');
+		Minz_View::appendScript(Minz_Url::display(
+			'/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
+		));
+
+		$this->view->no_form = false;
+		// Enable changement of auth only if Persona!
+		if (Minz_Configuration::authType() != 'persona') {
+			$this->view->message = array(
+				'status' => 'bad',
+				'title' => _t('damn'),
+				'body' => _t('auth_not_persona')
+			);
+			$this->view->no_form = true;
+			return;
+		}
+
+		$conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
+		// Admin user must have set its master password.
+		if (!$conf->passwordHash) {
+			$this->view->message = array(
+				'status' => 'bad',
+				'title' => _t('damn'),
+				'body' => _t('auth_no_password_set')
+			);
+			$this->view->no_form = true;
+			return;
+		}
+
+		invalidateHttpCache();
+
+		if (Minz_Request::isPost()) {
+			$nonce = Minz_Session::param('nonce');
+			$username = Minz_Request::param('username', '');
+			$c = Minz_Request::param('challenge', '');
+			if (!(ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce))) {
+				Minz_Log::debug('Invalid credential parameters:' .
+				                ' user=' . $username .
+				                ' challenge=' . $c .
+				                ' nonce=' . $nonce);
+				Minz_Request::bad(_t('invalid_login'),
+				                  array('c' => 'index', 'a' => 'resetAuth'));
+			}
+
+			if (!function_exists('password_verify')) {
+				include_once(LIB_PATH . '/password_compat.php');
+			}
+
+			$s = $conf->passwordHash;
+			$ok = password_verify($nonce . $s, $c);
+			if ($ok) {
+				Minz_Configuration::_authType('form');
+				$ok = Minz_Configuration::writeFile();
+
+				if ($ok) {
+					Minz_Request::good(_t('auth_form_set'));
+				} else {
+					Minz_Request::bad(_t('auth_form_not_set'),
+				                      array('c' => 'index', 'a' => 'resetAuth'));
+				}
+			} else {
+				Minz_Log::debug('Password mismatch for user ' . $username .
+				                ', nonce=' . $nonce . ', c=' . $c);
+
+				Minz_Request::bad(_t('invalid_login'),
+				                  array('c' => 'index', 'a' => 'resetAuth'));
+			}
+		}
+	}
+}

+ 46 - 0
app/Controllers/javascriptController.php

@@ -0,0 +1,46 @@
+<?php
+
+class FreshRSS_javascript_Controller extends Minz_ActionController {
+	public function firstAction () {
+		$this->view->_useLayout (false);
+	}
+
+	public function actualizeAction () {
+		header('Content-Type: text/javascript; charset=UTF-8');
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$this->view->feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default);
+	}
+
+	public function nbUnreadsPerFeedAction() {
+		header('Content-Type: application/json; charset=UTF-8');
+		$catDAO = new FreshRSS_CategoryDAO();
+		$this->view->categories = $catDAO->listCategories(true, false);
+	}
+
+	//For Web-form login
+	public function nonceAction() {
+		header('Content-Type: application/json; charset=UTF-8');
+		header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T'));
+		header('Expires: 0');
+		header('Cache-Control: private, no-cache, no-store, must-revalidate');
+		header('Pragma: no-cache');
+
+		$user = isset($_GET['user']) ? $_GET['user'] : '';
+		if (ctype_alnum($user)) {
+			try {
+				$conf = new FreshRSS_Configuration($user);
+				$s = $conf->passwordHash;
+				if (strlen($s) >= 60) {
+					$this->view->salt1 = substr($s, 0, 29);	//CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z".
+					$this->view->nonce = sha1(Minz_Configuration::salt() . uniqid(mt_rand(), true));
+					Minz_Session::_param('nonce', $this->view->nonce);
+					return;	//Success
+				}
+			} catch (Minz_Exception $me) {
+				Minz_Log::record('Nonce failure: ' . $me->getMessage(), Minz_Log::WARNING);
+			}
+		}
+		$this->view->nonce = '';	//Failure
+		$this->view->salt1 = '';
+	}
+}

+ 129 - 0
app/Controllers/statsController.php

@@ -0,0 +1,129 @@
+<?php
+
+/**
+ * Controller to handle application statistics.
+ */
+class FreshRSS_stats_Controller extends Minz_ActionController {
+
+	/**
+	 * This action handles the statistic main page.
+	 *
+	 * It displays the statistic main page.
+	 * The values computed to display the page are:
+	 *   - repartition of read/unread/favorite/not favorite
+	 *   - number of article per day
+	 *   - number of feed by category
+	 *   - number of article by category
+	 *   - list of most prolific feed
+	 */
+	public function indexAction() {
+		$statsDAO = FreshRSS_Factory::createStatsDAO();
+		Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
+		$this->view->repartition = $statsDAO->calculateEntryRepartition();
+		$this->view->count = $statsDAO->calculateEntryCount();
+		$this->view->feedByCategory = $statsDAO->calculateFeedByCategory();
+		$this->view->entryByCategory = $statsDAO->calculateEntryByCategory();
+		$this->view->topFeed = $statsDAO->calculateTopFeed();
+	}
+
+	/**
+	 * This action handles the idle feed statistic page.
+	 *
+	 * It displays the list of idle feed for different period. The supported
+	 * periods are:
+	 *   - last year
+	 *   - last 6 months
+	 *   - last 3 months
+	 *   - last month
+	 *   - last week
+	 */
+	public function idleAction() {
+		$statsDAO = FreshRSS_Factory::createStatsDAO();
+		$feeds = $statsDAO->calculateFeedLastDate();
+		$idleFeeds = array(
+			'last_year' => array(),
+			'last_6_month' => array(),
+			'last_3_month' => array(),
+			'last_month' => array(),
+			'last_week' => array(),
+		);
+		$now = new \DateTime();
+		$feedDate = clone $now;
+		$lastWeek = clone $now;
+		$lastWeek->modify('-1 week');
+		$lastMonth = clone $now;
+		$lastMonth->modify('-1 month');
+		$last3Month = clone $now;
+		$last3Month->modify('-3 month');
+		$last6Month = clone $now;
+		$last6Month->modify('-6 month');
+		$lastYear = clone $now;
+		$lastYear->modify('-1 year');
+
+		foreach ($feeds as $feed) {
+			$feedDate->setTimestamp($feed['last_date']);
+			if ($feedDate >= $lastWeek) {
+				continue;
+			}
+			if ($feedDate < $lastYear) {
+				$idleFeeds['last_year'][] = $feed;
+			} elseif ($feedDate < $last6Month) {
+				$idleFeeds['last_6_month'][] = $feed;
+			} elseif ($feedDate < $last3Month) {
+				$idleFeeds['last_3_month'][] = $feed;
+			} elseif ($feedDate < $lastMonth) {
+				$idleFeeds['last_month'][] = $feed;
+			} elseif ($feedDate < $lastWeek) {
+				$idleFeeds['last_week'][] = $feed;
+			}
+		}
+
+		$this->view->idleFeeds = $idleFeeds;
+	}
+
+	/**
+	 * This action handles the article repartition statistic page.
+	 *
+	 * It displays the number of article and the average of article for the
+	 * following periods:
+	 *   - hour of the day
+	 *   - day of the week
+	 *   - month
+	 *
+	 * @todo verify that the metrics used here make some sense. Especially
+	 *       for the average.
+	 */
+	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);
+	}
+
+	/**
+	 * This action is called before every other action in that class. It is
+	 * the common boiler plate for every action. It is triggered by the
+	 * underlying framework.
+	 */
+	public function firstAction() {
+		if (!$this->view->loginOk) {
+			Minz_Error::error(
+			    403, array('error' => array(Minz_Translate::t('access_denied')))
+			);
+		}
+
+		Minz_View::prependTitle(Minz_Translate::t('stats') . ' · ');
+	}
+
+}

+ 129 - 0
app/Controllers/updateController.php

@@ -0,0 +1,129 @@
+<?php
+
+class FreshRSS_update_Controller extends Minz_ActionController {
+	public function firstAction() {
+		$current_user = Minz_Session::param('currentUser', '');
+		if (!$this->view->loginOk && Minz_Configuration::isAdmin($current_user)) {
+			Minz_Error::error(
+				403,
+				array('error' => array(_t('access_denied')))
+			);
+		}
+
+		invalidateHttpCache();
+
+		Minz_View::prependTitle(_t('update_system') . ' · ');
+		$this->view->update_to_apply = false;
+		$this->view->last_update_time = 'unknown';
+		$this->view->check_last_hour = false;
+		$timestamp = (int)@file_get_contents(DATA_PATH . '/last_update.txt');
+		if (is_numeric($timestamp) && $timestamp > 0) {
+			$this->view->last_update_time = timestamptodate($timestamp);
+			$this->view->check_last_hour = (time() - 3600) <= $timestamp;
+		}
+	}
+
+	public function indexAction() {
+		if (file_exists(UPDATE_FILENAME) && !is_writable(FRESHRSS_PATH)) {
+			$this->view->message = array(
+				'status' => 'bad',
+				'title' => _t('damn'),
+				'body' => _t('file_is_nok', FRESHRSS_PATH)
+			);
+		} elseif (file_exists(UPDATE_FILENAME)) {
+			// There is an update file to apply!
+			$this->view->update_to_apply = true;
+			$this->view->message = array(
+				'status' => 'good',
+				'title' => _t('ok'),
+				'body' => _t('update_can_apply')
+			);
+		}
+	}
+
+	public function checkAction() {
+		$this->view->change_view('update', 'index');
+
+		if (file_exists(UPDATE_FILENAME) || $this->view->check_last_hour) {
+			// There is already an update file to apply: we don't need to check
+			// the webserver!
+			// Or if already check during the last hour, do nothing.
+			Minz_Request::forward(array('c' => 'update'));
+
+			return;
+		}
+
+		$c = curl_init(FRESHRSS_UPDATE_WEBSITE);
+		curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
+		curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true);
+		curl_setopt($c, CURLOPT_SSL_VERIFYHOST, 2);
+		$result = curl_exec($c);
+		$c_status = curl_getinfo($c, CURLINFO_HTTP_CODE);
+		$c_error = curl_error($c);
+		curl_close($c);
+
+		if ($c_status !== 200) {
+			Minz_Log::error(
+				'Error during update (HTTP code ' . $c_status . '): ' . $c_error
+			);
+
+			$this->view->message = array(
+				'status' => 'bad',
+				'title' => _t('damn'),
+				'body' => _t('update_server_not_found', FRESHRSS_UPDATE_WEBSITE)
+			);
+			return;
+		}
+
+		$res_array = explode("\n", $result, 2);
+		$status = $res_array[0];
+		if (strpos($status, 'UPDATE') !== 0) {
+			$this->view->message = array(
+				'status' => 'bad',
+				'title' => _t('damn'),
+				'body' => _t('no_update')
+			);
+
+			@file_put_contents(DATA_PATH . '/last_update.txt', time());
+
+			return;
+		}
+
+		$script = $res_array[1];
+		if (file_put_contents(UPDATE_FILENAME, $script) !== false) {
+			Minz_Request::forward(array('c' => 'update'));
+		} else {
+			$this->view->message = array(
+				'status' => 'bad',
+				'title' => _t('damn'),
+				'body' => _t('update_problem', 'Cannot save the update script')
+			);
+		}
+	}
+
+	public function applyAction() {
+		if (!file_exists(UPDATE_FILENAME) || !is_writable(FRESHRSS_PATH)) {
+			Minz_Request::forward(array('c' => 'update'), true);
+		}
+
+		require(UPDATE_FILENAME);
+
+		if (Minz_Request::isPost()) {
+			save_info_update();
+		}
+
+		if (!need_info_update()) {
+			$res = apply_update();
+
+			if ($res === true) {
+				@unlink(UPDATE_FILENAME);
+				@file_put_contents(DATA_PATH . '/last_update.txt', time());
+
+				Minz_Request::good(_t('update_finished'));
+			} else {
+				Minz_Request::bad(_t('update_problem', $res),
+				                  array('c' => 'update', 'a' => 'index'));
+			}
+		}
+	}
+}

+ 203 - 0
app/Controllers/usersController.php

@@ -0,0 +1,203 @@
+<?php
+
+class FreshRSS_users_Controller extends Minz_ActionController {
+
+	const BCRYPT_COST = 9;	//Will also have to be computed client side on mobile devices, so do not use a too high cost
+
+	public function firstAction() {
+		if (!$this->view->loginOk) {
+			Minz_Error::error(
+				403,
+				array('error' => array(Minz_Translate::t('access_denied')))
+			);
+		}
+	}
+
+	public function authAction() {
+		if (Minz_Request::isPost()) {
+			$ok = true;
+
+			$passwordPlain = Minz_Request::param('passwordPlain', '', true);
+			if ($passwordPlain != '') {
+				Minz_Request::_param('passwordPlain');	//Discard plain-text password ASAP
+				$_POST['passwordPlain'] = '';
+				if (!function_exists('password_hash')) {
+					include_once(LIB_PATH . '/password_compat.php');
+				}
+				$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
+				$passwordPlain = '';
+				$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
+				$ok &= ($passwordHash != '');
+				$this->view->conf->_passwordHash($passwordHash);
+			}
+			Minz_Session::_param('passwordHash', $this->view->conf->passwordHash);
+
+			$passwordPlain = Minz_Request::param('apiPasswordPlain', '', true);
+			if ($passwordPlain != '') {
+				if (!function_exists('password_hash')) {
+					include_once(LIB_PATH . '/password_compat.php');
+				}
+				$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
+				$passwordPlain = '';
+				$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
+				$ok &= ($passwordHash != '');
+				$this->view->conf->_apiPasswordHash($passwordHash);
+			}
+
+			if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+				$this->view->conf->_mail_login(Minz_Request::param('mail_login', '', true));
+			}
+			$email = $this->view->conf->mail_login;
+			Minz_Session::_param('mail', $email);
+
+			$ok &= $this->view->conf->save();
+
+			if ($email != '') {
+				$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
+				@unlink($personaFile);
+				$ok &= (file_put_contents($personaFile, Minz_Session::param('currentUser', '_')) !== false);
+			}
+
+			if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+				$current_token = $this->view->conf->token;
+				$token = Minz_Request::param('token', $current_token);
+				$this->view->conf->_token($token);
+				$ok &= $this->view->conf->save();
+
+				$anon = Minz_Request::param('anon_access', false);
+				$anon = ((bool)$anon) && ($anon !== 'no');
+				$anon_refresh = Minz_Request::param('anon_refresh', false);
+				$anon_refresh = ((bool)$anon_refresh) && ($anon_refresh !== 'no');
+				$auth_type = Minz_Request::param('auth_type', 'none');
+				$unsafe_autologin = Minz_Request::param('unsafe_autologin', false);
+				$api_enabled = Minz_Request::param('api_enabled', false);
+				if ($anon != Minz_Configuration::allowAnonymous() ||
+					$auth_type != Minz_Configuration::authType() ||
+					$anon_refresh != Minz_Configuration::allowAnonymousRefresh() ||
+					$unsafe_autologin != Minz_Configuration::unsafeAutologinEnabled() ||
+					$api_enabled != Minz_Configuration::apiEnabled()) {
+
+					Minz_Configuration::_authType($auth_type);
+					Minz_Configuration::_allowAnonymous($anon);
+					Minz_Configuration::_allowAnonymousRefresh($anon_refresh);
+					Minz_Configuration::_enableAutologin($unsafe_autologin);
+					Minz_Configuration::_enableApi($api_enabled);
+					$ok &= Minz_Configuration::writeFile();
+				}
+			}
+
+			invalidateHttpCache();
+
+			$notif = array(
+				'type' => $ok ? 'good' : 'bad',
+				'content' => Minz_Translate::t($ok ? 'configuration_updated' : 'error_occurred')
+			);
+			Minz_Session::_param('notification', $notif);
+		}
+		Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true);
+	}
+
+	public function createAction() {
+		if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+			$db = Minz_Configuration::dataBase();
+			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+
+			$new_user_language = Minz_Request::param('new_user_language', $this->view->conf->language);
+			if (!in_array($new_user_language, $this->view->conf->availableLanguages())) {
+				$new_user_language = $this->view->conf->language;
+			}
+
+			$new_user_name = Minz_Request::param('new_user_name');
+			$ok = ($new_user_name != '') && ctype_alnum($new_user_name);
+
+			if ($ok) {
+				$ok &= (strcasecmp($new_user_name, Minz_Configuration::defaultUser()) !== 0);	//It is forbidden to alter the default user
+
+				$ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers()));	//Not an existing user, case-insensitive
+
+				$configPath = DATA_PATH . '/' . $new_user_name . '_user.php';
+				$ok &= !file_exists($configPath);
+			}
+			if ($ok) {
+			
+				$passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
+				$passwordHash = '';
+				if ($passwordPlain != '') {
+					Minz_Request::_param('new_user_passwordPlain');	//Discard plain-text password ASAP
+					$_POST['new_user_passwordPlain'] = '';
+					if (!function_exists('password_hash')) {
+						include_once(LIB_PATH . '/password_compat.php');
+					}
+					$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
+					$passwordPlain = '';
+					$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
+					$ok &= ($passwordHash != '');
+				}
+				if (empty($passwordHash)) {
+					$passwordHash = '';
+				}
+
+				$new_user_email = filter_var($_POST['new_user_email'], FILTER_VALIDATE_EMAIL);
+				if (empty($new_user_email)) {
+					$new_user_email = '';
+				} else {
+					$personaFile = DATA_PATH . '/persona/' . $new_user_email . '.txt';
+					@unlink($personaFile);
+					$ok &= (file_put_contents($personaFile, $new_user_name) !== false);
+				}
+			}
+			if ($ok) {
+				$config_array = array(
+					'language' => $new_user_language,
+					'passwordHash' => $passwordHash,
+					'mail_login' => $new_user_email,
+				);
+				$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($config_array, true) . ';') !== false);
+			}
+			if ($ok) {
+				$userDAO = new FreshRSS_UserDAO();
+				$ok &= $userDAO->createUser($new_user_name);
+			}
+			invalidateHttpCache();
+
+			$notif = array(
+				'type' => $ok ? 'good' : 'bad',
+				'content' => Minz_Translate::t($ok ? 'user_created' : 'error_occurred', $new_user_name)
+			);
+			Minz_Session::_param('notification', $notif);
+		}
+		Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true);
+	}
+
+	public function deleteAction() {
+		if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+			$db = Minz_Configuration::dataBase();
+			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+
+			$username = Minz_Request::param('username');
+			$ok = ctype_alnum($username);
+
+			if ($ok) {
+				$ok &= (strcasecmp($username, Minz_Configuration::defaultUser()) !== 0);	//It is forbidden to delete the default user
+			}
+			if ($ok) {
+				$configPath = DATA_PATH . '/' . $username . '_user.php';
+				$ok &= file_exists($configPath);
+			}
+			if ($ok) {
+				$userDAO = new FreshRSS_UserDAO();
+				$ok &= $userDAO->deleteUser($username);
+				$ok &= unlink($configPath);
+				//TODO: delete Persona file
+			}
+			invalidateHttpCache();
+
+			$notif = array(
+				'type' => $ok ? 'good' : 'bad',
+				'content' => Minz_Translate::t($ok ? 'user_deleted' : 'error_occurred', $username)
+			);
+			Minz_Session::_param('notification', $notif);
+		}
+		Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true);
+	}
+}

+ 6 - 0
app/Exceptions/BadUrlException.php

@@ -0,0 +1,6 @@
+<?php
+class FreshRSS_BadUrl_Exception extends FreshRSS_Feed_Exception {
+	public function __construct ($url) {
+		parent::__construct ('`' . $url . '` is not a valid URL');
+	}
+}

+ 7 - 0
app/Exceptions/EntriesGetterException.php

@@ -0,0 +1,7 @@
+<?php
+
+class FreshRSS_EntriesGetter_Exception extends Exception {
+	public function __construct ($message) {
+		parent::__construct ($message);
+	}
+}

+ 1 - 2
app/models/Exception/EntriesGetterException.php → app/Exceptions/FeedException.php

@@ -1,6 +1,5 @@
 <?php
-
-class EntriesGetterException extends Exception {
+class FreshRSS_Feed_Exception extends Exception {
 	public function __construct ($message) {
 		parent::__construct ($message);
 	}

+ 182 - 0
app/FreshRSS.php

@@ -0,0 +1,182 @@
+<?php
+class FreshRSS extends Minz_FrontController {
+	public function init() {
+		if (!isset($_SESSION)) {
+			Minz_Session::init('FreshRSS');
+		}
+		$loginOk = $this->accessControl(Minz_Session::param('currentUser', ''));
+		$this->loadParamsView();
+		if (Minz_Request::isPost() && !is_referer_from_same_domain()) {
+			$loginOk = false;	//Basic protection against XSRF attacks
+			Minz_Error::error(
+				403,
+				array('error' => array(Minz_Translate::t('access_denied') . ' [HTTP_REFERER=' .
+					htmlspecialchars(empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']) . ']'))
+			);
+		}
+		Minz_View::_param('loginOk', $loginOk);
+		$this->loadStylesAndScripts($loginOk);	//TODO: Do not load that when not needed, e.g. some Ajax requests
+		$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':
+					$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();
+					$loginOk = $currentUser != '';
+					break;
+				case 'persona':
+					$loginOk = false;
+					$email = filter_var(Minz_Session::param('mail'), FILTER_VALIDATE_EMAIL);
+					if ($email != '') {	//TODO: Remove redundancy with indexController
+						$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
+						if (($currentUser = @file_get_contents($personaFile)) !== false) {
+							$currentUser = trim($currentUser);
+							$loginOk = true;
+						}
+					}
+					if (!$loginOk) {
+						$currentUser = Minz_Configuration::defaultUser();
+					}
+					break;
+				case 'none':
+					$currentUser = Minz_Configuration::defaultUser();
+					$loginOk = true;
+					break;
+				default:
+					$currentUser = Minz_Configuration::defaultUser();
+					$loginOk = false;
+					break;
+			}
+		} else {
+			$loginOk = true;
+		}
+
+		if (!ctype_alnum($currentUser)) {
+			Minz_Session::_param('currentUser', '');
+			die('Invalid username [' . $currentUser . ']!');
+		}
+
+		try {
+			$this->conf = new FreshRSS_Configuration($currentUser);
+			Minz_View::_param ('conf', $this->conf);
+			Minz_Session::_param('currentUser', $currentUser);
+		} catch (Minz_Exception $me) {
+			$loginOk = false;
+			try {
+				$this->conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
+				Minz_Session::_param('currentUser', Minz_Configuration::defaultUser());
+				Minz_View::_param('conf', $this->conf);
+				$notif = array(
+					'type' => 'bad',
+					'content' => 'Invalid configuration for user [' . $currentUser . ']!',
+				);
+				Minz_Session::_param ('notification', $notif);
+				Minz_Log::record ($notif['content'] . ' ' . $me->getMessage(), Minz_Log::WARNING);
+				Minz_Session::_param('currentUser', '');
+			} catch (Exception $e) {
+				die($e->getMessage());
+			}
+		}
+
+		if ($loginOk) {
+			switch (Minz_Configuration::authType()) {
+				case 'form':
+					$loginOk = Minz_Session::param('passwordHash') === $this->conf->passwordHash;
+					break;
+				case 'http_auth':
+					$loginOk = strcasecmp($currentUser, httpAuthUser()) === 0;
+					break;
+				case 'persona':
+					$loginOk = strcasecmp(Minz_Session::param('mail'), $this->conf->mail_login) === 0;
+					break;
+				case 'none':
+					$loginOk = true;
+					break;
+				default:
+					$loginOk = false;
+					break;
+			}
+		}
+		return $loginOk;
+	}
+
+	private function loadParamsView () {
+		Minz_Session::_param ('language', $this->conf->language);
+		Minz_Translate::init();
+		$output = Minz_Request::param ('output', '');
+		if (($output === '') || ($output !== 'normal' && $output !== 'rss' && $output !== 'reader' && $output !== 'global')) {
+			$output = $this->conf->view_mode;
+			Minz_Request::_param ('output', $output);
+		}
+	}
+
+	private function loadStylesAndScripts($loginOk) {
+		$theme = FreshRSS_Themes::load($this->conf->theme);
+		if ($theme) {
+			foreach($theme['files'] as $file) {
+				if ($file[0] === '_') {
+					$theme_id = 'base-theme';
+					$filename = substr($file, 1);
+				} else {
+					$theme_id = $theme['id'];
+					$filename = $file;
+				}
+				$filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename);
+				Minz_View::appendStyle(Minz_Url::display(
+					'/themes/' . $theme_id . '/' . $filename . '?' . $filetime
+				));
+			}
+		}
+
+		switch (Minz_Configuration::authType()) {
+			case 'form':
+				if (!$loginOk) {
+					Minz_View::appendScript(Minz_Url::display ('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
+				}
+				break;
+			case 'persona':
+				Minz_View::appendScript('https://login.persona.org/include.js');
+				break;
+		}
+		Minz_View::appendScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')));
+		Minz_View::appendScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
+		Minz_View::appendScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
+	}
+
+	private function loadNotifications () {
+		$notif = Minz_Session::param ('notification');
+		if ($notif) {
+			Minz_View::_param ('notification', $notif);
+			Minz_Session::_param ('notification');
+		}
+	}
+}

+ 73 - 0
app/Models/Category.php

@@ -0,0 +1,73 @@
+<?php
+
+class FreshRSS_Category extends Minz_Model {
+	private $id = 0;
+	private $name;
+	private $nbFeed = -1;
+	private $nbNotRead = -1;
+	private $feeds = null;
+
+	public function __construct ($name = '', $feeds = null) {
+		$this->_name ($name);
+		if (isset ($feeds)) {
+			$this->_feeds ($feeds);
+			$this->nbFeed = 0;
+			$this->nbNotRead = 0;
+			foreach ($feeds as $feed) {
+				$this->nbFeed++;
+				$this->nbNotRead += $feed->nbNotRead ();
+			}
+		}
+	}
+
+	public function id () {
+		return $this->id;
+	}
+	public function name () {
+		return $this->name;
+	}
+	public function nbFeed () {
+		if ($this->nbFeed < 0) {
+			$catDAO = new FreshRSS_CategoryDAO ();
+			$this->nbFeed = $catDAO->countFeed ($this->id ());
+		}
+
+		return $this->nbFeed;
+	}
+	public function nbNotRead () {
+		if ($this->nbNotRead < 0) {
+			$catDAO = new FreshRSS_CategoryDAO ();
+			$this->nbNotRead = $catDAO->countNotRead ($this->id ());
+		}
+
+		return $this->nbNotRead;
+	}
+	public function feeds () {
+		if ($this->feeds === null) {
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$this->feeds = $feedDAO->listByCategory ($this->id ());
+			$this->nbFeed = 0;
+			$this->nbNotRead = 0;
+			foreach ($this->feeds as $feed) {
+				$this->nbFeed++;
+				$this->nbNotRead += $feed->nbNotRead ();
+			}
+		}
+
+		return $this->feeds;
+	}
+
+	public function _id ($value) {
+		$this->id = $value;
+	}
+	public function _name ($value) {
+		$this->name = $value;
+	}
+	public function _feeds ($values) {
+		if (!is_array ($values)) {
+			$values = array ($values);
+		}
+
+		$this->feeds = $values;
+	}
+}

+ 257 - 0
app/Models/CategoryDAO.php

@@ -0,0 +1,257 @@
+<?php
+
+class FreshRSS_CategoryDAO extends Minz_ModelPdo {
+	public function addCategory ($valuesTmp) {
+		$sql = 'INSERT INTO `' . $this->prefix . 'category` (name) VALUES(?)';
+		$stm = $this->bd->prepare ($sql);
+
+		$values = array (
+			substr($valuesTmp['name'], 0, 255),
+		);
+
+		if ($stm && $stm->execute ($values)) {
+			return $this->bd->lastInsertId();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error addCategory: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function addCategoryObject($category) {
+		$cat = $this->searchByName($category->name());
+		if (!$cat) {
+			// Category does not exist yet in DB so we add it before continue
+			$values = array(
+				'name' => $category->name(),
+			);
+			return $this->addCategory($values);
+		}
+
+		return $cat->id();
+	}
+
+	public function updateCategory ($id, $valuesTmp) {
+		$sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?';
+		$stm = $this->bd->prepare ($sql);
+
+		$values = array (
+			$valuesTmp['name'],
+			$id
+		);
+
+		if ($stm && $stm->execute ($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateCategory: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function deleteCategory ($id) {
+		$sql = 'DELETE FROM `' . $this->prefix . 'category` WHERE id=?';
+		$stm = $this->bd->prepare ($sql);
+
+		$values = array ($id);
+
+		if ($stm && $stm->execute ($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error deleteCategory: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function searchById ($id) {
+		$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=?';
+		$stm = $this->bd->prepare ($sql);
+
+		$values = array ($id);
+
+		$stm->execute ($values);
+		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
+		$cat = self::daoToCategory ($res);
+
+		if (isset ($cat[0])) {
+			return $cat[0];
+		} else {
+			return null;
+		}
+	}
+	public function searchByName ($name) {
+		$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE name=?';
+		$stm = $this->bd->prepare ($sql);
+
+		$values = array ($name);
+
+		$stm->execute ($values);
+		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
+		$cat = self::daoToCategory ($res);
+
+		if (isset ($cat[0])) {
+			return $cat[0];
+		} else {
+			return null;
+		}
+	}
+
+	public function listCategories ($prePopulateFeeds = true, $details = false) {
+		if ($prePopulateFeeds) {
+			$sql = 'SELECT c.id AS c_id, c.name AS c_name, '
+			     . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ')
+			     . 'FROM `' . $this->prefix . 'category` c '
+			     . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id '
+			     . 'GROUP BY f.id '
+			     . 'ORDER BY c.name, f.name';
+			$stm = $this->bd->prepare ($sql);
+			$stm->execute ();
+			return self::daoToCategoryPrepopulated ($stm->fetchAll (PDO::FETCH_ASSOC));
+		} else {
+			$sql = 'SELECT * FROM `' . $this->prefix . 'category` ORDER BY name';
+			$stm = $this->bd->prepare ($sql);
+			$stm->execute ();
+			return self::daoToCategory ($stm->fetchAll (PDO::FETCH_ASSOC));
+		}
+	}
+
+	public function getDefault () {
+		$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=1';
+		$stm = $this->bd->prepare ($sql);
+
+		$stm->execute ();
+		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
+		$cat = self::daoToCategory ($res);
+
+		if (isset ($cat[0])) {
+			return $cat[0];
+		} else {
+			return false;
+		}
+	}
+	public function checkDefault () {
+		$def_cat = $this->searchById (1);
+
+		if ($def_cat == null) {
+			$cat = new FreshRSS_Category (Minz_Translate::t ('default_category'));
+			$cat->_id (1);
+
+			$values = array (
+				'id' => $cat->id (),
+				'name' => $cat->name (),
+			);
+
+			$this->addCategory ($values);
+		}
+	}
+
+	public function count () {
+		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'category`';
+		$stm = $this->bd->prepare ($sql);
+		$stm->execute ();
+		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
+
+		return $res[0]['count'];
+	}
+
+	public function countFeed ($id) {
+		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'feed` WHERE category=?';
+		$stm = $this->bd->prepare ($sql);
+		$values = array ($id);
+		$stm->execute ($values);
+		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
+
+		return $res[0]['count'];
+	}
+
+	public function countNotRead ($id) {
+		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE category=? AND e.is_read=0';
+		$stm = $this->bd->prepare ($sql);
+		$values = array ($id);
+		$stm->execute ($values);
+		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
+
+		return $res[0]['count'];
+	}
+
+	public static function findFeed($categories, $feed_id) {
+		foreach ($categories as $category) {
+			foreach ($category->feeds () as $feed) {
+				if ($feed->id () === $feed_id) {
+					return $feed;
+				}
+			}
+		}
+		return null;
+	}
+
+	public static function CountUnreads($categories, $minPriority = 0) {
+		$n = 0;
+		foreach ($categories as $category) {
+			foreach ($category->feeds () as $feed) {
+				if ($feed->priority () >= $minPriority) {
+					$n += $feed->nbNotRead();
+				}
+			}
+		}
+		return $n;
+	}
+
+	public static function daoToCategoryPrepopulated ($listDAO) {
+		$list = array ();
+
+		if (!is_array ($listDAO)) {
+			$listDAO = array ($listDAO);
+		}
+
+		$previousLine = null;
+		$feedsDao = array();
+		foreach ($listDAO as $line) {
+			if ($previousLine['c_id'] != null && $line['c_id'] !== $previousLine['c_id']) {
+				// End of the current category, we add it to the $list
+				$cat = new FreshRSS_Category (
+					$previousLine['c_name'],
+					FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id'])
+				);
+				$cat->_id ($previousLine['c_id']);
+				$list[$previousLine['c_id']] = $cat;
+
+				$feedsDao = array();	//Prepare for next category
+			}
+
+			$previousLine = $line;
+			$feedsDao[] = $line;
+		}
+
+		// add the last category
+		if ($previousLine != null) {
+			$cat = new FreshRSS_Category (
+				$previousLine['c_name'],
+				FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id'])
+			);
+			$cat->_id ($previousLine['c_id']);
+			$list[$previousLine['c_id']] = $cat;
+		}
+
+		return $list;
+	}
+
+	public static function daoToCategory ($listDAO) {
+		$list = array ();
+
+		if (!is_array ($listDAO)) {
+			$listDAO = array ($listDAO);
+		}
+
+		foreach ($listDAO as $key => $dao) {
+			$cat = new FreshRSS_Category (
+				$dao['name']
+			);
+			$cat->_id ($dao['id']);
+			$list[$key] = $cat;
+		}
+
+		return $list;
+	}
+}

+ 335 - 0
app/Models/Configuration.php

@@ -0,0 +1,335 @@
+<?php
+
+class FreshRSS_Configuration {
+	private $filename;
+
+	private $data = array(
+		'language' => 'en',
+		'old_entries' => 3,
+		'keep_history_default' => 0,
+		'ttl_default' => 3600,
+		'mail_login' => '',
+		'token' => '',
+		'passwordHash' => '',	//CRYPT_BLOWFISH
+		'apiPasswordHash' => '',	//CRYPT_BLOWFISH
+		'posts_per_page' => 20,
+		'view_mode' => 'normal',
+		'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,
+		'sticky_post' => true,
+		'reading_confirm' => false,
+		'sort_order' => 'DESC',
+		'anon_access' => false,
+		'mark_when' => array(
+			'article' => true,
+			'site' => true,
+			'scroll' => false,
+			'reception' => false,
+		),
+		'theme' => 'Origine',
+		'content_width' => 'thin',
+		'shortcuts' => array(
+			'mark_read' => 'r',
+			'mark_favorite' => 'f',
+			'go_website' => 'space',
+			'next_entry' => 'j',
+			'prev_entry' => 'k',
+			'first_entry' => 'home',
+			'last_entry' => 'end',
+			'collapse_entry' => 'c',
+			'load_more' => 'm',
+			'auto_share' => 's',
+			'focus_search' => 'a',
+			'user_filter' => 'u',
+			'help' => 'f1',
+		),
+		'topline_read' => true,
+		'topline_favorite' => true,
+		'topline_date' => true,
+		'topline_link' => true,
+		'bottomline_read' => true,
+		'bottomline_favorite' => true,
+		'bottomline_sharing' => true,
+		'bottomline_tags' => true,
+		'bottomline_date' => true,
+		'bottomline_link' => true,
+		'sharing' => array(),
+		'queries' => array(),
+		'html5_notif_timeout' => 0,
+	);
+
+	private $available_languages = array(
+		'en' => 'English',
+		'fr' => 'Français',
+	);
+
+	private $shares;
+
+	public function __construct($user) {
+		$this->filename = DATA_PATH . DIRECTORY_SEPARATOR . $user . '_user.php';
+
+		$data = @include($this->filename);
+		if (!is_array($data)) {
+			throw new Minz_PermissionDeniedException($this->filename);
+		}
+
+		foreach ($data as $key => $value) {
+			if (isset($this->data[$key])) {
+				$function = '_' . $key;
+				$this->$function($value);
+			}
+		}
+		$this->data['user'] = $user;
+
+		$this->shares = DATA_PATH . DIRECTORY_SEPARATOR . 'shares.php';
+
+		$shares = @include($this->shares);
+		if (!is_array($shares)) {
+			throw new Minz_PermissionDeniedException($this->shares);
+		}
+
+		$this->data['shares'] = $shares;
+	}
+
+	public function save() {
+		@rename($this->filename, $this->filename . '.bak.php');
+		unset($this->data['shares']); // Remove shares because it is not intended to be stored in user configuration
+		if (file_put_contents($this->filename, "<?php\n return " . var_export($this->data, true) . ';', LOCK_EX) === false) {
+			throw new Minz_PermissionDeniedException($this->filename);
+		}
+		if (function_exists('opcache_invalidate')) {
+			opcache_invalidate($this->filename);	//Clear PHP 5.5+ cache for include
+		}
+		invalidateHttpCache();
+		return true;
+	}
+
+	public function __get($name) {
+		if (array_key_exists($name, $this->data)) {
+			return $this->data[$name];
+		} else {
+			$trace = debug_backtrace();
+			trigger_error('Undefined FreshRSS_Configuration->' . $name . 'in ' . $trace[0]['file'] . ' line ' . $trace[0]['line'], E_USER_NOTICE);	//TODO: Use Minz exceptions
+			return null;
+		}
+	}
+
+	public function availableLanguages() {
+		return $this->available_languages;
+	}
+
+	public function remove_query_by_get($get) {
+		$final_queries = array();
+		foreach ($this->queries as $key => $query) {
+			if (empty($query['get']) || $query['get'] !== $get) {
+				$final_queries[$key] = $query;
+			}
+		}
+		$this->_queries($final_queries);
+	}
+
+	public function _language($value) {
+		if (!isset($this->available_languages[$value])) {
+			$value = 'en';
+		}
+		$this->data['language'] = $value;
+	}
+	public function _posts_per_page ($value) {
+		$value = intval($value);
+		$this->data['posts_per_page'] = $value > 0 ? $value : 10;
+	}
+	public function _view_mode ($value) {
+		if ($value === 'global' || $value === 'reader') {
+			$this->data['view_mode'] = $value;
+		} else {
+			$this->data['view_mode'] = 'normal';
+		}
+	}
+	public function _default_view ($value) {
+		switch ($value) {
+		case FreshRSS_Entry::STATE_ALL:
+			// left blank on purpose
+		case FreshRSS_Entry::STATE_NOT_READ:
+			// left blank on purpose
+		case FreshRSS_Entry::STATE_STRICT + FreshRSS_Entry::STATE_NOT_READ:
+			$this->data['default_view'] = $value;
+			break;
+		default:
+			$this->data['default_view'] = FreshRSS_Entry::STATE_ALL;
+			break;
+		}
+	}
+	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;
+	}
+	public function _onread_jump_next ($value) {
+		$this->data['onread_jump_next'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _lazyload ($value) {
+		$this->data['lazyload'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _sticky_post($value) {
+		$this->data['sticky_post'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _reading_confirm($value) {
+		$this->data['reading_confirm'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _sort_order ($value) {
+		$this->data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC';
+	}
+	public function _old_entries($value) {
+		$value = intval($value);
+		$this->data['old_entries'] = $value > 0 ? $value : 3;
+	}
+	public function _keep_history_default($value) {
+		$value = intval($value);
+		$this->data['keep_history_default'] = $value >= -1 ? $value : 0;
+	}
+	public function _ttl_default($value) {
+		$value = intval($value);
+		$this->data['ttl_default'] = $value >= -1 ? $value : 3600;
+	}
+	public function _shortcuts ($values) {
+		foreach ($values as $key => $value) {
+			if (isset($this->data['shortcuts'][$key])) {
+				$this->data['shortcuts'][$key] = $value;
+			}
+		}
+	}
+	public function _passwordHash ($value) {
+		$this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
+	}
+	public function _apiPasswordHash ($value) {
+		$this->data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
+	}
+	public function _mail_login ($value) {
+		$value = filter_var($value, FILTER_VALIDATE_EMAIL);
+		if ($value) {
+			$this->data['mail_login'] = $value;
+		} else {
+			$this->data['mail_login'] = '';
+		}
+	}
+	public function _anon_access ($value) {
+		$this->data['anon_access'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _mark_when ($values) {
+		foreach ($values as $key => $value) {
+			if (isset($this->data['mark_when'][$key])) {
+				$this->data['mark_when'][$key] = ((bool)$value) && $value !== 'no';
+			}
+		}
+	}
+	public function _sharing ($values) {
+		$this->data['sharing'] = array();
+		$unique = array();
+		foreach ($values as $value) {
+			if (!is_array($value)) {
+				continue;
+			}
+
+			// Verify URL and add default value when needed
+			if (isset($value['url'])) {
+				$is_url = (
+					filter_var ($value['url'], FILTER_VALIDATE_URL) ||
+					(version_compare(PHP_VERSION, '5.3.3', '<') &&
+						(strpos($value, '-') > 0) &&
+						($value === filter_var($value, FILTER_SANITIZE_URL)))
+				);  //PHP bug #51192
+				if (!$is_url) {
+					continue;
+				}
+			} else {
+				$value['url'] = null;
+			}
+
+			// Add a default name
+			if (empty($value['name'])) {
+				$value['name'] = $value['type'];
+			}
+
+			$json_value = json_encode($value);
+			if (!in_array($json_value, $unique)) {
+				$unique[] = $json_value;
+				$this->data['sharing'][] = $value;
+			}
+		}
+	}
+	public function _queries ($values) {
+		$this->data['queries'] = array();
+		foreach ($values as $value) {
+			$value = array_filter($value);
+			$params = $value;
+			unset($params['name']);
+			unset($params['url']);
+			$value['url'] = Minz_Url::display(array('params' => $params));
+
+			$this->data['queries'][] = $value;
+		}
+	}
+	public function _theme($value) {
+		$this->data['theme'] = $value;
+	}
+	public function _content_width($value) {
+		if ($value === 'medium' ||
+				$value === 'large' ||
+				$value === 'no_limit') {
+			$this->data['content_width'] = $value;
+		} else {
+			$this->data['content_width'] = 'thin';
+		}
+	}
+	
+	public function _html5_notif_timeout ($value) {
+		$value = intval($value);
+		$this->data['html5_notif_timeout'] = $value >= 0 ? $value : 0;
+	}
+	
+	public function _token($value) {
+		$this->data['token'] = $value;
+	}
+	public function _auto_load_more($value) {
+		$this->data['auto_load_more'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _topline_read($value) {
+		$this->data['topline_read'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _topline_favorite($value) {
+		$this->data['topline_favorite'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _topline_date($value) {
+		$this->data['topline_date'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _topline_link($value) {
+		$this->data['topline_link'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _bottomline_read($value) {
+		$this->data['bottomline_read'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _bottomline_favorite($value) {
+		$this->data['bottomline_favorite'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _bottomline_sharing($value) {
+		$this->data['bottomline_sharing'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _bottomline_tags($value) {
+		$this->data['bottomline_tags'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _bottomline_date($value) {
+		$this->data['bottomline_date'] = ((bool)$value) && $value !== 'no';
+	}
+	public function _bottomline_link($value) {
+		$this->data['bottomline_link'] = ((bool)$value) && $value !== 'no';
+	}
+}

+ 1 - 1
app/models/Days.php → app/Models/Days.php

@@ -1,6 +1,6 @@
 <?php
 
-class Days {
+class FreshRSS_Days {
 	const TODAY = 0;
 	const YESTERDAY = 1;
 	const BEFORE_YESTERDAY = 2;

+ 192 - 0
app/Models/Entry.php

@@ -0,0 +1,192 @@
+<?php
+
+class FreshRSS_Entry extends Minz_Model {
+	const STATE_ALL = 0;
+	const STATE_READ = 1;
+	const STATE_NOT_READ = 2;
+	const STATE_FAVORITE = 4;
+	const STATE_NOT_FAVORITE = 8;
+	const STATE_STRICT = 16;
+
+	private $id = 0;
+	private $guid;
+	private $title;
+	private $author;
+	private $content;
+	private $link;
+	private $date;
+	private $is_read;
+	private $is_favorite;
+	private $feed;
+	private $tags;
+
+	public function __construct ($feed = '', $guid = '', $title = '', $author = '', $content = '',
+	                             $link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') {
+		$this->_guid ($guid);
+		$this->_title ($title);
+		$this->_author ($author);
+		$this->_content ($content);
+		$this->_link ($link);
+		$this->_date ($pubdate);
+		$this->_isRead ($is_read);
+		$this->_isFavorite ($is_favorite);
+		$this->_feed ($feed);
+		$this->_tags (preg_split('/[\s#]/', $tags));
+	}
+
+	public function id () {
+		return $this->id;
+	}
+	public function guid () {
+		return $this->guid;
+	}
+	public function title () {
+		return $this->title;
+	}
+	public function author () {
+		return $this->author === null ? '' : $this->author;
+	}
+	public function content () {
+		return $this->content;
+	}
+	public function link () {
+		return $this->link;
+	}
+	public function date ($raw = false) {
+		if ($raw) {
+			return $this->date;
+		} else {
+			return timestamptodate ($this->date);
+		}
+	}
+	public function dateAdded ($raw = false) {
+		$date = intval(substr($this->id, 0, -6));
+		if ($raw) {
+			return $date;
+		} else {
+			return timestamptodate ($date);
+		}
+	}
+	public function isRead () {
+		return $this->is_read;
+	}
+	public function isFavorite () {
+		return $this->is_favorite;
+	}
+	public function feed ($object = false) {
+		if ($object) {
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			return $feedDAO->searchById ($this->feed);
+		} else {
+			return $this->feed;
+		}
+	}
+	public function tags ($inString = false) {
+		if ($inString) {
+			return empty ($this->tags) ? '' : '#' . implode(' #', $this->tags);
+		} else {
+			return $this->tags;
+		}
+	}
+
+	public function _id ($value) {
+		$this->id = $value;
+	}
+	public function _guid ($value) {
+		$this->guid = $value;
+	}
+	public function _title ($value) {
+		$this->title = $value;
+	}
+	public function _author ($value) {
+		$this->author = $value;
+	}
+	public function _content ($value) {
+		$this->content = $value;
+	}
+	public function _link ($value) {
+		$this->link = $value;
+	}
+	public function _date ($value) {
+		$value = intval($value);
+		$this->date = $value > 1 ? $value : time();
+	}
+	public function _isRead ($value) {
+		$this->is_read = $value;
+	}
+	public function _isFavorite ($value) {
+		$this->is_favorite = $value;
+	}
+	public function _feed ($value) {
+		$this->feed = $value;
+	}
+	public function _tags ($value) {
+		if (!is_array ($value)) {
+			$value = array ($value);
+		}
+
+		foreach ($value as $key => $t) {
+			if (!$t) {
+				unset ($value[$key]);
+			}
+		}
+
+		$this->tags = $value;
+	}
+
+	public function isDay ($day, $today) {
+		$date = $this->dateAdded(true);
+		switch ($day) {
+			case FreshRSS_Days::TODAY:
+				$tomorrow = $today + 86400;
+				return $date >= $today && $date < $tomorrow;
+			case FreshRSS_Days::YESTERDAY:
+				$yesterday = $today - 86400;
+				return $date >= $yesterday && $date < $today;
+			case FreshRSS_Days::BEFORE_YESTERDAY:
+				$yesterday = $today - 86400;
+				return $date < $yesterday;
+			default:
+				return false;
+		}
+	}
+
+	public function loadCompleteContent($pathEntries) {
+		// Gestion du contenu
+		// On cherche à récupérer les articles en entier... même si le flux ne le propose pas
+		if ($pathEntries) {
+			$entryDAO = FreshRSS_Factory::createEntryDao();
+			$entry = $entryDAO->searchByGuid($this->feed, $this->guid);
+
+			if($entry) {
+				// l'article existe déjà en BDD, en se contente de recharger ce contenu
+				$this->content = $entry->content();
+			} else {
+				try {
+					// l'article n'est pas en BDD, on va le chercher sur le site
+					$this->content = get_content_by_parsing(
+						htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries
+					);
+				} catch (Exception $e) {
+					// rien à faire, on garde l'ancien contenu (requête a échoué)
+				}
+			}
+		}
+	}
+
+	public function toArray () {
+		return array (
+			'id' => $this->id (),
+			'guid' => $this->guid (),
+			'title' => $this->title (),
+			'author' => $this->author (),
+			'content' => $this->content (),
+			'link' => $this->link (),
+			'date' => $this->date (true),
+			'is_read' => $this->isRead (),
+			'is_favorite' => $this->isFavorite (),
+			'id_feed' => $this->feed (),
+			'tags' => $this->tags (true),
+		);
+	}
+}

+ 566 - 0
app/Models/EntryDAO.php

@@ -0,0 +1,566 @@
+<?php
+
+class FreshRSS_EntryDAO extends Minz_ModelPdo {
+
+	public function isCompressed() {
+		return parent::$sharedDbType !== 'sqlite';
+	}
+
+	public function addEntryPrepare() {
+		$sql = 'INSERT INTO `' . $this->prefix . 'entry`(id, guid, title, author, '
+		     . ($this->isCompressed() ? 'content_bin' : 'content')
+		     . ', link, date, is_read, is_favorite, id_feed, tags) '
+		     . 'VALUES(?, ?, ?, ?, '
+		     . ($this->isCompressed() ? 'COMPRESS(?)' : '?')
+		     . ', ?, ?, ?, ?, ?, ?)';
+		return $this->bd->prepare($sql);
+	}
+
+	public function addEntry($valuesTmp, $preparedStatement = null) {
+		$stm = $preparedStatement === null ?
+				FreshRSS_EntryDAO::addEntryPrepare() :
+				$preparedStatement;
+
+		$values = array(
+			$valuesTmp['id'],
+			substr($valuesTmp['guid'], 0, 760),
+			substr($valuesTmp['title'], 0, 255),
+			substr($valuesTmp['author'], 0, 255),
+			$valuesTmp['content'],
+			substr($valuesTmp['link'], 0, 1023),
+			$valuesTmp['date'],
+			$valuesTmp['is_read'] ? 1 : 0,
+			$valuesTmp['is_favorite'] ? 1 : 0,
+			$valuesTmp['id_feed'],
+			substr($valuesTmp['tags'], 0, 1023),
+		);
+
+		if ($stm && $stm->execute($values)) {
+			return $this->bd->lastInsertId();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			if ((int)($info[0] / 1000) !== 23) {	//Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries
+				Minz_Log::record('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
+				. ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title'], Minz_Log::ERROR);
+			} /*else {
+				Minz_Log::record ('SQL error ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
+				. ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title'], Minz_Log::DEBUG);
+			}*/
+			return false;
+		}
+	}
+
+	public function addEntryObject($entry, $conf, $feedHistory) {
+		$existingGuids = array_fill_keys(
+			$this->listLastGuidsByFeed($entry->feed(), 20), 1
+		);
+
+		$nb_month_old = max($conf->old_entries, 1);
+		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
+
+		$eDate = $entry->date(true);
+
+		if ($feedHistory == -2) {
+			$feedHistory = $conf->keep_history_default;
+		}
+
+		if (!isset($existingGuids[$entry->guid()]) &&
+				($feedHistory != 0 || $eDate >= $date_min || $entry->isFavorite())) {
+			$values = $entry->toArray();
+
+			$useDeclaredDate = empty($existingGuids);
+			$values['id'] = ($useDeclaredDate || $eDate < $date_min) ?
+				min(time(), $eDate) . uSecString() :
+				uTimeString();
+
+			return $this->addEntry($values);
+		}
+
+		// We don't return Entry object to avoid a research in DB
+		return -1;
+	}
+
+	public function markFavorite($ids, $is_favorite = true) {
+		if (!is_array($ids)) {
+			$ids = array($ids);
+		}
+		$sql = 'UPDATE `' . $this->prefix . 'entry` '
+		     . 'SET is_favorite=? '
+		     . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
+		$values = array($is_favorite ? 1 : 0);
+		$values = array_merge($values, $ids);
+		$stm = $this->bd->prepare($sql);
+		if ($stm && $stm->execute($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markFavorite: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	protected function updateCacheUnreads($catId = false, $feedId = false) {
+		$sql = 'UPDATE `' . $this->prefix . 'feed` f '
+		 . 'LEFT OUTER JOIN ('
+		 .	'SELECT e.id_feed, '
+		 .	'COUNT(*) AS nbUnreads '
+		 .	'FROM `' . $this->prefix . 'entry` e '
+		 .	'WHERE e.is_read=0 '
+		 .	'GROUP BY e.id_feed'
+		 . ') x ON x.id_feed=f.id '
+		 . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) '
+		 . 'WHERE 1';
+		$values = array();
+		if ($feedId !== false) {
+			$sql .= ' AND f.id=?';
+			$values[] = $id;
+		}
+		if ($catId !== false) {
+			$sql .= ' AND f.category=?';
+			$values[] = $catId;
+		}
+		$stm = $this->bd->prepare($sql);
+		if ($stm && $stm->execute($values)) {
+			return true;
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateCacheUnreads: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function markRead($ids, $is_read = true) {
+		if (is_array($ids)) {	//Many IDs at once (used by API)
+			if (count($ids) < 6) {	//Speed heuristics
+				$affected = 0;
+				foreach ($ids as $id) {
+					$affected += $this->markRead($id, $is_read);
+				}
+				return $affected;
+			}
+
+			$sql = 'UPDATE `' . $this->prefix . 'entry` '
+				 . 'SET is_read=? '
+				 . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
+			$values = array($is_read ? 1 : 0);
+			$values = array_merge($values, $ids);
+			$stm = $this->bd->prepare($sql);
+			if (!($stm && $stm->execute($values))) {
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::record('SQL error markRead: ' . $info[2], Minz_Log::ERROR);
+				return false;
+			}
+			$affected = $stm->rowCount();
+			if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+				return false;
+			}
+			return $affected;
+		} else {
+			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
+				 . 'SET e.is_read=?,'
+				 . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
+				 . 'WHERE e.id=? AND e.is_read=?';
+			$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
+			$stm = $this->bd->prepare($sql);
+			if ($stm && $stm->execute($values)) {
+				return $stm->rowCount();
+			} else {
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::record('SQL error markRead: ' . $info[2], Minz_Log::ERROR);
+				return false;
+			}
+		}
+	}
+
+	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) {
+		if ($idMax == 0) {
+			$idMax = time() . '000000';
+			Minz_Log::record('Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG);
+		}
+
+		$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
+			 . 'SET e.is_read=1 '
+			 . 'WHERE e.is_read=0 AND e.id <= ?';
+		if ($onlyFavorites) {
+			$sql .= ' AND e.is_favorite=1';
+		} elseif ($priorityMin >= 0) {
+			$sql .= ' AND f.priority > ' . intval($priorityMin);
+		}
+		$values = array($idMax);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markReadEntries: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+		$affected = $stm->rowCount();
+		if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+			return false;
+		}
+		return $affected;
+	}
+
+	public function markReadCat($id, $idMax = 0) {
+		if ($idMax == 0) {
+			$idMax = time() . '000000';
+			Minz_Log::record('Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG);
+		}
+
+		$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
+			 . 'SET e.is_read=1 '
+			 . 'WHERE f.category=? AND e.is_read=0 AND e.id <= ?';
+		$values = array($id, $idMax);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markReadCat: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+		$affected = $stm->rowCount();
+		if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) {
+			return false;
+		}
+		return $affected;
+	}
+
+	public function markReadFeed($id, $idMax = 0) {
+		if ($idMax == 0) {
+			$idMax = time() . '000000';
+			Minz_Log::record('Calling markReadFeed(0) is deprecated!', Minz_Log::DEBUG);
+		}
+		$this->bd->beginTransaction();
+
+		$sql = 'UPDATE `' . $this->prefix . 'entry` '
+			 . 'SET is_read=1 '
+			 . 'WHERE id_feed=? AND is_read=0 AND id <= ?';
+		$values = array($id, $idMax);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markReadFeed: ' . $info[2], Minz_Log::ERROR);
+			$this->bd->rollBack();
+			return false;
+		}
+		$affected = $stm->rowCount();
+
+		if ($affected > 0) {
+			$sql = 'UPDATE `' . $this->prefix . 'feed` '
+				 . 'SET cache_nbUnreads=cache_nbUnreads-' . $affected
+				 . ' WHERE id=?';
+			$values = array($id);
+			$stm = $this->bd->prepare($sql);
+			if (!($stm && $stm->execute($values))) {
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::record('SQL error markReadFeed: ' . $info[2], Minz_Log::ERROR);
+				$this->bd->rollBack();
+				return false;
+			}
+		}
+
+		$this->bd->commit();
+		return $affected;
+	}
+
+	public function searchByGuid($feed_id, $id) {
+		// un guid est unique pour un flux donné
+		$sql = 'SELECT id, guid, title, author, '
+		     . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
+		     . ', link, date, is_read, is_favorite, id_feed, tags '
+		     . 'FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid=?';
+		$stm = $this->bd->prepare($sql);
+
+		$values = array(
+			$feed_id,
+			$id
+		);
+
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$entries = self::daoToEntry($res);
+		return isset($entries[0]) ? $entries[0] : null;
+	}
+
+	public function searchById($id) {
+		$sql = 'SELECT id, guid, title, author, '
+		     . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
+		     . ', link, date, is_read, is_favorite, id_feed, tags '
+		     . 'FROM `' . $this->prefix . 'entry` WHERE id=?';
+		$stm = $this->bd->prepare($sql);
+
+		$values = array($id);
+
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$entries = self::daoToEntry($res);
+		return isset($entries[0]) ? $entries[0] : null;
+	}
+
+	protected function sqlConcat($s1, $s2) {
+		return 'CONCAT(' . $s1 . ',' . $s2 . ')';	//MySQL
+	}
+
+	private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) {
+		if (!$state) {
+			$state = FreshRSS_Entry::STATE_ALL;
+		}
+		$where = '';
+		$joinFeed = false;
+		$values = array();
+		switch ($type) {
+			case 'a':
+				$where .= 'f.priority > 0 ';
+				$joinFeed = true;
+				break;
+			case 's':	//Deprecated: use $state instead
+				$where .= 'e1.is_favorite=1 ';
+				break;
+			case 'c':
+				$where .= 'f.category=? ';
+				$values[] = intval($id);
+				$joinFeed = true;
+				break;
+			case 'f':
+				$where .= 'e1.id_feed=? ';
+				$values[] = intval($id);
+				break;
+			case 'A':
+				$where .= '1 ';
+				break;
+			default:
+				throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
+		}
+
+		if ($state & FreshRSS_Entry::STATE_NOT_READ) {
+			if (!($state & FreshRSS_Entry::STATE_READ)) {
+				$where .= 'AND e1.is_read=0 ';
+			} elseif ($state & FreshRSS_Entry::STATE_STRICT) {
+				$where .= 'AND e1.is_read=0 ';
+			}
+		}
+		elseif ($state & FreshRSS_Entry::STATE_READ) {
+			$where .= 'AND e1.is_read=1 ';
+		}
+		if ($state & FreshRSS_Entry::STATE_FAVORITE) {
+			if (!($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) {
+				$where .= 'AND e1.is_favorite=1 ';
+			}
+		}
+		elseif ($state & FreshRSS_Entry::STATE_NOT_FAVORITE) {
+			$where .= 'AND e1.is_favorite=0 ';
+		}
+
+		switch ($order) {
+			case 'DESC':
+			case 'ASC':
+				break;
+			default:
+				throw new FreshRSS_EntriesGetter_Exception('Bad order in Entry->listByType: [' . $order . ']!');
+		}
+		if ($firstId === '' && parent::$sharedDbType === 'mysql') {
+			$firstId = $order === 'DESC' ? '9000000000'. '000000' : '0';	//MySQL optimization. Tested on MySQL 5.5 with 150k articles
+		}
+		if ($firstId !== '') {
+			$where .= 'AND e1.id ' . ($order === 'DESC' ? '<=' : '>=') . $firstId . ' ';
+		}
+		if (($date_min > 0) && ($type !== 's')) {
+			$where .= 'AND (e1.id >= ' . $date_min . '000000';
+			if ($showOlderUnreadsorFavorites) {	//Lax date constraint
+				$where .= ' OR e1.is_read=0 OR e1.is_favorite=1 OR (f.keep_history <> 0';
+				if (intval($keepHistoryDefault) === 0) {
+					$where .= ' AND f.keep_history <> -2';	//default
+				}
+				$where .= ')';
+			}
+			$where .= ') ';
+			$joinFeed = true;
+		}
+		$search = '';
+		if ($filter !== '') {
+			require_once(LIB_PATH . '/lib_date.php');
+			$filter = trim($filter);
+			$filter = addcslashes($filter, '\\%_');
+			$terms = array_unique(explode(' ', $filter));
+			//sort($terms);	//Put #tags first	//TODO: Put the cheapest filters first
+			foreach ($terms as $word) {
+				$word = trim($word);
+				if (stripos($word, 'intitle:') === 0) {
+					$word = substr($word, strlen('intitle:'));
+					$search .= 'AND e1.title LIKE ? ';
+					$values[] = '%' . $word .'%';
+				} elseif (stripos($word, 'inurl:') === 0) {
+					$word = substr($word, strlen('inurl:'));
+					$search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? ';
+					$values[] = '%' . $word .'%';
+				} elseif (stripos($word, 'author:') === 0) {
+					$word = substr($word, strlen('author:'));
+					$search .= 'AND e1.author LIKE ? ';
+					$values[] = '%' . $word .'%';
+				} elseif (stripos($word, 'date:') === 0) {
+					$word = substr($word, strlen('date:'));
+					list($minDate, $maxDate) = parseDateInterval($word);
+					if ($minDate) {
+						$search .= 'AND e1.id >= ' . $minDate . '000000 ';
+					}
+					if ($maxDate) {
+						$search .= 'AND e1.id <= ' . $maxDate . '000000 ';
+					}
+				} elseif (stripos($word, 'pubdate:') === 0) {
+					$word = substr($word, strlen('pubdate:'));
+					list($minDate, $maxDate) = parseDateInterval($word);
+					if ($minDate) {
+						$search .= 'AND e1.date >= ' . $minDate . ' ';
+					}
+					if ($maxDate) {
+						$search .= 'AND e1.date <= ' . $maxDate . ' ';
+					}
+				} else {
+					if ($word[0] === '#' && isset($word[1])) {
+						$search .= 'AND e1.tags LIKE ? ';
+						$values[] = '%' . $word .'%';
+					} else {
+						$search .= 'AND ' . $this->sqlconcat('e1.title', $this->isCompressed() ? 'UNCOMPRESS(content_bin)' : 'content') . ' LIKE ? ';
+						$values[] = '%' . $word .'%';
+					}
+				}
+			}
+		}
+
+		return array($values,
+			'SELECT e1.id FROM `' . $this->prefix . 'entry` e1 '
+			. ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed=f.id ' : '')
+			. 'WHERE ' . $where
+			. $search
+			. 'ORDER BY e1.id ' . $order
+			. ($limit > 0 ? ' LIMIT ' . $limit : ''));	//TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
+	}
+
+	public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) {
+		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault);
+
+		$sql = 'SELECT e.id, e.guid, e.title, e.author, '
+		     . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
+		     . ', e.link, e.date, e.is_read, e.is_favorite, e.id_feed, e.tags '
+		     . 'FROM `' . $this->prefix . 'entry` e '
+		     . 'INNER JOIN ('
+		     . $sql
+		     . ') e2 ON e2.id=e.id '
+		     . 'ORDER BY e.id ' . $order;
+
+		$stm = $this->bd->prepare($sql);
+		$stm->execute($values);
+
+		return self::daoToEntry($stm->fetchAll(PDO::FETCH_ASSOC));
+	}
+
+	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) {	//For API
+		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault);
+
+		$stm = $this->bd->prepare($sql);
+		$stm->execute($values);
+
+		return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+	}
+
+	public function listLastGuidsByFeed($id, $n) {
+		$sql = 'SELECT guid FROM `' . $this->prefix . 'entry` WHERE id_feed=? ORDER BY id DESC LIMIT ' . intval($n);
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$stm->execute($values);
+		return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+	}
+
+	public function countUnreadRead() {
+		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE priority > 0'
+		     . ' UNION SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE priority > 0 AND is_read=0';
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+		$all = empty($res[0]) ? 0 : $res[0];
+		$unread = empty($res[1]) ? 0 : $res[1];
+		return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
+	}
+	public function count($minPriority = null) {
+		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id';
+		if ($minPriority !== null) {
+			$sql = ' WHERE priority > ' . intval($minPriority);
+		}
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+		return $res[0];
+	}
+	public function countNotRead($minPriority = null) {
+		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE is_read=0';
+		if ($minPriority !== null) {
+			$sql = ' AND priority > ' . intval($minPriority);
+		}
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+		return $res[0];
+	}
+
+	public function countUnreadReadFavorites() {
+		$sql = 'SELECT c FROM ('
+		     .	'SELECT COUNT(id) AS c, 1 as o FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 '
+		     .	'UNION SELECT COUNT(id) AS c, 2 AS o FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 AND is_read=0'
+		     .	') u ORDER BY o';
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+		$all = empty($res[0]) ? 0 : $res[0];
+		$unread = empty($res[1]) ? 0 : $res[1];
+		return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
+	}
+
+	public function optimizeTable() {
+		$sql = 'OPTIMIZE TABLE `' . $this->prefix . 'entry`';	//MySQL
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+	}
+
+	public function size($all = false) {
+		$db = Minz_Configuration::dataBase();
+		$sql = 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema=?';	//MySQL
+		$values = array($db['base']);
+		if (!$all) {
+			$sql .= ' AND table_name LIKE ?';
+			$values[] = $this->prefix . '%';
+		}
+		$stm = $this->bd->prepare($sql);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+		return $res[0];
+	}
+
+	public static function daoToEntry($listDAO) {
+		$list = array();
+
+		if (!is_array($listDAO)) {
+			$listDAO = array($listDAO);
+		}
+
+		foreach ($listDAO as $key => $dao) {
+			$entry = new FreshRSS_Entry(
+				$dao['id_feed'],
+				$dao['guid'],
+				$dao['title'],
+				$dao['author'],
+				$dao['content'],
+				$dao['link'],
+				$dao['date'],
+				$dao['is_read'],
+				$dao['is_favorite'],
+				$dao['tags']
+			);
+			if (isset($dao['id'])) {
+				$entry->_id($dao['id']);
+			}
+			$list[] = $entry;
+		}
+
+		unset($listDAO);
+
+		return $list;
+	}
+}

+ 129 - 0
app/Models/EntryDAOSQLite.php

@@ -0,0 +1,129 @@
+<?php
+
+class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
+
+	protected function sqlConcat($s1, $s2) {
+		return $s1 . '||' . $s2;
+	}
+
+	protected function updateCacheUnreads($catId = false, $feedId = false) {
+		$sql = 'UPDATE `' . $this->prefix . 'feed` '
+		 . 'SET cache_nbUnreads=('
+		 .	'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e '
+		 .	'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0) '
+		 . 'WHERE 1';
+		$values = array();
+		if ($feedId !== false) {
+			$sql .= ' AND id=?';
+			$values[] = $feedId;
+		}
+		if ($catId !== false) {
+			$sql .= ' AND category=?';
+			$values[] = $catId;
+		}
+		$stm = $this->bd->prepare($sql);
+		if ($stm && $stm->execute($values)) {
+			return true;
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateCacheUnreads: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function markRead($ids, $is_read = true) {
+		if (is_array($ids)) {	//Many IDs at once (used by API)
+			if (true) {	//Speed heuristics	//TODO: Not implemented yet for SQLite (so always call IDs one by one)
+				$affected = 0;
+				foreach ($ids as $id) {
+					$affected += $this->markRead($id, $is_read);
+				}
+				return $affected;
+			}
+		} else {
+			$this->bd->beginTransaction();
+			$sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=? WHERE id=? AND is_read=?';
+			$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
+			$stm = $this->bd->prepare($sql);
+			if (!($stm && $stm->execute($values))) {
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::record('SQL error markRead 1: ' . $info[2], Minz_Log::ERROR);
+				$this->bd->rollBack();
+				return false;
+			}
+			$affected = $stm->rowCount();
+			if ($affected > 0) {
+				$sql = 'UPDATE `' . $this->prefix . 'feed` SET cache_nbUnreads=cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
+				 . 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)';
+				$values = array($ids);
+				$stm = $this->bd->prepare($sql);
+				if (!($stm && $stm->execute($values))) {
+					$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+					Minz_Log::record('SQL error markRead 2: ' . $info[2], Minz_Log::ERROR);
+					$this->bd->rollBack();
+					return false;
+				}
+			}
+			$this->bd->commit();
+			return $affected;
+		}
+	}
+
+	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) {
+		if ($idMax == 0) {
+			$idMax = time() . '000000';
+			Minz_Log::record('Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG);
+		}
+
+		$sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=1 WHERE is_read=0 AND id <= ?';
+		if ($onlyFavorites) {
+			$sql .= ' AND is_favorite=1';
+		} elseif ($priorityMin >= 0) {
+			$sql .= ' AND id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.priority > ' . intval($priorityMin) . ')';
+		}
+		$values = array($idMax);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markReadEntries: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+		$affected = $stm->rowCount();
+		if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+			return false;
+		}
+		return $affected;
+	}
+
+	public function markReadCat($id, $idMax = 0) {
+		if ($idMax == 0) {
+			$idMax = time() . '000000';
+			Minz_Log::record('Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG);
+		}
+
+		$sql = 'UPDATE `' . $this->prefix . 'entry` '
+			 . 'SET is_read=1 '
+			 . 'WHERE is_read=0 AND id <= ? AND '
+			 . 'id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.category=?)';
+		$values = array($idMax, $id);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error markReadCat: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+		$affected = $stm->rowCount();
+		if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) {
+			return false;
+		}
+		return $affected;
+	}
+
+	public function optimizeTable() {
+		//TODO: Search for an equivalent in SQLite
+	}
+
+	public function size($all = false) {
+		return @filesize(DATA_PATH . '/' . Minz_Session::param('currentUser', '_') . '.sqlite');
+	}
+}

+ 32 - 0
app/Models/Factory.php

@@ -0,0 +1,32 @@
+<?php
+
+class FreshRSS_Factory {
+
+	public static function createFeedDao() {
+		$db = Minz_Configuration::dataBase();
+		if ($db['type'] === 'sqlite') {
+			return new FreshRSS_FeedDAOSQLite();
+		} else {
+			return new FreshRSS_FeedDAO();
+		}
+	}
+
+	public static function createEntryDao() {
+		$db = Minz_Configuration::dataBase();
+		if ($db['type'] === 'sqlite') {
+			return new FreshRSS_EntryDAOSQLite();
+		} else {
+			return new FreshRSS_EntryDAO();
+		}
+	}
+
+	public static function createStatsDAO() {
+		$db = Minz_Configuration::dataBase();
+		if ($db['type'] === 'sqlite') {
+			return new FreshRSS_StatsDAOSQLite();
+		} else {
+			return new FreshRSS_StatsDAO();
+		}
+	}
+
+}

+ 331 - 0
app/Models/Feed.php

@@ -0,0 +1,331 @@
+<?php
+
+class FreshRSS_Feed extends Minz_Model {
+	private $id = 0;
+	private $url;
+	private $category = 1;
+	private $nbEntries = -1;
+	private $nbNotRead = -1;
+	private $entries = null;
+	private $name = '';
+	private $website = '';
+	private $description = '';
+	private $lastUpdate = 0;
+	private $priority = 10;
+	private $pathEntries = '';
+	private $httpAuth = '';
+	private $error = false;
+	private $keep_history = -2;
+	private $ttl = -2;
+	private $hash = null;
+	private $lockPath = '';
+
+	public function __construct($url, $validate=true) {
+		if ($validate) {
+			$this->_url($url);
+		} else {
+			$this->url = $url;
+		}
+	}
+
+	public static function example() {
+		$f = new FreshRSS_Feed('http://example.net/', false);
+		$f->faviconPrepare();
+		return $f;
+	}
+
+	public function id() {
+		return $this->id;
+	}
+
+	public function hash() {
+		if ($this->hash === null) {
+			$this->hash = hash('crc32b', Minz_Configuration::salt() . $this->url);
+		}
+		return $this->hash;
+	}
+
+	public function url() {
+		return $this->url;
+	}
+	public function category() {
+		return $this->category;
+	}
+	public function entries() {
+		return $this->entries === null ? array() : $this->entries;
+	}
+	public function name() {
+		return $this->name;
+	}
+	public function website() {
+		return $this->website;
+	}
+	public function description() {
+		return $this->description;
+	}
+	public function lastUpdate() {
+		return $this->lastUpdate;
+	}
+	public function priority() {
+		return $this->priority;
+	}
+	public function pathEntries() {
+		return $this->pathEntries;
+	}
+	public function httpAuth($raw = true) {
+		if ($raw) {
+			return $this->httpAuth;
+		} else {
+			$pos_colon = strpos($this->httpAuth, ':');
+			$user = substr($this->httpAuth, 0, $pos_colon);
+			$pass = substr($this->httpAuth, $pos_colon + 1);
+
+			return array(
+				'username' => $user,
+				'password' => $pass
+			);
+		}
+	}
+	public function inError() {
+		return $this->error;
+	}
+	public function keepHistory() {
+		return $this->keep_history;
+	}
+	public function ttl() {
+		return $this->ttl;
+	}
+	public function nbEntries() {
+		if ($this->nbEntries < 0) {
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$this->nbEntries = $feedDAO->countEntries($this->id());
+		}
+
+		return $this->nbEntries;
+	}
+	public function nbNotRead() {
+		if ($this->nbNotRead < 0) {
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$this->nbNotRead = $feedDAO->countNotRead($this->id());
+		}
+
+		return $this->nbNotRead;
+	}
+	public function faviconPrepare() {
+		$file = DATA_PATH . '/favicons/' . $this->hash() . '.txt';
+		if (!file_exists($file)) {
+			$t = $this->website;
+			if ($t == '') {
+				$t = $this->url;
+			}
+			file_put_contents($file, $t);
+		}
+	}
+	public static function faviconDelete($hash) {
+		$path = DATA_PATH . '/favicons/' . $hash;
+		@unlink($path . '.ico');
+		@unlink($path . '.txt');
+	}
+	public function favicon() {
+		return Minz_Url::display('/f.php?' . $this->hash());
+	}
+
+	public function _id($value) {
+		$this->id = $value;
+	}
+	public function _url($value, $validate=true) {
+		$this->hash = null;
+		if ($validate) {
+			$value = checkUrl($value);
+		}
+		if (empty($value)) {
+			throw new FreshRSS_BadUrl_Exception($value);
+		}
+		$this->url = $value;
+	}
+	public function _category($value) {
+		$value = intval($value);
+		$this->category = $value >= 0 ? $value : 0;
+	}
+	public function _name($value) {
+		$this->name = $value === null ? '' : $value;
+	}
+	public function _website($value, $validate=true) {
+		if ($validate) {
+			$value = checkUrl($value);
+		}
+		if (empty($value)) {
+			$value = '';
+		}
+		$this->website = $value;
+	}
+	public function _description($value) {
+		$this->description = $value === null ? '' : $value;
+	}
+	public function _lastUpdate($value) {
+		$this->lastUpdate = $value;
+	}
+	public function _priority($value) {
+		$value = intval($value);
+		$this->priority = $value >= 0 ? $value : 10;
+	}
+	public function _pathEntries($value) {
+		$this->pathEntries = $value;
+	}
+	public function _httpAuth($value) {
+		$this->httpAuth = $value;
+	}
+	public function _error($value) {
+		$this->error = (bool)$value;
+	}
+	public function _keepHistory($value) {
+		$value = intval($value);
+		$value = min($value, 1000000);
+		$value = max($value, -2);
+		$this->keep_history = $value;
+	}
+	public function _ttl($value) {
+		$value = intval($value);
+		$value = min($value, 100000000);
+		$value = max($value, -2);
+		$this->ttl = $value;
+	}
+	public function _nbNotRead($value) {
+		$this->nbNotRead = intval($value);
+	}
+	public function _nbEntries($value) {
+		$this->nbEntries = intval($value);
+	}
+
+	public function load($loadDetails = false) {
+		if ($this->url !== null) {
+			if (CACHE_PATH === false) {
+				throw new Minz_FileNotExistException(
+					'CACHE_PATH',
+					Minz_Exception::ERROR
+				);
+			} else {
+				$url = htmlspecialchars_decode($this->url, ENT_QUOTES);
+				if ($this->httpAuth != '') {
+					$url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url);
+				}
+				$feed = customSimplePie();
+				$feed->set_feed_url($url);
+				if (!$loadDetails) {	//Only activates auto-discovery when adding a new feed
+					$feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE);
+				}
+				$mtime = $feed->init();
+
+				if ((!$mtime) || $feed->error()) {
+					throw new FreshRSS_Feed_Exception($feed->error() . ' [' . $url . ']');
+				}
+
+				if ($loadDetails) {
+					// si on a utilisé l'auto-discover, notre url va avoir changé
+					$subscribe_url = $feed->subscribe_url(false);
+
+					$title = strtr(html_only_entity_decode($feed->get_title()), array('<' => '&lt;', '>' => '&gt;', '"' => '&quot;'));	//HTML to HTML-PRE	//ENT_COMPAT except &
+					$this->_name($title == '' ? $this->url : $title);
+
+					$this->_website(html_only_entity_decode($feed->get_link()));
+					$this->_description(html_only_entity_decode($feed->get_description()));
+				} else {
+					//The case of HTTP 301 Moved Permanently
+					$subscribe_url = $feed->subscribe_url(true);
+				}
+
+				if ($subscribe_url !== null && $subscribe_url !== $this->url) {
+					if ($this->httpAuth != '') {
+						// on enlève les id si authentification HTTP
+						$subscribe_url = preg_replace('#((.+)://)((.+)@)(.+)#', '${1}${5}', $subscribe_url);
+					}
+					$this->_url($subscribe_url);
+				}
+
+				if (($mtime === true) ||($mtime > $this->lastUpdate)) {
+					syslog(LOG_DEBUG, 'FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $subscribe_url);
+					$this->loadEntries($feed);	// et on charge les articles du flux
+				} else {
+					syslog(LOG_DEBUG, 'FreshRSS use cache for ' . $subscribe_url);
+					$this->entries = array();
+				}
+
+				$feed->__destruct();	//http://simplepie.org/wiki/faq/i_m_getting_memory_leaks
+				unset($feed);
+			}
+		}
+	}
+
+	private function loadEntries($feed) {
+		$entries = array();
+
+		foreach ($feed->get_items() as $item) {
+			$title = html_only_entity_decode(strip_tags($item->get_title()));
+			$author = $item->get_author();
+			$link = $item->get_permalink();
+			$date = @strtotime($item->get_date());
+
+			// gestion des tags (catégorie == tag)
+			$tags_tmp = $item->get_categories();
+			$tags = array();
+			if ($tags_tmp !== null) {
+				foreach ($tags_tmp as $tag) {
+					$tags[] = html_only_entity_decode($tag->get_label());
+				}
+			}
+
+			$content = html_only_entity_decode($item->get_content());
+
+			$elinks = array();
+			foreach ($item->get_enclosures() as $enclosure) {
+				$elink = $enclosure->get_link();
+				if (empty($elinks[$elink])) {
+					$elinks[$elink] = '1';
+					$mime = strtolower($enclosure->get_type());
+					if (strpos($mime, 'image/') === 0) {
+						$content .= '<br /><img lazyload="" postpone="" src="' . $elink . '" alt="" />';
+					} elseif (strpos($mime, 'audio/') === 0) {
+						$content .= '<br /><audio lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />';
+					} elseif (strpos($mime, 'video/') === 0) {
+						$content .= '<br /><video lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />';
+					}
+				}
+			}
+
+			$entry = new FreshRSS_Entry(
+				$this->id(),
+				$item->get_id(),
+				$title === null ? '' : $title,
+				$author === null ? '' : html_only_entity_decode($author->name),
+				$content === null ? '' : $content,
+				$link === null ? '' : $link,
+				$date ? $date : time()
+			);
+			$entry->_tags($tags);
+			// permet de récupérer le contenu des flux tronqués
+			$entry->loadCompleteContent($this->pathEntries());
+
+			$entries[] = $entry;
+			unset($item);
+		}
+
+		$this->entries = $entries;
+	}
+
+	function lock() {
+		$this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock';
+		if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) {
+			@unlink($this->lockPath);
+		}
+		if (($handle = @fopen($this->lockPath, 'x')) === false) {
+			return false;
+		}
+		//register_shutdown_function('unlink', $this->lockPath);
+		@fclose($handle);
+		return true;
+	}
+
+	function unlock() {
+		@unlink($this->lockPath);
+	}
+}

+ 388 - 0
app/Models/FeedDAO.php

@@ -0,0 +1,388 @@
+<?php
+
+class FreshRSS_FeedDAO extends Minz_ModelPdo {
+	public function addFeed($valuesTmp) {
+		$sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
+		$stm = $this->bd->prepare($sql);
+
+		$values = array(
+			substr($valuesTmp['url'], 0, 511),
+			$valuesTmp['category'],
+			substr($valuesTmp['name'], 0, 255),
+			substr($valuesTmp['website'], 0, 255),
+			substr($valuesTmp['description'], 0, 1023),
+			$valuesTmp['lastUpdate'],
+			base64_encode($valuesTmp['httpAuth']),
+		);
+
+		if ($stm && $stm->execute($values)) {
+			return $this->bd->lastInsertId();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error addFeed: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function addFeedObject($feed) {
+		// TODO: not sure if we should write this method in DAO since DAO
+		// should not be aware about feed class
+
+		// Add feed only if we don't find it in DB
+		$feed_search = $this->searchByUrl($feed->url());
+		if (!$feed_search) {
+			$values = array(
+				'id' => $feed->id(),
+				'url' => $feed->url(),
+				'category' => $feed->category(),
+				'name' => $feed->name(),
+				'website' => $feed->website(),
+				'description' => $feed->description(),
+				'lastUpdate' => 0,
+				'httpAuth' => $feed->httpAuth()
+			);
+
+			$id = $this->addFeed($values);
+			if ($id) {
+				$feed->_id($id);
+				$feed->faviconPrepare();
+			}
+
+			return $id;
+		}
+
+		return $feed_search->id();
+	}
+
+	public function updateFeed($id, $valuesTmp) {
+		$set = '';
+		foreach ($valuesTmp as $key => $v) {
+			$set .= $key . '=?, ';
+
+			if ($key == 'httpAuth') {
+				$valuesTmp[$key] = base64_encode($v);
+			}
+		}
+		$set = substr($set, 0, -2);
+
+		$sql = 'UPDATE `' . $this->prefix . 'feed` SET ' . $set . ' WHERE id=?';
+		$stm = $this->bd->prepare($sql);
+
+		foreach ($valuesTmp as $v) {
+			$values[] = $v;
+		}
+		$values[] = $id;
+
+		if ($stm && $stm->execute($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateFeed: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function updateLastUpdate($id, $inError = 0, $updateCache = true) {
+		if ($updateCache) {
+			$sql = 'UPDATE `' . $this->prefix . 'feed` '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
+			     . 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+			     . 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
+			     . 'lastUpdate=?, error=? '
+			     . 'WHERE id=?';
+		} else {
+			$sql = 'UPDATE `' . $this->prefix . 'feed` '
+			     . 'SET lastUpdate=?, error=? '
+			     . 'WHERE id=?';
+		}
+
+		$values = array(
+			time(),
+			$inError,
+			$id,
+		);
+
+		$stm = $this->bd->prepare($sql);
+
+		if ($stm && $stm->execute($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateLastUpdate: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function changeCategory($idOldCat, $idNewCat) {
+		$catDAO = new FreshRSS_CategoryDAO();
+		$newCat = $catDAO->searchById($idNewCat);
+		if (!$newCat) {
+			$newCat = $catDAO->getDefault();
+		}
+
+		$sql = 'UPDATE `' . $this->prefix . 'feed` SET category=? WHERE category=?';
+		$stm = $this->bd->prepare($sql);
+
+		$values = array(
+			$newCat->id(),
+			$idOldCat
+		);
+
+		if ($stm && $stm->execute($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error changeCategory: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function deleteFeed($id) {
+		$sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE id=?';
+		$stm = $this->bd->prepare($sql);
+
+		$values = array($id);
+
+		if ($stm && $stm->execute($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error deleteFeed: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+	public function deleteFeedByCategory($id) {
+		$sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE category=?';
+		$stm = $this->bd->prepare($sql);
+
+		$values = array($id);
+
+		if ($stm && $stm->execute($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error deleteFeedByCategory: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function searchById($id) {
+		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE id=?';
+		$stm = $this->bd->prepare($sql);
+
+		$values = array($id);
+
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$feed = self::daoToFeed($res);
+
+		if (isset($feed[$id])) {
+			return $feed[$id];
+		} else {
+			return null;
+		}
+	}
+	public function searchByUrl($url) {
+		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE url=?';
+		$stm = $this->bd->prepare($sql);
+
+		$values = array($url);
+
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$feed = current(self::daoToFeed($res));
+
+		if (isset($feed)) {
+			return $feed;
+		} else {
+			return null;
+		}
+	}
+
+	public function listFeeds() {
+		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` ORDER BY name';
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+
+		return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+	}
+
+	public function arrayFeedCategoryNames() {	//For API
+		$sql = 'SELECT f.id, f.name, c.name as c_name FROM `' . $this->prefix . 'feed` f '
+		     . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category';
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$feedCategoryNames = array();
+		foreach ($res as $line) {
+			$feedCategoryNames[$line['id']] = array(
+				'name' => $line['name'],
+				'c_name' => $line['c_name'],
+			);
+		}
+		return $feedCategoryNames;
+	}
+
+	public function listFeedsOrderUpdate($defaultCacheDuration = 3600) {
+		if ($defaultCacheDuration < 0) {
+			$defaultCacheDuration = 2147483647;
+		}
+		$sql = 'SELECT id, url, name, website, lastUpdate, pathEntries, httpAuth, keep_history, ttl '
+		     . 'FROM `' . $this->prefix . 'feed` '
+		     . 'WHERE ttl <> -1 AND lastUpdate < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) '
+		     . 'ORDER BY lastUpdate';
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute())) {
+			$sql2 = 'ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN ttl INT NOT NULL DEFAULT -2';	//v0.7.3
+			$stm = $this->bd->prepare($sql2);
+			$stm->execute();
+			$stm = $this->bd->prepare($sql);
+			$stm->execute();
+		}
+
+		return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+	}
+
+	public function listByCategory($cat) {
+		$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE category=? ORDER BY name';
+		$stm = $this->bd->prepare($sql);
+
+		$values = array($cat);
+
+		$stm->execute($values);
+
+		return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+	}
+
+	public function countEntries($id) {
+		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+		return $res[0]['count'];
+	}
+
+	public function countNotRead($id) {
+		$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND is_read=0';
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+		return $res[0]['count'];
+	}
+
+	public function updateCachedValues() {	//For one single feed, call updateLastUpdate($id)
+		$sql = 'UPDATE `' . $this->prefix . 'feed` f '
+		     . 'INNER JOIN ('
+		     .	'SELECT e.id_feed, '
+		     .	'COUNT(CASE WHEN e.is_read = 0 THEN 1 END) AS nbUnreads, '
+		     .	'COUNT(e.id) AS nbEntries '
+		     .	'FROM `' . $this->prefix . 'entry` e '
+		     .	'GROUP BY e.id_feed'
+		     . ') x ON x.id_feed=f.id '
+		     . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads';
+		$stm = $this->bd->prepare($sql);
+
+		if ($stm && $stm->execute()) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateCachedValues: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function truncate($id) {
+		$sql = 'DELETE FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$this->bd->beginTransaction();
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error truncate: ' . $info[2], Minz_Log::ERROR);
+			$this->bd->rollBack();
+			return false;
+		}
+		$affected = $stm->rowCount();
+
+		$sql = 'UPDATE `' . $this->prefix . 'feed` '
+			 . 'SET cache_nbEntries=0, cache_nbUnreads=0 WHERE id=?';
+		$values = array($id);
+		$stm = $this->bd->prepare($sql);
+		if (!($stm && $stm->execute($values))) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error truncate: ' . $info[2], Minz_Log::ERROR);
+			$this->bd->rollBack();
+			return false;
+		}
+
+		$this->bd->commit();
+		return $affected;
+	}
+
+	public function cleanOldEntries($id, $date_min, $keep = 15) {	//Remember to call updateLastUpdate($id) just after
+		$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
+		     . 'WHERE id_feed = :id_feed AND id <= :id_max AND is_favorite=0 AND id NOT IN '
+		     . '(SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed = :id_feed ORDER BY id DESC LIMIT :keep) keep)';	//Double select MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
+		$stm = $this->bd->prepare($sql);
+
+		$id_max = intval($date_min) . '000000';
+
+		$stm->bindParam(':id_feed', $id, PDO::PARAM_INT);
+		$stm->bindParam(':id_max', $id_max, PDO::PARAM_STR);
+		$stm->bindParam(':keep', $keep, PDO::PARAM_INT);
+
+		if ($stm && $stm->execute()) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error cleanOldEntries: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public static function daoToFeed($listDAO, $catID = null) {
+		$list = array();
+
+		if (!is_array($listDAO)) {
+			$listDAO = array($listDAO);
+		}
+
+		foreach ($listDAO as $key => $dao) {
+			if (!isset($dao['name'])) {
+				continue;
+			}
+			if (isset($dao['id'])) {
+				$key = $dao['id'];
+			}
+			if ($catID === null) {
+				$category = isset($dao['category']) ? $dao['category'] : 0;
+			} else {
+				$category = $catID ;
+			}
+
+			$myFeed = new FreshRSS_Feed(isset($dao['url']) ? $dao['url'] : '', false);
+			$myFeed->_category($category);
+			$myFeed->_name($dao['name']);
+			$myFeed->_website(isset($dao['website']) ? $dao['website'] : '', false);
+			$myFeed->_description(isset($dao['description']) ? $dao['description'] : '');
+			$myFeed->_lastUpdate(isset($dao['lastUpdate']) ? $dao['lastUpdate'] : 0);
+			$myFeed->_priority(isset($dao['priority']) ? $dao['priority'] : 10);
+			$myFeed->_pathEntries(isset($dao['pathEntries']) ? $dao['pathEntries'] : '');
+			$myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode($dao['httpAuth']) : '');
+			$myFeed->_error(isset($dao['error']) ? $dao['error'] : 0);
+			$myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : -2);
+			$myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : -2);
+			$myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0);
+			$myFeed->_nbEntries(isset($dao['cache_nbEntries']) ? $dao['cache_nbEntries'] : 0);
+			if (isset($dao['id'])) {
+				$myFeed->_id($dao['id']);
+			}
+			$list[$key] = $myFeed;
+		}
+
+		return $list;
+	}
+}

+ 19 - 0
app/Models/FeedDAOSQLite.php

@@ -0,0 +1,19 @@
+<?php
+
+class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
+
+	public function updateCachedValues() {	//For one single feed, call updateLastUpdate($id)
+		$sql = 'UPDATE `' . $this->prefix . 'feed` '
+		     . 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+		     . 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
+		$stm = $this->bd->prepare($sql);
+		if ($stm && $stm->execute()) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record('SQL error updateCachedValues: ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+}

+ 26 - 0
app/Models/Log.php

@@ -0,0 +1,26 @@
+<?php
+
+class FreshRSS_Log extends Minz_Model {
+	private $date;
+	private $level;
+	private $information;
+
+	public function date () {
+		return $this->date;
+	}
+	public function level () {
+		return $this->level;
+	}
+	public function info () {
+		return $this->information;
+	}
+	public function _date ($date) {
+		$this->date = $date;
+	}
+	public function _level ($level) {
+		$this->level = $level;
+	}
+	public function _info ($information) {
+		$this->information = $information;
+	}
+}

+ 25 - 0
app/Models/LogDAO.php

@@ -0,0 +1,25 @@
+<?php
+
+class FreshRSS_LogDAO {
+	public static function lines() {
+		$logs = array ();
+		$handle = @fopen(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log', 'r');
+		if ($handle) {
+			while (($line = fgets($handle)) !== false) {
+				if (preg_match ('/^\[([^\[]+)\] \[([^\[]+)\] --- (.*)$/', $line, $matches)) {
+					$myLog = new FreshRSS_Log ();
+					$myLog->_date ($matches[1]);
+					$myLog->_level ($matches[2]);
+					$myLog->_info ($matches[3]);
+					$logs[] = $myLog;
+				}
+			}
+			fclose($handle);
+		}
+		return array_reverse($logs);
+	}
+
+	public static function truncate() {
+		file_put_contents(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log', '');
+	}
+}

+ 44 - 0
app/Models/Share.php

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

+ 404 - 0
app/Models/StatsDAO.php

@@ -0,0 +1,404 @@
+<?php
+
+class FreshRSS_StatsDAO extends Minz_ModelPdo {
+
+	const ENTRY_COUNT_PERIOD = 30;
+
+	/**
+	 * Calculates entry repartition for all feeds and for main stream.
+	 * The repartition includes:
+	 *   - total entries
+	 *   - read entries
+	 *   - unread entries
+	 *   - favorite entries
+	 *
+	 * @return type
+	 */
+	public function calculateEntryRepartition() {
+		$repartition = array();
+
+		// Generates the repartition for the main stream of entry
+		$sql = <<<SQL
+SELECT COUNT(1) AS `total`,
+COUNT(1) - SUM(e.is_read) AS `unread`,
+SUM(e.is_read) AS `read`,
+SUM(e.is_favorite) AS `favorite`
+FROM {$this->prefix}entry AS e
+, {$this->prefix}feed AS f
+WHERE e.id_feed = f.id
+AND f.priority = 10
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$repartition['main_stream'] = $res[0];
+
+		// Generates the repartition for all entries
+		$sql = <<<SQL
+SELECT COUNT(1) AS `total`,
+COUNT(1) - SUM(e.is_read) AS `unread`,
+SUM(e.is_read) AS `read`,
+SUM(e.is_favorite) AS `favorite`
+FROM {$this->prefix}entry AS e
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		$repartition['all_feeds'] = $res[0];
+
+		return $repartition;
+	}
+
+	/**
+	 * Calculates entry count per day on a 30 days period.
+	 * Returns the result as a JSON string.
+	 *
+	 * @return string
+	 */
+	public function calculateEntryCount() {
+		$count = $this->initEntryCountArray();
+		$period = self::ENTRY_COUNT_PERIOD;
+
+		// Get stats per day for the last 30 days
+		$sql = <<<SQL
+SELECT DATEDIFF(FROM_UNIXTIME(e.date), NOW()) AS day,
+COUNT(1) AS count
+FROM {$this->prefix}entry AS e
+WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -{$period} DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d')
+GROUP BY day
+ORDER BY day ASC
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+		foreach ($res as $value) {
+			$count[$value['day']] = (int) $value['count'];
+		}
+
+		return $this->convertToSerie($count);
+	}
+
+	/**
+	 * Initialize an array for the entry count.
+	 *
+	 * @return array
+	 */
+	protected function initEntryCountArray() {
+		return $this->initStatsArray(-self::ENTRY_COUNT_PERIOD, -1);
+	}
+
+	/**
+	 * Calculates the number of article per hour of the day per feed
+	 *
+	 * @param integer $feed id
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerHour($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('%H', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per day of week per feed
+	 *
+	 * @param integer $feed id
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('%w', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per month per feed
+	 *
+	 * @param integer $feed
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerMonth($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('%m', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per period per feed
+	 *
+	 * @param string $period format string to use for grouping
+	 * @param integer $feed id
+	 * @return string
+	 */
+	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
+		if ($feed) {
+			$restrict = "WHERE e.id_feed = {$feed}";
+		} else {
+			$restrict = '';
+		}
+		$sql = <<<SQL
+SELECT DATE_FORMAT(FROM_UNIXTIME(e.date), '{$period}') AS period
+, COUNT(1) AS count
+FROM {$this->prefix}entry AS e
+{$restrict}
+GROUP BY period
+ORDER BY period ASC
+SQL;
+
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_NAMED);
+
+		foreach ($res as $value) {
+			$repartition[(int) $value['period']] = (int) $value['count'];
+		}
+
+		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);
+		$interval_in_days = $interval->format('%a');
+		if ($interval_in_days <= 0) {
+			// Surely only one article.
+			// We will return count / (period/period) == count.
+			$interval_in_days = $period;
+		}
+
+		return round($res['count'] / ($interval_in_days / $period), 2);
+	}
+
+	/**
+	 * Initialize an array for statistics depending on a range
+	 *
+	 * @param integer $min
+	 * @param integer $max
+	 * @return array
+	 */
+	protected function initStatsArray($min, $max) {
+		return array_map(function () {
+			return 0;
+		}, array_flip(range($min, $max)));
+	}
+
+	/**
+	 * Calculates feed count per category.
+	 * Returns the result as a JSON string.
+	 *
+	 * @return string
+	 */
+	public function calculateFeedByCategory() {
+		$sql = <<<SQL
+SELECT c.name AS label
+, COUNT(f.id) AS data
+FROM {$this->prefix}category AS c,
+{$this->prefix}feed AS f
+WHERE c.id = f.category
+GROUP BY label
+ORDER BY data DESC
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+		return $this->convertToPieSerie($res);
+	}
+
+	/**
+	 * Calculates entry count per category.
+	 * Returns the result as a JSON string.
+	 *
+	 * @return string
+	 */
+	public function calculateEntryByCategory() {
+		$sql = <<<SQL
+SELECT c.name AS label
+, COUNT(e.id) AS data
+FROM {$this->prefix}category AS c,
+{$this->prefix}feed AS f,
+{$this->prefix}entry AS e
+WHERE c.id = f.category
+AND f.id = e.id_feed
+GROUP BY label
+ORDER BY data DESC
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+		return $this->convertToPieSerie($res);
+	}
+
+	/**
+	 * Calculates the 10 top feeds based on their number of entries
+	 *
+	 * @return array
+	 */
+	public function calculateTopFeed() {
+		$sql = <<<SQL
+SELECT f.id AS id
+, MAX(f.name) AS name
+, MAX(c.name) AS category
+, COUNT(e.id) AS count
+FROM {$this->prefix}category AS c,
+{$this->prefix}feed AS f,
+{$this->prefix}entry AS e
+WHERE c.id = f.category
+AND f.id = e.id_feed
+GROUP BY f.id
+ORDER BY count DESC
+LIMIT 10
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		return $stm->fetchAll(PDO::FETCH_ASSOC);
+	}
+
+	/**
+	 * Calculates the last publication date for each feed
+	 *
+	 * @return array
+	 */
+	public function calculateFeedLastDate() {
+		$sql = <<<SQL
+SELECT MAX(f.id) as id
+, MAX(f.name) AS name
+, MAX(date) AS last_date
+, COUNT(*) AS nb_articles
+FROM {$this->prefix}feed AS f,
+{$this->prefix}entry AS e
+WHERE f.id = e.id_feed
+GROUP BY f.id
+ORDER BY name
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		return $stm->fetchAll(PDO::FETCH_ASSOC);
+	}
+
+	protected function convertToSerie($data) {
+		$serie = array();
+
+		foreach ($data as $key => $value) {
+			$serie[] = array($key, $value);
+		}
+
+		return json_encode($serie);
+	}
+
+	protected function convertToPieSerie($data) {
+		$serie = array();
+
+		foreach ($data as $value) {
+			$value['data'] = array(array(0, (int) $value['data']));
+			$serie[] = $value;
+		}
+
+		return json_encode($serie);
+	}
+
+	/**
+	 * Gets days ready for graphs
+	 *
+	 * @return string
+	 */
+	public function getDays() {
+		return $this->convertToTranslatedJson(array(
+			'sun',
+			'mon',
+			'tue',
+			'wed',
+			'thu',
+			'fri',
+			'sat',
+		));
+	}
+
+	/**
+	 * Gets months ready for graphs
+	 *
+	 * @return string
+	 */
+	public function getMonths() {
+		return $this->convertToTranslatedJson(array(
+			'jan',
+			'feb',
+			'mar',
+			'apr',
+			'may',
+			'jun',
+			'jul',
+			'aug',
+			'sep',
+			'oct',
+			'nov',
+			'dec',
+		));
+	}
+
+	/**
+	 * Translates array content and encode it as JSON
+	 *
+	 * @param array $data
+	 * @return string
+	 */
+	private function convertToTranslatedJson($data = array()) {
+		$translated = array_map(function ($a) {
+			return Minz_Translate::t($a);
+		}, $data);
+
+		return json_encode($translated);
+	}
+
+}

+ 64 - 0
app/Models/StatsDAOSQLite.php

@@ -0,0 +1,64 @@
+<?php
+
+class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
+
+	/**
+	 * Calculates entry count per day on a 30 days period.
+	 * Returns the result as a JSON string.
+	 *
+	 * @return string
+	 */
+	public function calculateEntryCount() {
+		$count = $this->initEntryCountArray();
+		$period = parent::ENTRY_COUNT_PERIOD;
+
+		// Get stats per day for the last 30 days
+		$sql = <<<SQL
+SELECT round(julianday(e.date, 'unixepoch') - julianday('now')) AS day,
+COUNT(1) AS count
+FROM {$this->prefix}entry AS e
+WHERE strftime('%Y%m%d', e.date, 'unixepoch')
+	BETWEEN strftime('%Y%m%d', 'now', '-{$period} days')
+	AND strftime('%Y%m%d', 'now', '-1 day')
+GROUP BY day
+ORDER BY day ASC
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+		foreach ($res as $value) {
+			$count[(int) $value['day']] = (int) $value['count'];
+		}
+
+		return $this->convertToSerie($count);
+	}
+
+	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
+		if ($feed) {
+			$restrict = "WHERE e.id_feed = {$feed}";
+		} else {
+			$restrict = '';
+		}
+		$sql = <<<SQL
+SELECT strftime('{$period}', e.date, 'unixepoch') AS period
+, COUNT(1) AS count
+FROM {$this->prefix}entry AS e
+{$restrict}
+GROUP BY period
+ORDER BY period ASC
+SQL;
+
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_NAMED);
+
+		$repartition = array();
+		foreach ($res as $value) {
+			$repartition[(int) $value['period']] = (int) $value['count'];
+		}
+
+		return $this->convertToSerie($repartition);
+	}
+
+}

+ 121 - 0
app/Models/Themes.php

@@ -0,0 +1,121 @@
+<?php
+
+class FreshRSS_Themes extends Minz_Model {
+	private static $themesUrl = '/themes/';
+	private static $defaultIconsUrl = '/themes/icons/';
+	public static $defaultTheme = 'Origine';
+
+	public static function getList() {
+		return array_values(array_diff(
+			scandir(PUBLIC_PATH . self::$themesUrl),
+			array('..', '.')
+		));
+	}
+
+	public static function get() {
+		$themes_list = self::getList();
+		$list = array();
+		foreach ($themes_list as $theme_dir) {
+			$theme = self::get_infos($theme_dir);
+			if ($theme) {
+				$list[$theme_dir] = $theme;
+			}
+		}
+		return $list;
+	}
+
+	public static function get_infos($theme_id) {
+		$theme_dir = PUBLIC_PATH . self::$themesUrl . $theme_id ;
+		if (is_dir($theme_dir)) {
+			$json_filename = $theme_dir . '/metadata.json';
+			if (file_exists($json_filename)) {
+				$content = file_get_contents($json_filename);
+				$res = json_decode($content, true);
+				if ($res &&
+						!empty($res['name']) &&
+						isset($res['files']) &&
+						is_array($res['files'])) {
+					$res['id'] = $theme_id;
+					return $res;
+				}
+			}
+		}
+		return false;
+	}
+
+	private static $themeIconsUrl;
+	private static $themeIcons;
+
+	public static function load($theme_id) {
+		$infos = self::get_infos($theme_id);
+		if (!$infos) {
+			if ($theme_id !== self::$defaultTheme) {	//Fall-back to default theme
+				return self::load(self::$defaultTheme);
+			}
+			$themes_list = self::getList();
+			if (!empty($themes_list)) {
+				if ($theme_id !== $themes_list[0]) {	//Fall-back to first theme
+					return self::load($themes_list[0]);
+				}
+			}
+			return false;
+		}
+		self::$themeIconsUrl = self::$themesUrl . $theme_id . '/icons/';
+		self::$themeIcons = is_dir(PUBLIC_PATH . self::$themeIconsUrl) ? array_fill_keys(array_diff(
+			scandir(PUBLIC_PATH . self::$themeIconsUrl),
+			array('..', '.')
+		), 1) : array();
+		return $infos;
+	}
+
+	public static function icon($name, $urlOnly = false) {
+		static $alts = array(
+			'add' => '✚',
+			'all' => '☰',
+			'bookmark' => '★',
+			'bookmark-add' => '✚',
+			'category' => '☷',
+			'category-white' => '☷',
+			'close' => '❌',
+			'configure' => '⚙',
+			'down' => '▽',
+			'favorite' => '★',
+			'help' => 'ⓘ',
+			'icon' => '⊚',
+			'key' => '⚿',
+			'link' => '↗',
+			'login' => '🔒',
+			'logout' => '🔓',
+			'next' => '⏩',
+			'non-starred' => '☆',
+			'prev' => '⏪',
+			'read' => '☑',
+			'rss' => '☄',
+			'unread' => '☐',
+			'refresh' => '🔃',	//↻
+			'search' => '🔍',
+			'share' => '♺',
+			'starred' => '★',
+			'stats' => '%',
+			'tag' => '⚐',
+			'up' => '△',
+			'view-normal' => '☰',
+			'view-global' => '☷',
+			'view-reader' => '☕',
+		);
+		if (!isset($alts[$name])) {
+			return '';
+		}
+
+		$url = $name . '.svg';
+		$url = isset(self::$themeIcons[$url]) ? (self::$themeIconsUrl . $url) :
+			(self::$defaultIconsUrl . $url);
+
+		return $urlOnly ? Minz_Url::display($url) :
+			'<img class="icon" src="' . Minz_Url::display($url) . '" alt="' . $alts[$name] . '" />';
+	}
+}
+
+function _i($icon, $url_only = false) {
+	return FreshRSS_Themes::icon($icon, $url_only);
+}

+ 56 - 0
app/Models/UserDAO.php

@@ -0,0 +1,56 @@
+<?php
+
+class FreshRSS_UserDAO extends Minz_ModelPdo {
+	public function createUser($username) {
+		$db = Minz_Configuration::dataBase();
+		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+
+		$userPDO = new Minz_ModelPdo($username);
+
+		$ok = false;
+		if (defined('SQL_CREATE_TABLES')) {	//E.g. MySQL
+			$sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_', Minz_Translate::t('default_category'));
+			$stm = $userPDO->bd->prepare($sql);
+			$ok = $stm && $stm->execute();
+		} else {	//E.g. SQLite
+			global $SQL_CREATE_TABLES;
+			if (is_array($SQL_CREATE_TABLES)) {
+				$ok = true;
+				foreach ($SQL_CREATE_TABLES as $instruction) {
+					$sql = sprintf($instruction, '', Minz_Translate::t('default_category'));
+					$stm = $userPDO->bd->prepare($sql);
+					$ok &= ($stm && $stm->execute());
+				}
+			}
+		}
+
+		if ($ok) {
+			return true;
+		} else {
+			$info = empty($stm) ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function deleteUser($username) {
+		$db = Minz_Configuration::dataBase();
+		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+
+		if ($db['type'] === 'sqlite') {
+			return unlink(DATA_PATH . '/' . $username . '.sqlite');
+		} else {
+			$userPDO = new Minz_ModelPdo($username);
+
+			$sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_');
+			$stm = $userPDO->bd->prepare($sql);
+			if ($stm && $stm->execute()) {
+				return true;
+			} else {
+				$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+				return false;
+			}
+		}
+	}
+}

+ 61 - 0
app/SQL/install.sql.mysql.php

@@ -0,0 +1,61 @@
+<?php
+define('SQL_CREATE_TABLES', '
+CREATE TABLE IF NOT EXISTS `%1$scategory` (
+	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
+	`name` varchar(255) NOT NULL,
+	PRIMARY KEY (`id`),
+	UNIQUE KEY (`name`)	-- v0.7
+) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci
+ENGINE = INNODB;
+
+CREATE TABLE IF NOT EXISTS `%1$sfeed` (
+	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
+	`url` varchar(511) CHARACTER SET latin1 NOT NULL,
+	`category` SMALLINT DEFAULT 0,	-- v0.7
+	`name` varchar(255) NOT NULL,
+	`website` varchar(255) CHARACTER SET latin1,
+	`description` text,
+	`lastUpdate` int(11) DEFAULT 0,
+	`priority` tinyint(2) NOT NULL DEFAULT 10,
+	`pathEntries` varchar(511) DEFAULT NULL,
+	`httpAuth` varchar(511) DEFAULT NULL,
+	`error` boolean DEFAULT 0,
+	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,	-- v0.7
+	`ttl` INT NOT NULL DEFAULT -2,	-- v0.7.3
+	`cache_nbEntries` int DEFAULT 0,	-- v0.7
+	`cache_nbUnreads` int DEFAULT 0,	-- v0.7
+	PRIMARY KEY (`id`),
+	FOREIGN KEY (`category`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
+	UNIQUE KEY (`url`),	-- v0.7
+	INDEX (`name`),	-- v0.7
+	INDEX (`priority`),	-- v0.7
+	INDEX (`keep_history`)	-- v0.7
+) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci
+ENGINE = INNODB;
+
+CREATE TABLE IF NOT EXISTS `%1$sentry` (
+	`id` bigint NOT NULL,	-- v0.7
+	`guid` varchar(760) CHARACTER SET latin1 NOT NULL,	-- Maximum for UNIQUE is 767B
+	`title` varchar(255) NOT NULL,
+	`author` varchar(255),
+	`content_bin` blob,	-- v0.7
+	`link` varchar(1023) CHARACTER SET latin1 NOT NULL,
+	`date` int(11),
+	`is_read` boolean NOT NULL DEFAULT 0,
+	`is_favorite` boolean NOT NULL DEFAULT 0,
+	`id_feed` SMALLINT,	-- v0.7
+	`tags` varchar(1023),
+	PRIMARY KEY (`id`),
+	FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+	UNIQUE KEY (`id_feed`,`guid`),	-- v0.7
+	INDEX (`is_favorite`),	-- v0.7
+	INDEX (`is_read`)	-- v0.7
+) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci
+ENGINE = INNODB;
+
+INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
+');
+
+define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
+
+define('SQL_SHOW_TABLES', 'SHOW tables;');

+ 59 - 0
app/SQL/install.sql.sqlite.php

@@ -0,0 +1,59 @@
+<?php
+global $SQL_CREATE_TABLES;
+$SQL_CREATE_TABLES = array(
+'CREATE TABLE IF NOT EXISTS `%1$scategory` (
+	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+	`name` varchar(255) NOT NULL,
+	UNIQUE (`name`)
+);',
+
+'CREATE TABLE IF NOT EXISTS `%1$sfeed` (
+	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+	`url` varchar(511) NOT NULL,
+	`%1$scategory` SMALLINT DEFAULT 0,
+	`name` varchar(255) NOT NULL,
+	`website` varchar(255),
+	`description` text,
+	`lastUpdate` int(11) DEFAULT 0,
+	`priority` tinyint(2) NOT NULL DEFAULT 10,
+	`pathEntries` varchar(511) DEFAULT NULL,
+	`httpAuth` varchar(511) DEFAULT NULL,
+	`error` boolean DEFAULT 0,
+	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,
+	`ttl` INT NOT NULL DEFAULT -2,
+	`cache_nbEntries` int DEFAULT 0,
+	`cache_nbUnreads` int DEFAULT 0,
+	FOREIGN KEY (`%1$scategory`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
+	UNIQUE (`url`)
+);',
+
+'CREATE INDEX IF NOT EXISTS feed_name_index ON `%1$sfeed`(`name`);',
+'CREATE INDEX IF NOT EXISTS feed_priority_index ON `%1$sfeed`(`priority`);',
+'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `%1$sfeed`(`keep_history`);',
+
+'CREATE TABLE IF NOT EXISTS `%1$sentry` (
+	`id` bigint NOT NULL,
+	`guid` varchar(760) NOT NULL,
+	`title` varchar(255) NOT NULL,
+	`author` varchar(255),
+	`content` text,
+	`link` varchar(1023) NOT NULL,
+	`date` int(11),
+	`is_read` boolean NOT NULL DEFAULT 0,
+	`is_favorite` boolean NOT NULL DEFAULT 0,
+	`id_feed` SMALLINT,
+	`tags` varchar(1023),
+	PRIMARY KEY (`id`),
+	FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+	UNIQUE (`id_feed`,`guid`)
+);',
+
+'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `%1$sentry`(`is_favorite`);',
+'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `%1$sentry`(`is_read`);',
+
+'INSERT OR IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");',
+);
+
+define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
+
+define('SQL_SHOW_TABLES', 'SELECT name FROM sqlite_master WHERE type="table"');

+ 54 - 0
app/actualize_script.php

@@ -0,0 +1,54 @@
+<?php
+require(dirname(__FILE__) . '/../constants.php');
+require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
+
+session_cache_limiter('');
+ob_implicit_flush(false);
+ob_start();
+echo 'Results: ', "\n";	//Buffered
+
+Minz_Configuration::init();
+
+$users = listUsers();
+shuffle($users);	//Process users in random order
+array_unshift($users, Minz_Configuration::defaultUser());	//But always start with admin
+$users = array_unique($users);
+
+foreach ($users as $myUser) {
+	syslog(LOG_INFO, 'FreshRSS actualize ' . $myUser);
+	if (defined('STDOUT')) {
+		fwrite(STDOUT, 'Actualize ' . $myUser . "...\n");	//Unbuffered
+	}
+	echo $myUser, ' ';	//Buffered
+
+	$_GET['c'] = 'feed';
+	$_GET['a'] = 'actualize';
+	$_GET['ajax'] = 1;
+	$_GET['force'] = true;
+	$_SERVER['HTTP_HOST'] = '';
+
+	$freshRSS = new FreshRSS();
+
+	Minz_Configuration::_authType('none');
+
+	Minz_Session::init('FreshRSS');
+	Minz_Session::_param('currentUser', $myUser);
+
+	$freshRSS->init();
+	$freshRSS->run();
+
+	if (!invalidateHttpCache()) {
+		syslog(LOG_NOTICE, 'FreshRSS write access problem in ' . LOG_PATH . '/*.log!');
+		if (defined('STDERR')) {
+			fwrite(STDERR, 'Write access problem in ' . LOG_PATH . '/*.log!' . "\n");
+		}
+	}
+	Minz_Session::unset_session(true);
+	Minz_ModelPdo::clean();
+}
+syslog(LOG_INFO, 'FreshRSS actualize done.');
+if (defined('STDOUT')) {
+	fwrite(STDOUT, 'Done.' . "\n");
+}
+echo 'End.', "\n";
+ob_end_flush();

+ 0 - 1
app/configuration/.gitignore

@@ -1 +0,0 @@
-*

+ 0 - 343
app/controllers/configureController.php

@@ -1,343 +0,0 @@
-<?php
-
-class configureController extends ActionController {
-	public function firstAction () {
-		if (login_is_conf ($this->view->conf) && !is_logged ()) {
-			Error::error (
-				403,
-				array ('error' => array (Translate::t ('access_denied')))
-			);
-		}
-
-		$catDAO = new CategoryDAO ();
-		$catDAO->checkDefault ();
-	}
-
-	public function categorizeAction () {
-		$feedDAO = new FeedDAO ();
-		$catDAO = new CategoryDAO ();
-		$catDAO->checkDefault ();
-		$defaultCategory = $catDAO->getDefault ();
-		$defaultId = $defaultCategory->id ();
-
-		if (Request::isPost ()) {
-			$cats = Request::param ('categories', array ());
-			$ids = Request::param ('ids', array ());
-			$newCat = trim (Request::param ('new_category', ''));
-
-			foreach ($cats as $key => $name) {
-				if (strlen ($name) > 0) {
-					$cat = new Category ($name);
-					$values = array (
-						'name' => $cat->name (),
-						'color' => $cat->color ()
-					);
-					$catDAO->updateCategory ($ids[$key], $values);
-				} elseif ($ids[$key] != $defaultId) {
-					$feedDAO->changeCategory ($ids[$key], $defaultId);
-					$catDAO->deleteCategory ($ids[$key]);
-				}
-			}
-
-			if ($newCat != '') {
-				$cat = new Category ($newCat);
-				$values = array (
-					'id' => $cat->id (),
-					'name' => $cat->name (),
-					'color' => $cat->color ()
-				);
-
-				if ($catDAO->searchByName ($newCat) == false) {
-					$catDAO->addCategory ($values);
-				}
-			}
-
-			// notif
-			$notif = array (
-				'type' => 'good',
-				'content' => Translate::t ('categories_updated')
-			);
-			Session::_param ('notification', $notif);
-
-			Request::forward (array ('c' => 'configure', 'a' => 'categorize'), true);
-		}
-
-		$this->view->categories = $catDAO->listCategories (false);
-		$this->view->defaultCategory = $catDAO->getDefault ();
-
-		View::prependTitle (Translate::t ('categories_management') . ' - ');
-	}
-
-	public function feedAction () {
-		$catDAO = new CategoryDAO ();
-		$this->view->categories = $catDAO->listCategories (false);
-
-		$feedDAO = new FeedDAO ();
-		$this->view->feeds = $feedDAO->listFeeds ();
-
-		$id = Request::param ('id');
-		if ($id == false && !empty ($this->view->feeds)) {
-			$id = current ($this->view->feeds)->id ();
-		}
-
-		$this->view->flux = false;
-		if ($id != false) {
-			$this->view->flux = $feedDAO->searchById ($id);
-
-			if (!$this->view->flux) {
-				Error::error (
-					404,
-					array ('error' => array (Translate::t ('page_not_found')))
-				);
-			} else {
-				$catDAO = new CategoryDAO ();
-				$this->view->categories = $catDAO->listCategories (false);
-
-				if (Request::isPost () && $this->view->flux) {
-					$name = Request::param ('name', '');
-					$hist = Request::param ('keep_history', 'no');
-					$cat = Request::param ('category', 0);
-					$path = Request::param ('path_entries', '');
-					$priority = Request::param ('priority', 0);
-					$user = Request::param ('http_user', '');
-					$pass = Request::param ('http_pass', '');
-
-					$keep_history = false;
-					if ($hist == 'yes') {
-						$keep_history = true;
-					}
-
-					$httpAuth = '';
-					if ($user != '' || $pass != '') {
-						$httpAuth = $user . ':' . $pass;
-					}
-
-					$values = array (
-						'name' => $name,
-						'category' => $cat,
-						'pathEntries' => $path,
-						'priority' => $priority,
-						'httpAuth' => $httpAuth,
-						'keep_history' => $keep_history
-					);
-
-					if ($feedDAO->updateFeed ($id, $values)) {
-						$this->view->flux->_category ($cat);
-
-						$notif = array (
-							'type' => 'good',
-							'content' => Translate::t ('feed_updated')
-						);
-					} else {
-						$notif = array (
-							'type' => 'bad',
-							'content' => Translate::t ('error_occurred_update')
-						);
-					}
-
-					Session::_param ('notification', $notif);
-					Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => array ('id' => $id)), true);
-				}
-
-				View::prependTitle (Translate::t ('rss_feed_management') . ' - ' . $this->view->flux->name () . ' - ');
-			}
-		} else {
-			View::prependTitle (Translate::t ('rss_feed_management') . ' - ');
-		}
-	}
-
-	public function displayAction () {
-		if (Request::isPost ()) {
-			$current_token = $this->view->conf->token ();
-
-			$language = Request::param ('language', 'en');
-			$nb = Request::param ('posts_per_page', 10);
-			$mode = Request::param ('view_mode', 'normal');
-			$view = Request::param ('default_view', 'all');
-			$auto_load_more = Request::param ('auto_load_more', 'no');
-			$display = Request::param ('display_posts', 'no');
-			$onread_jump_next = Request::param ('onread_jump_next', 'yes');
-			$lazyload = Request::param ('lazyload', 'no');
-			$sort = Request::param ('sort_order', 'low_to_high');
-			$old = Request::param ('old_entries', 3);
-			$mail = Request::param ('mail_login', false);
-			$anon = Request::param ('anon_access', 'no');
-			$token = Request::param ('token', $current_token);
-			$openArticle = Request::param ('mark_open_article', 'no');
-			$openSite = Request::param ('mark_open_site', 'no');
-			$scroll = Request::param ('mark_scroll', 'no');
-			$urlShaarli = Request::param ('shaarli', '');
-			$theme = Request::param ('theme', 'default');
-
-			$this->view->conf->_language ($language);
-			$this->view->conf->_postsPerPage (intval ($nb));
-			$this->view->conf->_viewMode ($mode);
-			$this->view->conf->_defaultView ($view);
-			$this->view->conf->_autoLoadMore ($auto_load_more);
-			$this->view->conf->_displayPosts ($display);
-			$this->view->conf->_onread_jump_next ($onread_jump_next);
-			$this->view->conf->_lazyload ($lazyload);
-			$this->view->conf->_sortOrder ($sort);
-			$this->view->conf->_oldEntries ($old);
-			$this->view->conf->_mailLogin ($mail);
-			$this->view->conf->_anonAccess ($anon);
-			$this->view->conf->_token ($token);
-			$this->view->conf->_markWhen (array (
-				'article' => $openArticle,
-				'site' => $openSite,
-				'scroll' => $scroll,
-			));
-			$this->view->conf->_urlShaarli ($urlShaarli);
-			$this->view->conf->_theme ($theme);
-
-			$values = array (
-				'language' => $this->view->conf->language (),
-				'posts_per_page' => $this->view->conf->postsPerPage (),
-				'view_mode' => $this->view->conf->viewMode (),
-				'default_view' => $this->view->conf->defaultView (),
-				'auto_load_more' => $this->view->conf->autoLoadMore (),
-				'display_posts' => $this->view->conf->displayPosts (),
-				'onread_jump_next' => $this->view->conf->onread_jump_next (), 
-				'lazyload' => $this->view->conf->lazyload (),
-				'sort_order' => $this->view->conf->sortOrder (),
-				'old_entries' => $this->view->conf->oldEntries (),
-				'mail_login' => $this->view->conf->mailLogin (),
-				'anon_access' => $this->view->conf->anonAccess (),
-				'token' => $this->view->conf->token (),
-				'mark_when' => $this->view->conf->markWhen (),
-				'url_shaarli' => $this->view->conf->urlShaarli (),
-				'theme' => $this->view->conf->theme ()
-			);
-
-			$confDAO = new RSSConfigurationDAO ();
-			$confDAO->update ($values);
-			Session::_param ('conf', $this->view->conf);
-			Session::_param ('mail', $this->view->conf->mailLogin ());
-
-			Session::_param ('language', $this->view->conf->language ());
-			Translate::reset ();
-
-			// notif
-			$notif = array (
-				'type' => 'good',
-				'content' => Translate::t ('configuration_updated')
-			);
-			Session::_param ('notification', $notif);
-
-			Request::forward (array ('c' => 'configure', 'a' => 'display'), true);
-		}
-
-		$this->view->themes = RSSThemes::get();
-
-		View::prependTitle (Translate::t ('general_and_reading_management') . ' - ');
-	}
-
-	public function importExportAction () {
-		$catDAO = new CategoryDAO ();
-		$this->view->categories = $catDAO->listCategories ();
-
-		$this->view->req = Request::param ('q');
-
-		if ($this->view->req == 'export') {
-			View::_title ('freshrss_feeds.opml');
-
-			$this->view->_useLayout (false);
-			header('Content-Type: text/xml; charset=utf-8');
-			header('Content-disposition: attachment; filename=freshrss_feeds.opml');
-
-			$feedDAO = new FeedDAO ();
-			$catDAO = new CategoryDAO ();
-
-			$list = array ();
-			foreach ($catDAO->listCategories () as $key => $cat) {
-				$list[$key]['name'] = $cat->name ();
-				$list[$key]['feeds'] = $feedDAO->listByCategory ($cat->id ());
-			}
-
-			$this->view->categories = $list;
-		} elseif ($this->view->req == 'import' && Request::isPost ()) {
-			if ($_FILES['file']['error'] == 0) {
-				// on parse le fichier OPML pour récupérer les catégories et les flux associés
-				try {
-					list ($categories, $feeds) = opml_import (
-						file_get_contents ($_FILES['file']['tmp_name'])
-					);
-
-					// On redirige vers le controller feed qui va se charger d'insérer les flux en BDD
-					// les flux sont mis au préalable dans des variables de Request
-					Request::_param ('q', 'null');
-					Request::_param ('categories', $categories);
-					Request::_param ('feeds', $feeds);
-					Request::forward (array ('c' => 'feed', 'a' => 'massiveImport'));
-				} catch (OpmlException $e) {
-					Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
-
-					$notif = array (
-						'type' => 'bad',
-						'content' => Translate::t ('bad_opml_file')
-					);
-					Session::_param ('notification', $notif);
-
-					Request::forward (array (
-						'c' => 'configure',
-						'a' => 'importExport'
-					), true);
-				}
-			}
-		}
-
-		$feedDAO = new FeedDAO ();
-		$this->view->feeds = $feedDAO->listFeeds ();
-
-		// au niveau de la vue, permet de ne pas voir un flux sélectionné dans la liste
-		$this->view->flux = false;
-
-		View::prependTitle (Translate::t ('import_export_opml') . ' - ');
-	}
-
-	public function shortcutAction () {
-		$list_keys = array ('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter',
-		                    'escape', 'f', 'g', 'h', 'i', 'insert', 'j', 'k', 'l', 'left',
-		                    'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right',
-		                    's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y',
-		                    'z', '0', '1', '2', '3', '4', '5', '6', '7', '8',
-		                    '9', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9',
-		                    'f10', 'f11', 'f12');
-		$this->view->list_keys = $list_keys;
-		$list_names = array ('mark_read', 'mark_favorite', 'go_website', 'next_entry',
-		                     'prev_entry', 'next_page', 'prev_page');
-
-		if (Request::isPost ()) {
-			$shortcuts = Request::param ('shortcuts');
-			$shortcuts_ok = array ();
-
-			foreach ($shortcuts as $key => $value) {
-				if (in_array ($key, $list_names)
-				 && in_array ($value, $list_keys)) {
-					$shortcuts_ok[$key] = $value;
-				}
-			}
-
-			$this->view->conf->_shortcuts ($shortcuts_ok);
-
-			$values = array (
-				'shortcuts' => $this->view->conf->shortcuts ()
-			);
-
-			$confDAO = new RSSConfigurationDAO ();
-			$confDAO->update ($values);
-			Session::_param ('conf', $this->view->conf);
-
-			// notif
-			$notif = array (
-				'type' => 'good',
-				'content' => Translate::t ('shortcuts_updated')
-			);
-			Session::_param ('notification', $notif);
-
-			Request::forward (array ('c' => 'configure', 'a' => 'shortcut'), true);
-		}
-
-		View::prependTitle (Translate::t ('shortcuts_management') . ' - ');
-	}
-}

+ 0 - 115
app/controllers/entryController.php

@@ -1,115 +0,0 @@
-<?php
-
-class entryController extends ActionController {
-	public function firstAction () {
-		if (login_is_conf ($this->view->conf) && !is_logged ()) {
-			Error::error (
-				403,
-				array ('error' => array (Translate::t ('access_denied')))
-			);
-		}
-
-		$this->params = array ();
-		$this->redirect = false;
-		$ajax = Request::param ('ajax');
-		if ($ajax) {
-			$this->view->_useLayout (false);
-		}
-	}
-	public function lastAction () {
-		$ajax = Request::param ('ajax');
-		if (!$ajax && $this->redirect) {
-			Request::forward (array (
-				'c' => 'index',
-				'a' => 'index',
-				'params' => $this->params
-			), true);
-		} else {
-			Request::_param ('ajax');
-		}
-	}
-
-	public function readAction () {
-		$this->redirect = true;
-
-		$id = Request::param ('id');
-		$is_read = Request::param ('is_read');
-		$get = Request::param ('get');
-		$nextGet = Request::param ('nextGet', $get); 
-		$dateMax = Request::param ('dateMax', 0);
-
-		$is_read = !!$is_read;
-
-		$entryDAO = new EntryDAO ();
-		if ($id == false) {
-			if (!$get) {
-				$entryDAO->markReadEntries ($is_read, $dateMax);
-			} else {
-				$typeGet = $get[0];
-				$get = substr ($get, 2);
-
-				if ($typeGet == 'c') {
-					$entryDAO->markReadCat ($get, $is_read, $dateMax);
-					$this->params = array ('get' => $nextGet); 
-				} elseif ($typeGet == 'f') {
-					$entryDAO->markReadFeed ($get, $is_read, $dateMax);
-					$this->params = array ('get' => $nextGet);
-				}
-			}
-
-			// notif
-			$notif = array (
-				'type' => 'good',
-				'content' => Translate::t ('feeds_marked_read')
-			);
-			Session::_param ('notification', $notif);
-		} else {
-			$entryDAO->updateEntry ($id, array ('is_read' => $is_read));
-		}
-	}
-
-	public function bookmarkAction () {
-		$this->redirect = true;
-
-		$id = Request::param ('id');
-		$is_fav = Request::param ('is_favorite');
-
-		if ($is_fav) {
-			$is_fav = true;
-		} else {
-			$is_fav = false;
-		}
-
-		$entryDAO = new EntryDAO ();
-		if ($id != false) {
-			$entry = $entryDAO->searchById ($id);
-
-			if ($entry != false) {
-				$values = array (
-					'is_favorite' => $is_fav,
-				);
-
-				$entryDAO->updateEntry ($entry->id (), $values);
-			}
-		}
-	}
-
-	public function optimizeAction() {
-		// La table des entrées a tendance à grossir énormément
-		// Cette action permet d'optimiser cette table permettant de grapiller un peu de place
-		// Cette fonctionnalité n'est à appeler qu'occasionnellement
-		$entryDAO = new EntryDAO();
-		$entryDAO->optimizeTable();
-
-		$notif = array (
-			'type' => 'good',
-			'content' => Translate::t ('optimization_complete')
-		);
-		Session::_param ('notification', $notif);
-
-		Request::forward(array(
-			'c' => 'configure',
-			'a' => 'display'
-		), true);
-	}
-}

+ 0 - 26
app/controllers/errorController.php

@@ -1,26 +0,0 @@
-<?php
-
-class ErrorController extends ActionController {
-	public function indexAction () {
-		switch (Request::param ('code')) {
-		case 403:
-			$this->view->code = 'Error 403 - Forbidden';
-			break;
-		case 404:
-			$this->view->code = 'Error 404 - Not found';
-			break;
-		case 500:
-			$this->view->code = 'Error 500 - Internal Server Error';
-			break;
-		case 503:
-			$this->view->code = 'Error 503 - Service Unavailable';
-			break;
-		default:
-			$this->view->code = 'Error 404 - Not found';
-		}
-		
-		$this->view->logs = Request::param ('logs');
-		
-		View::prependTitle ($this->view->code . ' - ');
-	}
-}

+ 0 - 351
app/controllers/feedController.php

@@ -1,351 +0,0 @@
-<?php
-
-class feedController extends ActionController {
-	public function firstAction () {
-		if (login_is_conf ($this->view->conf) && !is_logged ()) {
-			Error::error (
-				403,
-				array ('error' => array (Translate::t ('access_denied')))
-			);
-		}
-
-		$this->catDAO = new CategoryDAO ();
-		$this->catDAO->checkDefault ();
-	}
-
-	public function addAction () {
-		if (Request::isPost ()) {
-			$url = Request::param ('url_rss');
-			$cat = Request::param ('category', false);
-			if ($cat === false) {
-				$def_cat = $this->catDAO->getDefault ();
-				$cat = $def_cat->id ();
-			}
-
-			$user = Request::param ('username');
-			$pass = Request::param ('password');
-			$params = array ();
-
-			try {
-				$feed = new Feed ($url);
-				$feed->_category ($cat);
-
-				$httpAuth = '';
-				if ($user != '' || $pass != '') {
-					$httpAuth = $user . ':' . $pass;
-				}
-				$feed->_httpAuth ($httpAuth);
-
-				$feed->load ();
-
-				$feedDAO = new FeedDAO ();
-				$values = array (
-					'id' => $feed->id (),
-					'url' => $feed->url (),
-					'category' => $feed->category (),
-					'name' => $feed->name (),
-					'website' => $feed->website (),
-					'description' => $feed->description (),
-					'lastUpdate' => time (),
-					'httpAuth' => $feed->httpAuth (),
-				);
-
-				if ($feedDAO->searchByUrl ($values['url'])) {
-					// on est déjà abonné à ce flux
-					$notif = array (
-						'type' => 'bad',
-						'content' => Translate::t ('already_subscribed', $feed->name ())
-					);
-					Session::_param ('notification', $notif);
-				} elseif (!$feedDAO->addFeed ($values)) {
-					// problème au niveau de la base de données
-					$notif = array (
-						'type' => 'bad',
-						'content' => Translate::t ('feed_not_added', $feed->name ())
-					);
-					Session::_param ('notification', $notif);
-				} else {
-					$entryDAO = new EntryDAO ();
-					$entries = $feed->entries ();
-
-					// on calcule la date des articles les plus anciens qu'on accepte
-					$nb_month_old = $this->view->conf->oldEntries ();
-					$date_min = time () - (60 * 60 * 24 * 30 * $nb_month_old);
-
-					// on ajoute les articles en masse sans vérification
-					foreach ($entries as $entry) {
-						if ($entry->date (true) >= $date_min ||
-						    $feed->keepHistory ()) {
-							$values = $entry->toArray ();
-							$entryDAO->addEntry ($values);
-						}
-					}
-
-					// ok, ajout terminé
-					$notif = array (
-						'type' => 'good',
-						'content' => Translate::t ('feed_added', $feed->name ())
-					);
-					Session::_param ('notification', $notif);
-
-					// permet de rediriger vers la page de conf du flux
-					$params['id'] = $feed->id ();
-				}
-			} catch (BadUrlException $e) {
-				Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
-				$notif = array (
-					'type' => 'bad',
-					'content' => Translate::t ('invalid_url', $url)
-				);
-				Session::_param ('notification', $notif);
-			} catch (FeedException $e) {
-				Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
-				$notif = array (
-					'type' => 'bad',
-					'content' => Translate::t ('internal_problem_feed')
-				);
-				Session::_param ('notification', $notif);
-			} catch (FileNotExistException $e) {
-				// Répertoire de cache n'existe pas
-				Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
-				$notif = array (
-					'type' => 'bad',
-					'content' => Translate::t ('internal_problem_feed')
-				);
-				Session::_param ('notification', $notif);
-			}
-
-			Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true);
-		}
-	}
-
-	public function actualizeAction () {
-		$feedDAO = new FeedDAO ();
-		$entryDAO = new EntryDAO ();
-
-		$id = Request::param ('id');
-		$force = Request::param ('force', false);
-
-		// on créé la liste des flux à mettre à actualiser
-		// si on veut mettre un flux à jour spécifiquement, on le met
-		// dans la liste, mais seul (permet d'automatiser le traitement)
-		$feeds = array ();
-		if ($id) {
-			$feed = $feedDAO->searchById ($id);
-			if ($feed) {
-				$feeds = array ($feed);
-			}
-		} else {
-			$feeds = $feedDAO->listFeedsOrderUpdate ();
-		}
-
-		// on calcule la date des articles les plus anciens qu'on accepte
-		$nb_month_old = $this->view->conf->oldEntries ();
-		$date_min = time () - (60 * 60 * 24 * 30 * $nb_month_old);
-
-		$i = 0;
-		$flux_update = 0;
-		foreach ($feeds as $feed) {
-			try {
-				$feed->load ();
-				$entries = $feed->entries ();
-
-				// ajout des articles en masse sans se soucier des erreurs
-				// On ne vérifie pas que l'article n'est pas déjà en BDD
-				// car demanderait plus de ressources
-				// La BDD refusera l'ajout de son côté car l'id doit être
-				// unique
-				foreach ($entries as $entry) {
-					if ($entry->date (true) >= $date_min ||
-					    $feed->keepHistory ()) {
-						$values = $entry->toArray ();
-						$entryDAO->addEntry ($values);
-					}
-				}
-
-				// on indique que le flux vient d'être mis à jour en BDD
-				$feedDAO->updateLastUpdate ($feed->id ());
-				$flux_update++;
-			} catch (FeedException $e) {
-				Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
-				$feedDAO->isInError ($feed->id ());
-			}
-
-			// On arrête à 10 flux pour ne pas surcharger le serveur
-			// sauf si le paramètre $force est à vrai
-			$i++;
-			if ($i >= 10 && !$force) {
-				break;
-			}
-		}
-
-		$entryDAO->cleanOldEntries ($nb_month_old);
-
-		$url = array ();
-		if ($flux_update == 1) {
-			// on a mis un seul flux à jour
-			$notif = array (
-				'type' => 'good',
-				'content' => Translate::t ('feed_actualized', $feed->name ())
-			);
-		} elseif ($flux_update > 1) {
-			// plusieurs flux on été mis à jour
-			$notif = array (
-				'type' => 'good',
-				'content' => Translate::t ('n_feeds_actualized', $flux_update)
-			);
-		} else {
-			// aucun flux n'a été mis à jour, oups
-			$notif = array (
-				'type' => 'bad',
-				'content' => Translate::t ('no_feed_actualized')
-			);
-		}
-
-		if($i == 1) {
-			// Si on a voulu mettre à jour qu'un flux
-			// on filtre l'affichage par ce flux
-			$feed = reset ($feeds);
-			$url['params'] = array ('get' => 'f_' . $feed->id ());
-		}
-
-		if (Request::param ('ajax', 0) == 0) {
-			Session::_param ('notification', $notif);
-			Request::forward ($url, true);
-		} else {
-			// Une requête Ajax met un seul flux à jour.
-			// Comme en principe plusieurs requêtes ont lieu,
-			// on indique que "plusieurs flux ont été mis à jour".
-			// Cela permet d'avoir une notification plus proche du
-			// ressenti utilisateur
-			$notif = array (
-				'type' => 'good',
-				'content' => Translate::t ('feeds_actualized')
-			);
-			Session::_param ('notification', $notif);
-			// et on désactive le layout car ne sert à rien
-			$this->view->_useLayout (false);
-		}
-	}
-
-	public function massiveImportAction () {
-		$entryDAO = new EntryDAO ();
-		$feedDAO = new FeedDAO ();
-
-		$categories = Request::param ('categories', array (), true);
-		$feeds = Request::param ('feeds', array (), true);
-
-		// on ajoute les catégories en masse dans une fonction à part
-		$this->addCategories ($categories);
-
-		// on calcule la date des articles les plus anciens qu'on accepte
-		$nb_month_old = $this->view->conf->oldEntries ();
-		$date_min = time () - (60 * 60 * 24 * 30 * $nb_month_old);
-
-		// la variable $error permet de savoir si une erreur est survenue
-		// Le but est de ne pas arrêter l'import même en cas d'erreur
-		// L'utilisateur sera mis au courant s'il y a eu des erreurs, mais
-		// ne connaîtra pas les détails. Ceux-ci seront toutefois logguées
-		$error = false;
-		$i = 0;
-		foreach ($feeds as $feed) {
-			try {
-				$feed->load ();
-
-				$values = array (
-					'id' => $feed->id (),
-					'url' => $feed->url (),
-					'category' => $feed->category (),
-					'name' => $feed->name (),
-					'website' => $feed->website (),
-					'description' => $feed->description (),
-					'lastUpdate' => 0,
-					'httpAuth' => $feed->httpAuth ()
-				);
-
-				// ajout du flux que s'il n'est pas déjà en BDD
-				if (!$feedDAO->searchByUrl ($values['url'])) {
-					if (!$feedDAO->addFeed ($values)) {
-						$error = true;
-					}
-				}
-			} catch (FeedException $e) {
-				$error = true;
-				Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
-			}
-		}
-
-		if ($error) {
-			$res = Translate::t ('feeds_imported_with_errors');
-		} else {
-			$res = Translate::t ('feeds_imported');
-		}
-
-		$notif = array (
-			'type' => 'good',
-			'content' => $res
-		);
-		Session::_param ('notification', $notif);
-
-		// et on redirige vers la page import/export
-		Request::forward (array (
-			'c' => 'configure',
-			'a' => 'importExport'
-		), true);
-	}
-
-	public function deleteAction () {
-		$type = Request::param ('type', 'feed');
-		$id = Request::param ('id');
-
-		$feedDAO = new FeedDAO ();
-		if ($type == 'category') {
-			if ($feedDAO->deleteFeedByCategory ($id)) {
-				$notif = array (
-					'type' => 'good',
-					'content' => Translate::t ('category_emptied')
-				);
-			} else {
-				$notif = array (
-					'type' => 'bad',
-					'content' => Translate::t ('error_occured')
-				);
-			}
-		} else {
-			if ($feedDAO->deleteFeed ($id)) {
-				$notif = array (
-					'type' => 'good',
-					'content' => Translate::t ('feed_deleted')
-				);
-			} else {
-				$notif = array (
-					'type' => 'bad',
-					'content' => Translate::t ('error_occured')
-				);
-			}
-		}
-
-		Session::_param ('notification', $notif);
-
-		if ($type == 'category') {
-			Request::forward (array ('c' => 'configure', 'a' => 'categorize'), true);
-		} else {
-			Request::forward (array ('c' => 'configure', 'a' => 'feed'), true);
-		}
-	}
-
-	private function addCategories ($categories) {
-		$catDAO = new CategoryDAO ();
-
-		foreach ($categories as $cat) {
-			if (!$catDAO->searchByName ($cat->name ())) {
-				$values = array (
-					'id' => $cat->id (),
-					'name' => $cat->name (),
-					'color' => $cat->color ()
-				);
-				$catDAO->addCategory ($values);
-			}
-		}
-	}
-}

+ 0 - 228
app/controllers/indexController.php

@@ -1,228 +0,0 @@
-<?php
-
-class indexController extends ActionController {
-	private $get = false;
-	private $nb_not_read = 0;
-	private $mode = 'all';	//TODO: Is this used?
-
-	public function indexAction () {
-		$output = Request::param ('output');
-
-		if ($output == 'rss') {
-			$this->view->_useLayout (false);
-		} else {
-			View::appendScript (Url::display (array ('c' => 'javascript', 'a' => 'actualize')));
-
-			if(!$output) {
-				$output = $this->view->conf->viewMode();
-				Request::_param ('output', $output);
-			}
-
-			View::appendScript (Url::display ('/scripts/shortcut.js'));
-			View::appendScript (Url::display (array ('c' => 'javascript', 'a' => 'main')));
-			View::appendScript (Url::display ('/scripts/endless_mode.js'));
-
-			if ($output == 'global') {
-				View::appendScript (Url::display ('/scripts/global_view.js'));
-			}
-		}
-
-		$nb_not_read = $this->view->nb_not_read;
-		if($nb_not_read > 0) {
-			View::appendTitle (' (' . $nb_not_read . ')');
-		}
-		View::prependTitle (' - ');
-
-		$entryDAO = new EntryDAO ();
-		$feedDAO = new FeedDAO ();
-		$catDAO = new CategoryDAO ();
-
-		$this->view->cat_aside = $catDAO->listCategories ();
-		$this->view->nb_favorites = $entryDAO->countFavorites ();
-		$this->view->nb_total = $entryDAO->count ();
-		$this->view->currentName = '';
-
-		$this->view->get_c = '';
-		$this->view->get_f = '';
-
-		$type = $this->getType ();
-		$error = $this->checkAndProcessType ($type);
-		if (!$error) {
-			// On récupère les différents éléments de filtrage
-			$this->view->state = $state = Request::param ('state', $this->view->conf->defaultView ());
-			$filter = Request::param ('search', '');
-			$this->view->order = $order = Request::param ('order', $this->view->conf->sortOrder ());
-			$nb = Request::param ('nb', $this->view->conf->postsPerPage ());
-			$first = Request::param ('next', '');
-
-			try {
-				// EntriesGetter permet de déporter la complexité du filtrage
-				$getter = new EntriesGetter ($type, $state, $filter, $order, $nb, $first);
-				$getter->execute ();
-				$entries = $getter->getPaginator ();
-
-				// Si on a récupéré aucun article "non lus"
-				// on essaye de récupérer tous les articles
-				if ($state == 'not_read' && $entries->isEmpty ()) {
-					$this->view->state = 'all';
-					$getter->_state ('all');
-					$getter->execute ();
-					$entries = $getter->getPaginator ();
-				}
-
-				$this->view->entryPaginator = $entries;
-			} catch(EntriesGetterException $e) {
-				Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
-				Error::error (
-					404,
-					array ('error' => array (Translate::t ('page_not_found')))
-				);
-			}
-		} else {
-			Error::error (
-				404,
-				array ('error' => array (Translate::t ('page_not_found')))
-			);
-		}
-	}
-
-	/*
-	 * Détermine le type d'article à récupérer :
-	 * "tous", "favoris", "public", "catégorie" ou "flux"
-	 */
-	private function getType () {
-		$get = Request::param ('get', 'all');
-		$typeGet = $get[0];
-		$id = substr ($get, 2);
-
-		$type = null;
-		if ($get == 'all' || $get == 'favoris' || $get == 'public') {
-			$type = array (
-				'type' => $get,
-				'id' => $get
-			);
-		} elseif ($typeGet == 'f' || $typeGet == 'c') {
-			$type = array (
-				'type' => $typeGet,
-				'id' => $id
-			);
-		}
-
-		return $type;
-	}
-	/*
-	 * Vérifie que la catégorie / flux sélectionné existe
-	 * + Initialise correctement les variables de vue get_c et get_f
-	 * + Initialise le titre
-	 */
-	private function checkAndProcessType ($type) {
-		if ($type['type'] == 'all') {
-			$this->view->currentName = Translate::t ('your_rss_feeds');
-			View::prependTitle ($this->view->currentName);
-			$this->view->get_c = $type['type'];
-			return false;
-		} elseif ($type['type'] == 'favoris') {
-			$this->view->currentName = Translate::t ('your_favorites');
-			View::prependTitle ($this->view->currentName);
-			$this->view->get_c = $type['type'];
-			return false;
-		} elseif ($type['type'] == 'public') {
-			$this->view->currentName = Translate::t ('public');
-			View::prependTitle ($this->view->currentName);
-			$this->view->get_c = $type['type'];
-			return false;
-		} elseif ($type['type'] == 'c') {
-			$catDAO = new CategoryDAO ();
-			$cat = $catDAO->searchById ($type['id']);
-			if ($cat) {
-				$this->view->currentName = $cat->name ();
-				$nbnr = $cat->nbNotRead ();
-				View::prependTitle ($this->view->currentName . ($nbnr > 0 ? ' (' . $nbnr . ')' : ''));
-				$this->view->get_c = $type['id'];
-				return false;
-			} else {
-				return true;
-			}
-		} elseif ($type['type'] == 'f') {
-			$feedDAO = new FeedDAO ();
-			$feed = $feedDAO->searchById ($type['id']);
-			if ($feed) {
-				$this->view->currentName = $feed->name ();
-				$nbnr = $feed->nbNotRead ();
-				View::prependTitle ($this->view->currentName . ($nbnr > 0 ? ' (' . $nbnr . ')' : ''));
-				$this->view->get_f = $type['id'];
-				$this->view->get_c = $feed->category ();
-				return false;
-			} else {
-				return true;
-			}
-		} else {
-			return true;
-		}
-	}
-
-	public function aboutAction () {
-		View::prependTitle (Translate::t ('about') . ' - ');
-	}
-
-	public function logsAction () {
-		if (login_is_conf ($this->view->conf) && !is_logged ()) {
-			Error::error (
-				403,
-				array ('error' => array (Translate::t ('access_denied')))
-			);
-		}
-
-		View::prependTitle (Translate::t ('logs') . ' - ');
-
-		$logs = array();
-		try {
-			$logDAO = new LogDAO ();
-			$logs = $logDAO->lister ();
-			$logs = array_reverse ($logs);
-		} catch(FileNotExistException $e) {
-
-		}
-
-		//gestion pagination
-		$page = Request::param ('page', 1);
-		$this->view->logsPaginator = new Paginator ($logs);
-		$this->view->logsPaginator->_nbItemsPerPage (50);
-		$this->view->logsPaginator->_currentPage ($page);
-	}
-
-	public function loginAction () {
-		$this->view->_useLayout (false);
-
-		$url = 'https://verifier.login.persona.org/verify';
-		$assert = Request::param ('assertion');
-		$params = 'assertion=' . $assert . '&audience=' .
-			  urlencode (Url::display () . ':80');
-		$ch = curl_init ();
-		$options = array (
-			CURLOPT_URL => $url,
-			CURLOPT_RETURNTRANSFER => TRUE,
-			CURLOPT_POST => 2,
-			CURLOPT_POSTFIELDS => $params
-		);
-		curl_setopt_array ($ch, $options);
-		$result = curl_exec ($ch);
-		curl_close ($ch);
-
-		$res = json_decode ($result, true);
-		if ($res['status'] == 'okay' && $res['email'] == $this->view->conf->mailLogin ()) {
-			Session::_param ('mail', $res['email']);
-		} else {
-			$res = array ();
-			$res['status'] = 'failure';
-			$res['reason'] = Translate::t ('invalid_login');
-		}
-
-		$this->view->res = json_encode ($res);
-	}
-
-	public function logoutAction () {
-		$this->view->_useLayout (false);
-		Session::_param ('mail');
-	}
-}

+ 0 - 15
app/controllers/javascriptController.php

@@ -1,15 +0,0 @@
-<?php
-
-class javascriptController extends ActionController {
-	public function firstAction () {
-		$this->view->_useLayout (false);
-		header('Content-type: text/javascript');
-	}
-
-	public function mainAction () {}
-
-	public function actualizeAction () {
-		$feedDAO = new FeedDAO ();
-		$this->view->feeds = $feedDAO->listFeeds ();
-	}
-}

+ 262 - 111
app/i18n/en.php

@@ -3,24 +3,76 @@
 return array (
 	// LAYOUT
 	'login'				=> 'Login',
+	'keep_logged_in'		=> 'Keep me logged in <small>(1 month)</small>',
+	'login_with_persona'		=> 'Login with Persona',
+	'login_persona_problem'		=> 'Connection problem with Persona?',
 	'logout'			=> 'Logout',
 	'search'			=> 'Search words or #tags',
+	'search_short'			=> 'Search',
 
 	'configuration'			=> 'Configuration',
-	'general_and_reading'		=> 'General and reading',
+	'users'				=> 'Users',
 	'categories'			=> 'Categories',
 	'category'			=> 'Category',
+	'feed'				=> 'Feed',
+	'feeds'				=> 'Feeds',
 	'shortcuts'			=> 'Shortcuts',
+	'queries'			=> 'User queries',
+	'query_search'			=> 'Search for "%s"',
+	'query_order_asc'		=> 'Display oldest articles first',
+	'query_order_desc'		=> 'Display newest articles first',
+	'query_get_category'		=> 'Display "%s" category',
+	'query_get_feed'		=> 'Display "%s" feed',
+	'query_get_all'			=> 'Display all articles',
+	'query_get_favorite'		=> 'Display favorite articles',
+	'query_state_0'			=> 'Display all articles',
+	'query_state_1'			=> 'Display read articles',
+	'query_state_2'			=> 'Display unread articles',
+	'query_state_3'			=> 'Display all articles',
+	'query_state_4'			=> 'Display favorite articles',
+	'query_state_5'			=> 'Display read favorite articles',
+	'query_state_6'			=> 'Display unread favorite articles',
+	'query_state_7'			=> 'Display favorite articles',
+	'query_state_8'			=> 'Display not favorite articles',
+	'query_state_9'			=> 'Display read not favorite articles',
+	'query_state_10'		=> 'Display unread not favorite articles',
+	'query_state_11'		=> 'Display not favorite articles',
+	'query_state_12'		=> 'Display all articles',
+	'query_state_13'		=> 'Display read articles',
+	'query_state_14'		=> 'Display unread articles',
+	'query_state_15'		=> 'Display all articles',
+	'query_number'			=> 'Query n°%d',
+	'add_query'			=> 'Add a query',
+	'query_created'			=> 'Query "%s" has been created.',
+	'no_query'			=> 'You haven’t created any user query yet.',
+	'query_filter'			=> 'Filter applied:',
+	'no_query_filter'		=> 'No filter',
+	'query_deprecated'		=> 'This query is no longer valid. The referenced category or feed has been deleted.',
 	'about'				=> 'About',
+	'stats'				=> 'Statistics',
+	'stats_idle'			=> 'Idle feeds',
+	'stats_main'			=> 'Main statistics',
+	'stats_repartition'		=> 'Articles repartition',
+	'stats_entry_per_hour'		=> 'Per hour',
+	'stats_entry_per_day_of_week'	=> 'Per day of week',
+	'stats_entry_per_month'		=> 'Per month',
+    
+	'last_week'			=> 'Last week',
+	'last_month'			=> 'Last month',
+	'last_3_month'			=> 'Last three months',
+	'last_6_month'			=> 'Last six months',
+	'last_year'			=> 'Last year',
 
 	'your_rss_feeds'		=> 'Your RSS feeds',
 	'add_rss_feed'			=> 'Add a RSS feed',
 	'no_rss_feed'			=> 'No RSS feed',
-	'import_export_opml'		=> 'Import / export (OPML)',
+	'import_export'			=> 'Import / export',
+	'bookmark'			=> 'Subscribe (FreshRSS bookmark)',
 
 	'subscription_management'	=> 'Subscriptions management',
-	'all_feeds'			=> 'All (%d)',
-	'favorite_feeds'		=> 'Favourites (%d)',
+	'main_stream'			=> 'Main stream',
+	'all_feeds'			=> 'All feeds',
+	'favorite_feeds'		=> 'Favourites (%s)',
 	'not_read'			=> '%d unread',
 	'not_reads'			=> '%d unread',
 
@@ -40,8 +92,13 @@ return array (
 	'normal_view'			=> 'Normal view',
 	'reader_view'			=> 'Reading view',
 	'global_view'			=> 'Global view',
+	'rss_view'			=> 'RSS feed',
 	'show_all_articles'		=> 'Show all articles',
 	'show_not_reads'		=> 'Show only unread',
+	'show_adaptive'			=> 'Adjust showing',
+	'show_read'			=> 'Show only read',
+	'show_favorite'			=> 'Show only favorites',
+	'show_not_favorite'		=> 'Show all but favorites',
 	'older_first'			=> 'Oldest first',
 	'newer_first'			=> 'Newer first',
 
@@ -58,7 +115,7 @@ return array (
 	'access_denied'			=> 'You don’t have permission to access this page',
 	'page_not_found'		=> 'You are looking for a page which doesn’t exist',
 	'error_occurred'		=> 'An error occurred',
-	'error_occurred_update'		=> 'An error occurred during update',
+	'error_occurred_update'		=> 'Nothing was changed',
 
 	'default_category'		=> 'Uncategorized',
 	'categories_updated'		=> 'Categories have been updated',
@@ -66,26 +123,31 @@ return array (
 	'feed_updated'			=> 'Feed has been updated',
 	'rss_feed_management'		=> 'RSS feeds management',
 	'configuration_updated'		=> 'Configuration has been updated',
-	'general_and_reading_management'=> 'General and reading management',
+	'sharing_management'		=> 'Sharing options management',
 	'bad_opml_file'			=> 'Your OPML file is invalid',
 	'shortcuts_updated'		=> 'Shortcuts have been updated',
-	'shortcuts_management'		=> 'Shortcuts management',
+	'shortcuts_navigation'		=> 'Navigation',
+	'shortcuts_navigation_help'	=> 'With the "Shift" modifier, navigation shortcuts apply on feeds.<br/>With the "Alt" modifier, navigation shortcuts apply on categories.',
+	'shortcuts_article_action'	=> 'Article actions',
+	'shortcuts_other_action'	=> 'Other actions',
 	'feeds_marked_read'		=> 'Feeds have been marked as read',
 	'updated'			=> 'Modifications have been updated',
 
 	'already_subscribed'		=> 'You have already subscribed to <em>%s</em>',
 	'feed_added'			=> 'RSS feed <em>%s</em> has been added',
 	'feed_not_added'		=> '<em>%s</em> could not be added',
-	'internal_problem_feed'		=> 'An internal problem occurred, RSS feed could not be added',
+	'internal_problem_feed'		=> 'The RSS feed could not be added. <a href="%s">Check FressRSS logs</a> for details.',
 	'invalid_url'			=> 'URL <em>%s</em> is invalid',
 	'feed_actualized'		=> '<em>%s</em> has been updated',
 	'n_feeds_actualized'		=> '%d feeds have been updated',
 	'feeds_actualized'		=> 'RSS feeds have been updated',
 	'no_feed_actualized'		=> 'No RSS feed has been updated',
-	'feeds_imported_with_errors'	=> 'Feeds have been imported but errors occurred',
-	'feeds_imported'		=> 'Feeds have been imported',
+	'n_entries_deleted'		=> '%d articles have been deleted',
+	'feeds_imported_with_errors'	=> 'Your feeds have been imported but some errors occurred',
+	'feeds_imported'		=> 'Your feeds have been imported and will now be updated',
 	'category_emptied'		=> 'Category has been emptied',
 	'feed_deleted'			=> 'Feed has been deleted',
+	'feed_validator'		=> 'Check the validity of the feed',
 
 	'optimization_complete'		=> 'Optimization complete',
 
@@ -94,10 +156,13 @@ return array (
 	'public'			=> 'Public',
 	'invalid_login'			=> 'Login is invalid',
 
+	'file_is_nok'			=> 'Check permissions on <em>%s</em> directory. HTTP server must have rights to write into.',
+
 	// VIEWS
 	'save'				=> 'Save',
 	'delete'			=> 'Delete',
 	'cancel'			=> 'Cancel',
+	'submit'			=> 'Submit',
 
 	'back_to_rss_feeds'		=> '← Go back to your RSS feeds',
 	'feeds_moved_category_deleted'	=> 'When you delete a category, their feeds are automatically classified under <em>%s</em>.',
@@ -111,28 +176,54 @@ return array (
 	'javascript_for_shortcuts'	=> 'JavaScript must be enabled in order to use shortcuts',
 	'javascript_should_be_activated'=> 'JavaScript must be enabled',
 	'shift_for_all_read'		=> '+ <code>shift</code> to mark all articles as read',
-	'see_on_website'		=> 'See article on its original website',
+	'see_on_website'		=> 'See on original website',
 	'next_article'			=> 'Skip to the next article',
-	'shift_for_last'		=> '+ <code>shift</code> to skip to the last article of page',
+	'last_article'			=> 'Skip to the last article',
 	'previous_article'		=> 'Skip to the previous article',
-	'shift_for_first'		=> '+ <code>shift</code> to skip to the first article of page',
+	'first_article'			=> 'Skip to the first article',
 	'next_page'			=> 'Skip to the next page',
 	'previous_page'			=> 'Skip to the previous page',
-
-	'file_to_import'		=> 'File to import',
+	'collapse_article'		=> 'Collapse',
+	'auto_share'			=> 'Share',
+	'auto_share_help'		=> 'If there is only one sharing mode, it is used. Else modes are accessible by their number.',
+	'focus_search'			=> 'Access search box',
+	'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',
+	'export_no_zip_extension'	=> 'Zip extension is not present on your server. Please try to export files one by one.',
+	'starred_list'			=> 'List of favourite articles',
+	'feed_list'			=> 'List of %s articles',
 	'or'				=> 'or',
 
 	'informations'			=> 'Information',
+	'damn'				=> 'Damn!',
+	'ok'				=> 'Ok!',
+	'attention'			=> 'Be careful!',
 	'feed_in_error'			=> 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.',
+	'feed_empty'			=> 'This feed is empty. Please verify that it is still maintained.',
+	'feed_description'		=> 'Description',
 	'website_url'			=> 'Website URL',
 	'feed_url'			=> 'Feed URL',
-	'number_articles'		=> 'Number of articles',
-	'keep_history'			=> 'Keep history?',
+	'articles'			=> 'articles',
+	'number_articles'		=> '%d articles',
+	'by_feed'			=> 'by feed',
+	'by_default'			=> 'By default',
+	'keep_history'			=> 'Minimum number of articles to keep',
+	'ttl'				=> 'Do not automatically refresh more often than',
 	'categorize'			=> 'Store in a category',
+	'truncate'			=> 'Delete all articles',
 	'advanced'			=> 'Advanced',
-	'show_in_all_flux'		=> 'Show in principal stream',
+	'show_in_all_flux'		=> 'Show in main stream',
 	'yes'				=> 'Yes',
 	'no'				=> 'No',
 	'css_path_on_website'		=> 'Articles CSS path on original website',
@@ -141,40 +232,98 @@ return array (
 	'http_username'			=> 'HTTP username',
 	'http_password'			=> 'HTTP password',
 	'blank_to_disable'		=> 'Leave blank to disable',
+	'share_name'			=> 'Share name to display',
+	'share_url'			=> 'Share URL to use',
 	'not_yet_implemented'		=> 'Not yet implemented',
 	'access_protected_feeds'	=> 'Connection allows to access HTTP protected RSS feeds',
 	'no_selected_feed'		=> 'No feed selected.',
-	'think_to_add'			=> 'Think to add RSS feeds!',
+	'think_to_add'			=> 'You may add some feeds.',
+
+	'current_user'			=> 'Current user',
+	'default_user'			=> 'Username of the default user <small>(maximum 16 alphanumeric characters)</small>',
+	'password_form'			=> 'Password<br /><small>(for the Web-form login method)</small>',
+	'password_api'			=> 'Password API<br /><small>(e.g., for mobile apps)</small>',
+	'persona_connection_email'	=> 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
+	'allow_anonymous'		=> 'Allow anonymous reading of the articles of the default user (%s)',
+	'allow_anonymous_refresh'	=> 'Allow anonymous refresh of the articles',
+	'unsafe_autologin'		=> 'Allow unsafe automatic login using the format: ',
+	'api_enabled'			=> 'Allow <abbr>API</abbr> access <small>(required for mobile apps)</small>',
+	'auth_token'			=> 'Authentication token',
+	'explain_token'			=> 'Allows to access RSS output of the default user without authentication.<br /><kbd>%s?output=rss&amp;token=%s</kbd>',
+	'login_configuration'		=> 'Login',
+	'is_admin'			=> 'is administrator',
+	'auth_type'			=> 'Authentication method',
+	'auth_none'			=> 'None (dangerous)',
+	'auth_form'			=> 'Web form (traditional, requires JavaScript)',
+	'http_auth'			=> 'HTTP (for advanced users with HTTPS)',
+	'auth_persona'			=> 'Mozilla Persona (modern, requires JavaScript)',
+	'users_list'			=> 'List of users',
+	'create_user'			=> 'Create new user',
+	'username'			=> 'Username',
+	'username_admin'		=> 'Administrator username',
+	'password'			=> 'Password',
+	'create'			=> 'Create',
+	'user_created'			=> 'User %s has been created',
+	'user_deleted'			=> 'User %s has been deleted',
 
-	'general_configuration'		=> 'General configuration',
 	'language'			=> 'Language',
-	'delete_articles_every'		=> 'Remove articles every',
 	'month'				=> 'months',
-	'persona_connection_email'	=> 'Login mail address (use <a href="https://persona.org/">Persona</a>)',
-	'allow_anonymous'		=> 'Allow anonymous reading',
-	'auth_token'			=> 'Authentication token',
-	'explain_token'			=> 'Allows to access RSS output without authentication.<br />%s?token=%s',
-	'reading_configuration'		=> 'Reading configuration',
+	'archiving_configuration'	=> 'Archiving',
+	'delete_articles_every'		=> 'Remove articles after',
+	'purge_now'			=> 'Purge now',
+	'purge_completed'		=> 'Purge completed (%d articles deleted)',
+	'archiving_configuration_help'	=> 'More options are available in the individual stream settings',
+	'reading_configuration'		=> 'Reading',
+	'display_configuration'		=> 'Display',
 	'articles_per_page'		=> 'Number of articles per page',
+	'number_divided_when_reader'	=> 'Divided by 2 in the reading view.',
 	'default_view'			=> 'Default view',
+	'articles_to_display'		=> 'Articles to display',
 	'sort_order'			=> 'Sort order',
 	'auto_load_more'		=> 'Load next articles at the page bottom',
 	'display_articles_unfolded'	=> 'Show articles unfolded by default',
-	'after_onread'			=> 'After marked as read,',
-	'jump_next'			=> 'jump to next unread sibling',
+	'display_categories_unfolded'	=> 'Show categories folded by default',
+	'hide_read_feeds'		=> 'Hide categories &amp; feeds with no unread article (does not work with “Show all articles” configuration)',
+	'after_onread'			=> 'After “mark all as read”,',
+	'jump_next'			=> 'jump to next unread sibling (feed or category)',
+	'article_icons'			=> 'Article icons',
+	'top_line'			=> 'Top line',
+	'bottom_line'			=> 'Bottom line',
+	'html5_notif_timeout'		=> 'HTML5 notification timeout',
+	'seconds_(0_means_no_timeout)'	=> 'seconds (0 means no timeout)',
 	'img_with_lazyload'		=> 'Use "lazy load" mode to load pictures',
-	'auto_read_when'		=> 'Mark as read when',
-	'article_selected'		=> 'article is selected',
-	'article_open_on_website'	=> 'article is opened on its original website',
-	'scroll'			=> 'page scrolls',
+	'sticky_post'			=> 'Stick the article to the top when opened',
+	'reading_confirm'		=> 'Display a confirmation dialog on “mark all as read” actions',
+	'auto_read_when'		=> 'Mark article as read…',
+	'article_viewed'		=> 'when article is viewed',
+	'article_open_on_website'	=> 'when article is opened on its original website',
+	'scroll'			=> 'while scrolling',
+	'upon_reception'		=> 'upon reception of the article',
 	'your_shaarli'			=> 'Your Shaarli',
+	'your_wallabag'			=> 'Your wallabag',
+	'your_diaspora_pod'		=> 'Your Diaspora* pod',
 	'sharing'			=> 'Sharing',
 	'share'				=> 'Share',
-	'by_email'			=> 'By mail',
-	'on_shaarli'			=> 'On your Shaarli',
+	'by_email'			=> 'By email',
 	'optimize_bdd'			=> 'Optimize database',
-	'optimize_todo_sometimes'	=> 'To do occasionally to reduce size of database',
+	'optimize_todo_sometimes'	=> 'To do occasionally to reduce the size of the database',
 	'theme'				=> 'Theme',
+	'content_width'			=> 'Content width',
+	'width_thin'			=> 'Thin',
+	'width_medium'			=> 'Medium',
+	'width_large'			=> 'Large',
+	'width_no_limit'		=> 'No limit',
+	'more_information'		=> 'More information',
+	'activate_sharing'		=> 'Activate sharing',
+	'shaarli'			=> 'Shaarli',
+	'blogotext'			=> 'Blogotext',
+	'wallabag'			=> 'wallabag',
+	'diaspora'			=> 'Diaspora*',
+	'twitter'			=> 'Twitter',
+	'g+'				=> 'Google+',
+	'facebook'			=> 'Facebook',
+	'email'				=> 'Email',
+	'print'				=> 'Print',
 
 	'article'			=> 'Article',
 	'title'				=> 'Title',
@@ -183,18 +332,20 @@ return array (
 	'by'				=> 'by',
 
 	'load_more'			=> 'Load more articles',
-	'nothing_to_load'		=> 'There is no more articles',
+	'nothing_to_load'		=> 'There are no more articles',
 
 	'rss_feeds_of'			=> 'RSS feed of %s',
 
 	'refresh'			=> 'Refresh',
+	'no_feed_to_refresh'		=> 'There is no feed to refresh…',
 
 	'today'				=> 'Today',
 	'yesterday'			=> 'Yesterday',
 	'before_yesterday'		=> 'Before yesterday',
+	'new_article'			=> 'There are new available articles, click to refresh the page.',
 	'by_author'			=> 'By <em>%s</em>',
 	'related_tags'			=> 'Related tags',
-	'no_feed_to_display'		=> 'No feed to show.',
+	'no_feed_to_display'		=> 'There is no article to show.',
 
 	'about_freshrss'		=> 'About FreshRSS',
 	'project_website'		=> 'Project website',
@@ -204,31 +355,55 @@ return array (
 	'github_or_email'		=> '<a href="https://github.com/marienfressinaud/FreshRSS/issues">on Github</a> or <a href="mailto:dev@marienfressinaud.fr">by mail</a>',
 	'license'			=> 'License',
 	'agpl3'				=> '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>',
-	'freshrss_description'		=> 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://rsslounge.aditu.de/">RSSLounge</a>, <a href="http://tt-rss.org/redmine/projects/tt-rss/wiki">TinyTinyRSS</a> or <a href="http://projet.idleman.fr/leed/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool. Objective is to provide a serious alternative to Google Reader.',
+	'freshrss_description'		=> 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://projet.idleman.fr/leed/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.',
 	'credits'			=> 'Credits',
-	'credits_content'		=> 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police used has been created by <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons are collected with <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
+	'credits_content'		=> 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons are collected with <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
+	'version'			=> 'Version',
 
 	'logs'				=> 'Logs',
 	'logs_empty'			=> 'Log file is empty',
+	'clear_logs'			=> 'Clear the logs',
 
-	'forbidden_access'		=> 'Forbidden access',
-	'forbidden_access_description'	=> 'Access is password protected, please <a class="signin" href="#">sign in</a> to read your feeds.',
+	'forbidden_access'		=> 'Access is forbidden!',
+	'login_required'		=> 'Login required:',
 
-	'confirm_action'		=> 'Are you sure you want perform this action? It cannot be cancelled!',
+	'confirm_action'		=> 'Are you sure you want to perform this action? It cannot be cancelled!',
+	'confirm_action_feed_cat'	=> 'Are you sure you want to perform this action? You may lost related favorites and user queries. It cannot be cancelled!',
+	'notif_title_new_articles'	=> 'FreshRSS: new articles!',
+	'notif_body_new_articles'	=> 'There are \d new articles to read on FreshRSS.',
 
 	// DATE
-	'january'			=> 'january',
-	'february'			=> 'february',
-	'march'				=> 'march',
-	'april'				=> 'april',
-	'may'				=> 'may',
-	'june'				=> 'june',
-	'july'				=> 'july',
-	'august'			=> 'august',
-	'september'			=> 'september',
-	'october'			=> 'october',
-	'november'			=> 'november',
-	'december'			=> 'december',
+	'january'			=> 'January',
+	'february'			=> 'February',
+	'march'				=> 'March',
+	'april'				=> 'April',
+	'may'				=> 'May',
+	'june'				=> 'June',
+	'july'				=> 'July',
+	'august'			=> 'August',
+	'september'			=> 'September',
+	'october'			=> 'October',
+	'november'			=> 'November',
+	'december'			=> 'December',
+	'january'			=> 'Jan',
+	'february'			=> 'Feb',
+	'march'				=> 'Mar',
+	'april'				=> 'Apr',
+	'may'				=> 'May',
+	'june'				=> 'Jun',
+	'july'				=> 'Jul',
+	'august'			=> 'Aug',
+	'september'			=> 'Sep',
+	'october'			=> 'Oct',
+	'november'			=> 'Nov',
+	'december'			=> 'Dec',
+	'sun'				=> 'Sun',
+	'mon'				=> 'Mon',
+	'tue'				=> 'Tue',
+	'wed'				=> 'Wed',
+	'thu'				=> 'Thu',
+	'fri'				=> 'Fri',
+	'sat'				=> 'Sat',
 	// special format for date() function
 	'Jan'				=> '\J\a\n\u\a\r\y',
 	'Feb'				=> '\F\e\b\r\u\a\r\y',
@@ -243,61 +418,37 @@ return array (
 	'Nov'				=> '\N\o\v\e\m\b\e\r',
 	'Dec'				=> '\D\e\c\e\m\b\e\r',
 	// format for date() function, %s allows to indicate month in letter
-	'format_date'			=> '%s dS Y',
-	'format_date_hour'		=> '%s dS Y \a\t H\.i',
-
-	// INSTALLATION
-	'freshrss_installation'		=> 'Installation - FreshRSS',
-	'freshrss'			=> 'FreshRSS',
-	'installation_step'		=> 'Installation - step %d',
-	'steps'				=> 'Steps',
-	'checks'			=> 'Checks',
-	'bdd_configuration'		=> 'Database configuration',
-	'this_is_the_end'		=> 'This is the end',
-
-	'ok'				=> 'Ok!',
-	'congratulations'		=> 'Congratulations!',
-	'attention'			=> 'Attention!',
-	'damn'				=> 'Damn!',
-	'oops'				=> 'Oops!',
-	'next_step'			=> 'Go to the next step',
-
-	'language_defined'		=> 'Language has been defined.',
-	'choose_language'		=> 'Choose a language for FreshRSS',
-
-	'javascript_is_better'		=> 'FreshRSS is more pleasant with JavaScript enabled',
-	'php_is_ok'			=> 'Your PHP version is %s and it’s compatible with FreshRSS',
-	'php_is_nok'			=> 'Your PHP version is %s. You must have at least version %s',
-	'minz_is_ok'			=> 'You have Minz framework',
-	'minz_is_nok'			=> 'You haven’t Minz framework. You should execute <em>build.sh</em> script or <a href="https://github.com/marienfressinaud/MINZ">download it on Github</a> and install in <em>%s</em> directory the content of its <em>/lib</em> directory.',
-	'curl_is_ok'			=> 'You have version %s of cURL',
-	'curl_is_nok'			=> 'You haven’t cURL',
-	'pdomysql_is_ok'		=> 'You have PDO and its driver for MySQL',
-	'pdomysql_is_nok'		=> 'You haven’t PDO or its driver for MySQL',
-	'dom_is_ok'			=> 'You have the necessary to browse the DOM',
-	'dom_is_nok'			=> 'You haven’t the necessary to browse the DOM (php-xml package can be useful)',
-	'cache_is_ok'			=> 'Permissions on cache directory are good',
-	'log_is_ok'			=> 'Permissions on logs directory are good',
-	'conf_is_ok'			=> 'Permissions on configuration directory are good',
-	'data_is_ok'			=> 'Permissions on data directory are good',
-	'file_is_nok'			=> 'Check permissions on <em>%s</em> directory. HTTP server must have rights to write into',
-	'fix_errors_before'		=> 'Fix errors before skip to the next step.',
-
-	'general_conf_is_ok'		=> 'General configuration has been saved.',
-	'random_string'			=> 'Random string',
-	'change_value'			=> 'You should change this value by any other',
-	'base_url'			=> 'Base URL',
-	'do_not_change_if_doubt'	=> 'Don’t change if you doubt about it',
-
-	'bdd_conf_is_ok'		=> 'Database configuration has been saved.',
-	'bdd_conf_is_ko'		=> 'Verify your database information.',
-	'host'				=> 'Host',
-	'username'			=> 'Username',
-	'password'			=> 'Password',
-	'bdd'				=> 'Database',
-	'prefix'			=> 'Table prefix',
-
-	'installation_is_ok'		=> 'Installation process is finished. You must delete <em>install.php</em> file to access FreshRSS… or simply click on following button :)',
-	'finish_installation'		=> 'Finish installation',
-	'install_not_deleted'		=> 'Something was going wrong, you must delete the file <em>%s</em> manually.',
+	'format_date'			=> '%s j\<\s\u\p\>S\<\/\s\u\p\> Y',
+	'format_date_hour'		=> '%s j\<\s\u\p\>S\<\/\s\u\p\> Y \a\t H\:i',
+	
+	'status_favorites'		=> 'Favourites',
+	'status_read'			=> 'Read',
+	'status_unread'			=> 'Unread',
+	'status_total'			=> 'Total',
+	
+	'stats_entry_repartition'	=> 'Entries repartition',
+	'stats_entry_per_day'		=> 'Entries per day (last 30 days)',
+	'stats_feed_per_category'	=> 'Feeds per category',
+	'stats_entry_per_category'	=> 'Entries per category',
+	'stats_top_feed'		=> 'Top ten feeds',
+	'stats_entry_count'		=> 'Entry count',
+	'stats_no_idle'			=> 'There is no idle feed!',
+
+	'update'			=> 'Update',
+	'update_system'			=> 'Update system',
+	'update_check'			=> 'Check for new updates',
+	'update_last'			=> 'Last verification: %s',
+	'update_can_apply'		=> 'An update is available.',
+	'update_apply'			=> 'Apply',
+	'update_server_not_found'	=> 'Update server cannot be found. [%s]',
+	'no_update'			=> 'No update to apply',
+	'update_problem'		=> 'The update process has encountered an error: %s',
+	'update_finished'		=> 'Update completed!',
+
+	'auth_reset'			=> 'Authentication reset',
+	'auth_will_reset'		=> 'Authentication system will be reset: a form will be used instead of Persona.',
+	'auth_not_persona'		=> 'Only Persona system can be reset.',
+	'auth_no_password_set'		=> 'Administrator password hasn’t been set. This feature isn’t available.',
+	'auth_form_set'			=> 'Form is now your default authentication system.',
+	'auth_form_not_set'		=> 'A problem occured during authentication system configuration. Please retry later.',
 );

+ 277 - 126
app/i18n/fr.php

@@ -3,30 +3,82 @@
 return array (
 	// LAYOUT
 	'login'				=> 'Connexion',
+	'keep_logged_in'		=> 'Rester connecté <small>(1 mois)</small>',
+	'login_with_persona'		=> 'Connexion avec Persona',
+	'login_persona_problem'		=> 'Problème de connexion à Persona ?',
 	'logout'			=> 'Déconnexion',
 	'search'			=> 'Rechercher des mots ou des #tags',
+	'search_short'			=> 'Rechercher',
 
 	'configuration'			=> 'Configuration',
-	'general_and_reading'		=> 'Général et lecture',
+	'users'				=> 'Utilisateurs',
 	'categories'			=> 'Catégories',
 	'category'			=> 'Catégorie',
+	'feed'				=> 'Flux',
+	'feeds'				=> 'Flux',
 	'shortcuts'			=> 'Raccourcis',
+	'queries'			=> 'Filtres utilisateurs',
+	'query_search'			=> 'Recherche de "%s"',
+	'query_order_asc'		=> 'Afficher les articles les plus anciens en premier',
+	'query_order_desc'		=> 'Afficher les articles les plus récents en premier',
+	'query_get_category'		=> 'Afficher la catégorie "%s"',
+	'query_get_feed'		=> 'Afficher le flux "%s"',
+	'query_get_all'			=> 'Afficher tous les articles',
+	'query_get_favorite'		=> 'Afficher les articles favoris',
+	'query_state_0'			=> 'Afficher tous les articles',
+	'query_state_1'			=> 'Afficher les articles lus',
+	'query_state_2'			=> 'Afficher les articles non lus',
+	'query_state_3'			=> 'Afficher tous les articles',
+	'query_state_4'			=> 'Afficher les articles favoris',
+	'query_state_5'			=> 'Afficher les articles lus et favoris',
+	'query_state_6'			=> 'Afficher les articles non lus et favoris',
+	'query_state_7'			=> 'Afficher les articles favoris',
+	'query_state_8'			=> 'Afficher les articles non favoris',
+	'query_state_9'			=> 'Afficher les articles lus et non favoris',
+	'query_state_10'		=> 'Afficher les articles non lus et non favoris',
+	'query_state_11'		=> 'Afficher les articles non favoris',
+	'query_state_12'		=> 'Afficher tous les articles',
+	'query_state_13'		=> 'Afficher les articles lus',
+	'query_state_14'		=> 'Afficher les articles non lus',
+	'query_state_15'		=> 'Afficher tous les articles',
+	'query_number'			=> 'Filtre n°%d',
+	'add_query'			=> 'Créer un filtre',
+	'query_created'			=> 'Le filtre "%s" a bien été créé.',
+	'no_query'			=> 'Vous n’avez pas encore créé de filtre.',
+	'query_filter'			=> 'Filtres appliqués :',
+	'no_query_filter'		=> 'Aucun filtre appliqué',
+	'query_deprecated'		=> 'Ce filtre n’est plus valide. La catégorie ou le flux concerné a été supprimé.',
 	'about'				=> 'À propos',
+	'stats'				=> 'Statistiques',
+	'stats_idle'			=> 'Flux inactifs',
+	'stats_main'			=> 'Statistiques principales',
+	'stats_repartition'		=> 'Répartition des articles',
+	'stats_entry_per_hour'		=> 'Par heure',
+	'stats_entry_per_day_of_week'	=> 'Par jour de la semaine',
+	'stats_entry_per_month'		=> 'Par mois',
+
+	'last_week'			=> 'Depuis la semaine dernière',
+	'last_month'			=> 'Depuis le mois dernier',
+	'last_3_month'			=> 'Depuis les trois derniers mois',
+	'last_6_month'			=> 'Depuis les six derniers mois',
+	'last_year'			=> 'Depuis l’année dernière',
 
 	'your_rss_feeds'		=> 'Vos flux RSS',
 	'add_rss_feed'			=> 'Ajouter un flux RSS',
 	'no_rss_feed'			=> 'Aucun flux RSS',
-	'import_export_opml'		=> 'Importer / exporter (OPML)',
+	'import_export'			=> 'Importer / exporter',
+	'bookmark'			=> 'S’abonner (bookmark FreshRSS)',
 
 	'subscription_management'	=> 'Gestion des abonnements',
-	'all_feeds'			=> 'Tous (%d)',
-	'favorite_feeds'		=> 'Favoris (%d)',
+	'main_stream'			=> 'Flux principal',
+	'all_feeds'			=> 'Tous les flux',
+	'favorite_feeds'		=> 'Favoris (%s)',
 	'not_read'			=> '%d non lu',
 	'not_reads'			=> '%d non lus',
 
 	'filter'			=> 'Filtrer',
 	'see_website'			=> 'Voir le site',
-	'administration'		=> 'Gestion',
+	'administration'		=> 'Gérer',
 	'actualize'			=> 'Actualiser',
 
 	'mark_read'			=> 'Marquer comme lu',
@@ -40,8 +92,13 @@ return array (
 	'normal_view'			=> 'Vue normale',
 	'reader_view'			=> 'Vue lecture',
 	'global_view'			=> 'Vue globale',
+	'rss_view'			=> 'Flux RSS',
 	'show_all_articles'		=> 'Afficher tous les articles',
 	'show_not_reads'		=> 'Afficher les non lus',
+	'show_adaptive'			=> 'Adapter l’affichage',
+	'show_read'			=> 'Afficher les lus',
+	'show_favorite'			=> 'Afficher les favoris',
+	'show_not_favorite'		=> 'Afficher tout sauf les favoris',
 	'older_first'			=> 'Plus anciens en premier',
 	'newer_first'			=> 'Plus récents en premier',
 
@@ -55,126 +112,218 @@ return array (
 	'article_published_on'		=> 'Article publié initialement sur <a href="%s">%s</a>',
 	'article_published_on_author'	=> 'Article publié initialement sur <a href="%s">%s</a> par %s',
 
-	'access_denied'			=> 'Vous n’avez pas le droit d’accéder à cette page',
-	'page_not_found'		=> 'La page que vous cherchez n’existe pas',
-	'error_occurred'		=> 'Une erreur est survenue',
-	'error_occurred_update'		=> 'Une erreur est survenue lors de la mise à jour',
+	'access_denied'			=> 'Vous n’avez pas le droit d’accéder à cette page !',
+	'page_not_found'		=> 'La page que vous cherchez n’existe pas !',
+	'error_occurred'		=> 'Une erreur est survenue !',
+	'error_occurred_update'		=> 'Rien n’a été modifié !',
 
 	'default_category'		=> 'Sans catégorie',
-	'categories_updated'		=> 'Les catégories ont été mises à jour',
+	'categories_updated'		=> 'Les catégories ont été mises à jour.',
 	'categories_management'		=> 'Gestion des catégories',
-	'feed_updated'			=> 'Le flux a été mis à jour',
+	'feed_updated'			=> 'Le flux a été mis à jour.',
 	'rss_feed_management'		=> 'Gestion des flux RSS',
-	'configuration_updated'		=> 'La configuration a été mise à jour',
-	'general_and_reading_management'=> 'Gestion générale et affichage',
-	'bad_opml_file'			=> 'Votre fichier OPML n’est pas valide',
-	'shortcuts_updated'		=> 'Les raccourcis ont été mis à jour',
-	'shortcuts_management'		=> 'Gestion des raccourcis',
-	'feeds_marked_read'		=> 'Les flux ont été marqués comme lu',
-	'updated'			=> 'Modifications enregistrées',
+	'configuration_updated'		=> 'La configuration a été mise à jour.',
+	'sharing_management'		=> 'Gestion des options de partage',
+	'bad_opml_file'			=> 'Votre fichier OPML n’est pas valide.',
+	'shortcuts_updated'		=> 'Les raccourcis ont été mis à jour.',
+	'shortcuts_navigation'		=> 'Navigation',
+	'shortcuts_navigation_help'	=> 'Avec le modificateur "Shift", les raccourcis de navigation s’appliquent aux flux.<br/>Avec le modificateur "Alt", les raccourcis de navigation s’appliquent aux catégories.',
+	'shortcuts_article_action'	=> 'Actions associées à l’article courant',
+	'shortcuts_other_action'	=> 'Autres actions',
+	'feeds_marked_read'		=> 'Les flux ont été marqués comme lus.',
+	'updated'			=> 'Modifications enregistrées.',
 
 	'already_subscribed'		=> 'Vous êtes déjà abonné à <em>%s</em>',
-	'feed_added'			=> 'Le flux <em>%s</em> a bien été ajouté',
-	'feed_not_added'		=> '<em>%s</em> n’ a pas pu être ajouté',
-	'internal_problem_feed'		=> 'Un problème interne a été rencontré, le flux n’a pas pu être ajouté',
-	'invalid_url'			=> 'L’url <em>%s</em> est invalide',
-	'feed_actualized'		=> '<em>%s</em> a été mis à jour',
-	'n_feeds_actualized'		=> '%d flux ont été mis à jour',
-	'feeds_actualized'		=> 'Les flux ont été mis à jour',
-	'no_feed_actualized'		=> 'Aucun flux n’a pu être mis à jour',
-	'feeds_imported_with_errors'	=> 'Les flux ont été importés mais des erreurs sont survenues',
-	'feeds_imported'		=> 'Les flux ont été importés',
-	'category_emptied'		=> 'La catégorie a été vidée',
-	'feed_deleted'			=> 'Le flux a été supprimé',
-
-	'optimization_complete'		=> 'Optimisation terminée',
+	'feed_added'			=> 'Le flux <em>%s</em> a bien été ajouté.',
+	'feed_not_added'		=> '<em>%s</em> n’a pas pu être ajouté.',
+	'internal_problem_feed'		=> 'Le flux ne peut pas être ajouté. <a href="%s">Consulter les logs de FreshRSS</a> pour plus de détails.',
+	'invalid_url'			=> 'L’url <em>%s</em> est invalide.',
+	'feed_actualized'		=> '<em>%s</em> a été mis à jour.',
+	'n_feeds_actualized'		=> '%d flux ont été mis à jour.',
+	'feeds_actualized'		=> 'Les flux ont été mis à jour.',
+	'no_feed_actualized'		=> 'Aucun flux n’a pu être mis à jour.',
+	'n_entries_deleted'		=> '%d articles ont été supprimés.',
+	'feeds_imported_with_errors'	=> 'Vos flux ont été importés mais des erreurs sont survenues.',
+	'feeds_imported'		=> 'Vos flux ont été importés et vont maintenant être actualisés.',
+	'category_emptied'		=> 'La catégorie a été vidée.',
+	'feed_deleted'			=> 'Le flux a été supprimé.',
+	'feed_validator'		=> 'Vérifier la valididé du flux',
+
+	'optimization_complete'		=> 'Optimisation terminée.',
 
 	'your_rss_feeds'		=> 'Vos flux RSS',
 	'your_favorites'		=> 'Vos favoris',
 	'public'			=> 'Public',
-	'invalid_login'			=> 'L’identifiant est invalide',
+	'invalid_login'			=> 'L’identifiant est invalide !',
+
+	'file_is_nok'			=> 'Veuillez vérifier les droits sur le répertoire <em>%s</em>. Le serveur HTTP doit être capable d’écrire dedans.',
 
 	// VIEWS
 	'save'				=> 'Enregistrer',
 	'delete'			=> 'Supprimer',
 	'cancel'			=> 'Annuler',
+	'submit'			=> 'Valider',
 
 	'back_to_rss_feeds'		=> '← Retour à vos flux RSS',
 	'feeds_moved_category_deleted'	=> 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans <em>%s</em>.',
 	'category_number'		=> 'Catégorie n°%d',
 	'ask_empty'			=> 'Vider ?',
 	'number_feeds'			=> '%d flux',
-	'can_not_be_deleted'		=> 'Ne peut pas être supprimée',
+	'can_not_be_deleted'		=> 'Ne peut pas être supprimée.',
 	'add_category'			=> 'Ajouter une catégorie',
 	'new_category'			=> 'Nouvelle catégorie',
 
-	'javascript_for_shortcuts'	=> 'Le javascript doit être activé pour pouvoir profiter des raccourcis',
-	'javascript_should_be_activated'=> 'Le javascript doit être activé',
+	'javascript_for_shortcuts'	=> 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis.',
+	'javascript_should_be_activated'=> 'Le JavaScript doit être activé.',
 	'shift_for_all_read'		=> '+ <code>shift</code> pour marquer tous les articles comme lus',
-	'see_on_website'		=> 'Voir l’article sur le site d’origine',
+	'see_on_website'		=> 'Voir sur le site d’origine',
 	'next_article'			=> 'Passer à l’article suivant',
-	'shift_for_last'		=> '+ <code>shift</code> pour passer au dernier article de la page',
+	'last_article'			=> 'Passer au dernier article',
 	'previous_article'		=> 'Passer à l’article précédent',
-	'shift_for_first'		=> '+ <code>shift</code> pour passer au premier article de la page',
+	'first_article'			=> 'Passer au premier article',
 	'next_page'			=> 'Passer à la page suivante',
 	'previous_page'			=> 'Passer à la page précédente',
-
-	'file_to_import'		=> 'Fichier à importer',
+	'collapse_article'		=> 'Refermer',
+	'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',
+	'export_no_zip_extension'	=> 'L’extension Zip n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.',
+	'starred_list'			=> 'Liste des articles favoris',
+	'feed_list'			=> 'Liste des articles de %s',
 	'or'				=> 'ou',
 
 	'informations'			=> 'Informations',
+	'damn'				=> 'Arf !',
+	'ok'				=> 'Ok !',
+	'attention'			=> 'Attention !',
 	'feed_in_error'			=> 'Ce flux a rencontré un problème. Veuillez vérifier qu’il est toujours accessible puis actualisez-le.',
+	'feed_empty'			=> 'Ce flux est vide. Veuillez vérifier qu’il est toujours maintenu.',
+	'feed_description'		=> 'Description',
 	'website_url'			=> 'URL du site',
 	'feed_url'			=> 'URL du flux',
-	'number_articles'		=> 'Nombre d’articles',
-	'keep_history'			=> 'Garder l’historique ?',
+	'articles'			=> 'articles',
+	'number_articles'		=> '%d articles',
+	'by_feed'			=> 'par flux',
+	'by_default'			=> 'Par défaut',
+	'keep_history'			=> 'Nombre minimum d’articles à conserver',
+	'ttl'				=> 'Ne pas automatiquement rafraîchir plus souvent que',
 	'categorize'			=> 'Ranger dans une catégorie',
+	'truncate'			=> 'Supprimer tous les articles',
 	'advanced'			=> 'Avancé',
 	'show_in_all_flux'		=> 'Afficher dans le flux principal',
 	'yes'				=> 'Oui',
 	'no'				=> 'Non',
-	'css_path_on_website'		=> 'Chemin CSS des articles sur le site d’origine',
+	'css_path_on_website'		=> 'Sélecteur CSS des articles sur le site d’origine',
 	'retrieve_truncated_feeds'	=> 'Permet de récupérer les flux tronqués (attention, demande plus de temps !)',
 	'http_authentication'		=> 'Authentification HTTP',
 	'http_username'			=> 'Identifiant HTTP',
 	'http_password'			=> 'Mot de passe HTTP',
 	'blank_to_disable'		=> 'Laissez vide pour désactiver',
+	'share_name'			=> 'Nom du partage à afficher',
+	'share_url'			=> 'URL du partage à utiliser',
 	'not_yet_implemented'		=> 'Pas encore implémenté',
-	'access_protected_feeds'	=> 'La connexion permet d’accéder aux flux protégés par une authentification HTTP',
+	'access_protected_feeds'	=> 'La connexion permet d’accéder aux flux protégés par une authentification HTTP.',
 	'no_selected_feed'		=> 'Aucun flux sélectionné.',
-	'think_to_add'			=> 'Pensez à en ajouter !',
+	'think_to_add'			=> 'Vous pouvez ajouter des flux.',
+
+	'current_user'			=> 'Utilisateur actuel',
+	'password_form'			=> 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
+	'password_api'			=> 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>',
+	'default_user'			=> 'Nom de l’utilisateur par défaut <small>(16 caractères alphanumériques maximum)</small>',
+	'persona_connection_email'	=> 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
+	'allow_anonymous'		=> 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)',
+	'allow_anonymous_refresh'	=> 'Autoriser le rafraîchissement anonyme des flux',
+	'unsafe_autologin'		=> 'Autoriser les connexions automatiques non-sûres au format : ',
+	'api_enabled'			=> 'Autoriser l’accès par <abbr>API</abbr> <small>(nécessaire pour les applis mobiles)</small>',
+	'auth_token'			=> 'Jeton d’identification',
+	'explain_token'			=> 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.<br /><kbd>%s?output=rss&amp;token=%s</kbd>',
+	'login_configuration'		=> 'Identification',
+	'is_admin'			=> 'est administrateur',
+	'auth_type'			=> 'Méthode d’authentification',
+	'auth_none'			=> 'Aucune (dangereux)',
+	'auth_form'			=> 'Formulaire (traditionnel, requiert JavaScript)',
+	'http_auth'			=> 'HTTP (pour utilisateurs avancés avec HTTPS)',
+	'auth_persona'			=> 'Mozilla Persona (moderne, requiert JavaScript)',
+	'users_list'			=> 'Liste des utilisateurs',
+	'create_user'			=> 'Créer un nouvel utilisateur',
+	'username'			=> 'Nom d’utilisateur',
+	'username_admin'		=> 'Nom d’utilisateur administrateur',
+	'password'			=> 'Mot de passe',
+	'create'			=> 'Créer',
+	'user_created'			=> 'L’utilisateur %s a été créé.',
+	'user_deleted'			=> 'L’utilisateur %s a été supprimé.',
 
-	'general_configuration'		=> 'Configuration générale',
 	'language'			=> 'Langue',
-	'delete_articles_every'		=> 'Supprimer les articles tous les',
 	'month'				=> 'mois',
-	'persona_connection_email'	=> 'Adresse mail de connexion (utilise <a href="https://persona.org/">Persona</a>)',
-	'allow_anonymous'		=> 'Autoriser la lecture anonyme',
-	'auth_token'			=> 'Jeton d’identification',
-	'explain_token'			=> 'Permet d’accéder à la sortie RSS sans besoin de s’authentifier.<br />%s?output=rss&token=%s',
-	'reading_configuration'		=> 'Configuration de lecture',
+	'archiving_configuration'	=> 'Archivage',
+	'delete_articles_every'		=> 'Supprimer les articles après',
+	'purge_now'			=> 'Purger maintenant',
+	'purge_completed'		=> 'Purge effectuée (%d articles supprimés).',
+	'archiving_configuration_help'	=> 'D’autres options sont disponibles dans la configuration individuelle des flux.',
+	'reading_configuration'		=> 'Lecture',
+	'display_configuration'		=> 'Affichage',
 	'articles_per_page'		=> 'Nombre d’articles par page',
+	'number_divided_when_reader'	=> 'Divisé par 2 dans la vue de lecture.',
 	'default_view'			=> 'Vue par défaut',
+	'articles_to_display'		=> 'Articles à afficher',
 	'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',
-	'after_onread'			=> 'Après marqué comme lu,',
-	'jump_next'			=> 'sauter au prochain voisin non lu',
-	'img_with_lazyload'		=> 'Utiliser le mode “lazy load” pour charger les images',
-	'auto_read_when'		=> 'Marquer comme lu lorsque',
-	'article_selected'		=> 'l’article est sélectionné',
-	'article_open_on_website'	=> 'l’article est ouvert sur le site d’origine',
+	'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 (ne fonctionne pas avec la configuration “Afficher tous les articles”)',
+	'after_onread'			=> 'Après “marquer tout comme lu”,',
+	'jump_next'			=> 'sauter au prochain voisin non lu (flux ou catégorie)',
+	'article_icons'			=> 'Icônes d’article',
+	'top_line'			=> 'Ligne du haut',
+	'bottom_line'			=> 'Ligne du bas',
+	'html5_notif_timeout'		=> 'Temps d’affichage de la notification HTML5',
+	'seconds_(0_means_no_timeout)'	=> 'secondes (0 signifie aucun timeout ) ',
+	'img_with_lazyload'		=> 'Utiliser le mode “chargement différé” pour les images',
+	'sticky_post'			=> 'Aligner l’article en haut quand il est ouvert',
+	'reading_confirm'		=> 'Afficher une confirmation lors des actions “marquer tout comme lu”',
+	'auto_read_when'		=> 'Marquer un article comme lu…',
+	'article_viewed'		=> 'lorsque l’article est affiché',
+	'article_open_on_website'	=> 'lorsque l’article est ouvert sur le site d’origine',
 	'scroll'			=> 'au défilement de la page',
+	'upon_reception'		=> 'dès la réception du nouvel article',
 	'your_shaarli'			=> 'Votre Shaarli',
+	'your_wallabag'			=> 'Votre wallabag',
+	'your_diaspora_pod'		=> 'Votre pod Diaspora*',
 	'sharing'			=> 'Partage',
 	'share'				=> 'Partager',
-	'by_email'			=> 'Par mail',
-	'on_shaarli'			=> 'Sur votre Shaarli',
+	'by_email'			=> 'Par courriel',
 	'optimize_bdd'			=> 'Optimiser la base de données',
 	'optimize_todo_sometimes'	=> 'À faire de temps en temps pour réduire la taille de la BDD',
 	'theme'				=> 'Thème',
+	'content_width'			=> 'Largeur du contenu',
+	'width_thin'			=> 'Fine',
+	'width_medium'			=> 'Moyenne',
+	'width_large'			=> 'Large',
+	'width_no_limit'		=> 'Pas de limite',
+	'more_information'		=> 'Plus d’informations',
+	'activate_sharing'		=> 'Activer le partage',
+	'shaarli'			=> 'Shaarli',
+	'blogotext'			=> 'Blogotext',
+	'wallabag'			=> 'wallabag',
+	'diaspora'			=> 'Diaspora*',
+	'twitter'			=> 'Twitter',
+	'g+'				=> 'Google+',
+	'facebook'			=> 'Facebook',
+	'email'				=> 'Courriel',
+	'print'				=> 'Imprimer',
 
 	'article'			=> 'Article',
 	'title'				=> 'Titre',
@@ -183,38 +332,45 @@ return array (
 	'by'				=> 'par',
 
 	'load_more'			=> 'Charger plus d’articles',
-	'nothing_to_load'		=> 'Il n’y a pas plus d’article',
+	'nothing_to_load'		=> 'Fin des articles',
 
 	'rss_feeds_of'			=> 'Flux RSS de %s',
 
 	'refresh'			=> 'Actualisation',
+	'no_feed_to_refresh'		=> 'Il n’y a aucun flux à actualiser…',
 
 	'today'				=> 'Aujourd’hui',
 	'yesterday'			=> 'Hier',
 	'before_yesterday'		=> 'À partir d’avant-hier',
+	'new_article'			=> 'Il y a de nouveaux articles disponibles, cliquez pour rafraîchir la page.',
 	'by_author'			=> 'Par <em>%s</em>',
 	'related_tags'			=> 'Tags associés',
-	'no_feed_to_display'		=> 'Il n’y a aucun flux à afficher.',
+	'no_feed_to_display'		=> 'Il n’y a aucun article à afficher.',
 
 	'about_freshrss'		=> 'À propos de FreshRSS',
 	'project_website'		=> 'Site du projet',
 	'lead_developer'		=> 'Développeur principal',
 	'website'			=> 'Site Internet',
 	'bugs_reports'			=> 'Rapports de bugs',
-	'github_or_email'		=> '<a href="https://github.com/marienfressinaud/FreshRSS/issues">sur Github</a> ou <a href="mailto:dev@marienfressinaud.fr">par mail</a>',
+	'github_or_email'		=> '<a href="https://github.com/marienfressinaud/FreshRSS/issues">sur Github</a> ou <a href="mailto:dev@marienfressinaud.fr">par courriel</a>',
 	'license'			=> 'Licence',
 	'agpl3'				=> '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>',
-	'freshrss_description'		=> 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de <a href="http://rsslounge.aditu.de/">RSSLounge</a>, <a href="http://tt-rss.org/redmine/projects/tt-rss/wiki">TinyTinyRSS</a> ou <a href="http://projet.idleman.fr/leed/">Leed</a>. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable. L’objectif étant d’offrir une alternative sérieuse au futur feu-Google Reader.',
+	'freshrss_description'		=> 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://projet.idleman.fr/leed/">Leed</a>. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.',
 	'credits'			=> 'Crédits',
 	'credits_content'		=> 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS n’utilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Les favicons sont récupérés grâce au site <a href="https://getfavicon.appspot.com/">getFavicon</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
+	'version'			=> 'Version',
 
 	'logs'				=> 'Logs',
-	'logs_empty'			=> 'Les logs sont vides',
+	'logs_empty'			=> 'Les logs sont vides.',
+	'clear_logs'			=> 'Effacer les logs',
 
-	'forbidden_access'		=> 'Accès interdit',
-	'forbidden_access_description'	=> 'L’accès est protégé par un mot de passe, veuillez <a class="signin" href="#">vous connecter</a> pour accéder aux flux.',
+	'forbidden_access'		=> 'L’accès vous est interdit !',
+	'login_required'		=> 'Accès protégé par mot de passe :',
 
-	'confirm_action'		=> 'Êtes-vous sûr de vouloir continuer ? Cette action ne peut être annulée !',
+	'confirm_action'		=> 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !',
+	'confirm_action_feed_cat'	=> 'Êtes-vous sûr(e) de vouloir continuer ? Vous pourriez perdre les favoris et les filtres associés. Cette action ne peut être annulée !',
+	'notif_title_new_articles'	=> 'FreshRSS : nouveaux articles !',
+	'notif_body_new_articles'	=> 'Il y a \d nouveaux articles à lire sur FreshRSS.',
 
 	// DATE
 	'january'			=> 'janvier',
@@ -229,6 +385,25 @@ return array (
 	'october'			=> 'octobre',
 	'november'			=> 'novembre',
 	'december'			=> 'décembre',
+	'jan'				=> 'jan.',
+	'feb'				=> 'fév.',
+	'mar'				=> 'mar.',
+	'apr'				=> 'avr.',
+	'may'				=> 'mai.',
+	'jun'				=> 'juin',
+	'jul'				=> 'jui.',
+	'aug'				=> 'août',
+	'sep'				=> 'sep.',
+	'oct'				=> 'oct.',
+	'nov'				=> 'nov.',
+	'dec'				=> 'déc.',
+	'sun'				=> 'dim.',
+	'mon'				=> 'lun.',
+	'tue'				=> 'mar.',
+	'wed'				=> 'mer.',
+	'thu'				=> 'jeu.',
+	'fri'				=> 'ven.',
+	'sat'				=> 'sam.',
 	// format spécial pour la fonction date()
 	'Jan'				=> '\j\a\n\v\i\e\r',
 	'Feb'				=> '\f\é\v\r\i\e\r',
@@ -243,61 +418,37 @@ return array (
 	'Nov'				=> '\n\o\v\e\m\b\r\e',
 	'Dec'				=> '\d\é\c\e\m\b\r\e',
 	// format pour la fonction date(), %s permet d'indiquer le mois en toutes lettres
-	'format_date'			=> 'd %s Y',
-	'format_date_hour'		=> '\l\e d %s Y \à H\:i',
-
-	// INSTALLATION
-	'freshrss_installation'		=> 'Installation - FreshRSS',
-	'freshrss'			=> 'FreshRSS',
-	'installation_step'		=> 'Installation - étape %d',
-	'steps'				=> 'Étapes',
-	'checks'			=> 'Vérifications',
-	'bdd_configuration'		=> 'Configuration de la base de données',
-	'this_is_the_end'		=> 'This is the end',
-
-	'ok'				=> 'Ok !',
-	'congratulations'		=> 'Félicitations !',
-	'attention'			=> 'Attention !',
-	'damn'				=> 'Arf !',
-	'oops'				=> 'Oups !',
-	'next_step'			=> 'Passer à l’étape suivante',
-
-	'language_defined'		=> 'La langue a bien été définie.',
-	'choose_language'		=> 'Choisissez la langue pour FreshRSS',
-
-	'javascript_is_better'		=> 'FreshRSS est plus agréable à utiliser avec le Javascript d’activé',
-	'php_is_ok'			=> 'Votre version de PHP est la %s et est compatible avec FreshRSS',
-	'php_is_nok'			=> 'Votre version de PHP est la %s. Vous devriez avoir au moins la version %s',
-	'minz_is_ok'			=> 'Vous disposez du framework Minz',
-	'minz_is_nok'			=> 'Vous ne disposez pas de la librairie Minz. Vous devriez exécuter le script <em>build.sh</em> ou bien <a href="https://github.com/marienfressinaud/MINZ">la télécharger sur Github</a> et installer dans le répertoire <em>%s</em> le contenu de son répertoire <em>/lib</em>.',
-	'curl_is_ok'			=> 'Vous disposez de cURL dans sa version %s',
-	'curl_is_nok'			=> 'Vous ne disposez pas de cURL',
-	'pdomysql_is_ok'		=> 'Vous disposez de PDO et de son driver pour MySQL',
-	'pdomysql_is_nok'		=> 'Vous ne disposez pas de PDO ou de son driver pour MySQL',
-	'dom_is_ok'			=> 'Vous disposez du nécessaire pour parcourir le DOM',
-	'dom_is_nok'			=> 'Vous ne disposez pas du nécessaire pour parcourir le DOM (voir du côté du paquet php-xml ?)',
-	'cache_is_ok'			=> 'Les droits sur le répertoire de cache sont bons',
-	'log_is_ok'			=> 'Les droits sur le répertoire des logs sont bons',
-	'conf_is_ok'			=> 'Les droits sur le répertoire de configuration sont bons',
-	'data_is_ok'			=> 'Les droits sur le répertoire de data sont bons',
-	'file_is_nok'			=> 'Veuillez vérifier les droits sur le répertoire <em>%s</em>. Le serveur HTTP doit être capable d’écrire dedans',
-	'fix_errors_before'		=> 'Veuillez corriger les erreurs avant de passer à l’étape suivante.',
-
-	'general_conf_is_ok'		=> 'La configuration générale a été enregistrée.',
-	'random_string'			=> 'Chaîne aléatoire',
-	'change_value'			=> 'Vous devriez changer cette valeur par n’importe quelle autre',
-	'base_url'			=> 'Base de l’url',
-	'do_not_change_if_doubt'	=> 'Laissez tel quel dans le doute',
-
-	'bdd_conf_is_ok'		=> 'La configuration de la base de données a été enregistrée.',
-	'bdd_conf_is_ko'		=> 'Vérifiez les informations d’accès à la base de données.',
-	'host'				=> 'Hôte',
-	'username'			=> 'Nom utilisateur',
-	'password'			=> 'Mot de passe',
-	'bdd'				=> 'Base de données',
-	'prefix'			=> 'Préfixe des tables',
-
-	'installation_is_ok'		=> 'L’installation s’est bien passée. Il faut maintenant supprimer le fichier <em>install.php</em> pour pouvoir accéder à FreshRSS… ou simplement cliquer sur le bouton ci-dessous :)',
-	'finish_installation'		=> 'Terminer l’installation',
-	'install_not_deleted'		=> 'Quelque chose s’est mal passé, vous devez supprimer le fichier <em>%s</em> à la main.',
+	'format_date'			=> 'j %s Y',
+	'format_date_hour'		=> 'j %s Y \à H\:i',
+	
+	'status_favorites'		=> 'favoris',
+	'status_read'			=> 'lus',
+	'status_unread'			=> 'non lus',
+	'status_total'			=> 'total',
+	
+	'stats_entry_repartition'	=> 'Répartition des articles',
+	'stats_entry_per_day'		=> 'Nombre d’articles par jour (30 derniers jours)',
+	'stats_feed_per_category'	=> 'Flux par catégorie',
+	'stats_entry_per_category'	=> 'Articles par catégorie',
+	'stats_top_feed'		=> 'Les dix plus gros flux',
+	'stats_entry_count'		=> 'Nombre d’articles',
+	'stats_no_idle'			=> 'Il n’y a aucun flux inactif !',
+
+	'update'			=> 'Mise à jour',
+	'update_system'			=> 'Système de mise à jour',
+	'update_check'			=> 'Vérifier les mises à jour',
+	'update_last'			=> 'Dernière vérification : %s',
+	'update_can_apply'		=> 'Une mise à jour est disponible.',
+	'update_apply'			=> 'Appliquer la mise à jour',
+	'update_server_not_found'	=> 'Le serveur de mise à jour n’a pas été trouvé. [%s]',
+	'no_update'			=> 'Aucune mise à jour à appliquer',
+	'update_problem'		=> 'La mise à jour a rencontré un problème : %s',
+	'update_finished'		=> 'La mise à jour est terminée !',
+
+	'auth_reset'			=> 'Réinitialisation de l’authentification',
+	'auth_will_reset'		=> 'Le système d’authentification va être réinitialisé : un formulaire sera utilisé à la place de Persona.',
+	'auth_not_persona'		=> 'Seul le système d’authentification Persona peut être réinitialisé.',
+	'auth_no_password_set'		=> 'Aucun mot de passe administrateur n’a été précisé. Cette fonctionnalité n’est pas disponible.',
+	'auth_form_set'			=> 'Le formulaire est désormais votre système d’authentification.',
+	'auth_form_not_set'		=> 'Un problème est survenu lors de la configuration de votre système d’authentification. Veuillez réessayer plus tard.',
 );

+ 69 - 0
app/i18n/install.en.php

@@ -0,0 +1,69 @@
+<?php
+return array (
+	'freshrss_installation'		=> 'Installation · FreshRSS',
+	'freshrss'			=> 'FreshRSS',
+	'installation_step'		=> 'Installation — step %d · FreshRSS',
+	'steps'				=> 'Steps',
+	'checks'			=> 'Checks',
+	'general_configuration'	=> 'General configuration',
+	'bdd_configuration'		=> 'Database configuration',
+	'bdd_type'		=> 'Type of database',
+	'version_update'		=> 'Update',
+	'this_is_the_end'		=> 'This is the end',
+
+	'ok'				=> 'Ok!',
+	'congratulations'		=> 'Congratulations!',
+	'attention'			=> 'Attention!',
+	'damn'				=> 'Damn!',
+	'oops'				=> 'Oops!',
+	'next_step'			=> 'Go to the next step',
+
+	'language_defined'		=> 'Language has been defined.',
+	'choose_language'		=> 'Choose a language for FreshRSS',
+
+	'javascript_is_better'		=> 'FreshRSS is more pleasant with JavaScript enabled',
+	'php_is_ok'			=> 'Your PHP version is %s, which is compatible with FreshRSS',
+	'php_is_nok'			=> 'Your PHP version is %s but FreshRSS requires at least version %s',
+	'minz_is_ok'			=> 'You have the Minz framework',
+	'minz_is_nok'			=> 'You lack the Minz framework. You should execute <em>build.sh</em> script or <a href="https://github.com/marienfressinaud/MINZ">download it on Github</a> and install in <em>%s</em> directory the content of its <em>/lib</em> directory.',
+	'curl_is_ok'			=> 'You have version %s of cURL',
+	'curl_is_nok'			=> 'You lack cURL (php5-curl package)',
+	'pdo_is_ok'			=> 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite)',
+	'pdo_is_nok'			=> 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite)',
+	'dom_is_ok'			=> 'You have the required library to browse the DOM',
+	'dom_is_nok'			=> 'You lack a required library to browse the DOM (php-xml package)',
+	'pcre_is_ok'			=> 'You have the required library for regular expressions (PCRE)',
+	'pcre_is_nok'			=> 'You lack a required library for regular expressions (php-pcre)',
+	'ctype_is_ok'			=> 'You have the required library for character type checking (ctype)',
+	'ctype_is_nok'			=> 'You lack a required library for character type checking (php-ctype)',
+	'cache_is_ok'			=> 'Permissions on cache directory are good',
+	'log_is_ok'			=> 'Permissions on logs directory are good',
+	'favicons_is_ok'		=> 'Permissions on favicons directory are good',
+	'data_is_ok'			=> 'Permissions on data directory are good',
+	'persona_is_ok'			=> 'Permissions on Mozilla Persona directory are good',
+	'file_is_nok'			=> 'Check permissions on <em>%s</em> directory. HTTP server must have rights to write into',
+	'http_referer_is_ok'		=> 'Your HTTP REFERER is known and corresponds to your server.',
+	'http_referer_is_nok'		=> 'Please check that you are not altering your HTTP REFERER.',
+	'fix_errors_before'		=> 'Fix errors before skip to the next step.',
+
+	'general_conf_is_ok'		=> 'General configuration has been saved.',
+	'random_string'			=> 'Random string',
+	'change_value'			=> 'You should change this value by any other',
+	'base_url'			=> 'Base URL',
+	'do_not_change_if_doubt'	=> 'Don’t change if you doubt about it',
+
+	'bdd_conf_is_ok'		=> 'Database configuration has been saved.',
+	'bdd_conf_is_ko'		=> 'Verify your database information.',
+	'host'				=> 'Host',
+	'bdd'				=> 'Database',
+	'prefix'			=> 'Table prefix',
+
+	'update_start'			=> 'Start update process',
+	'update_long'			=> 'This can take a long time, depending on the size of your database. You may have to wait for this page to time out (~5 minutes) and then refresh this page.',
+	'update_end'			=> 'Update process is completed, now you can go to the final step.',
+
+
+	'installation_is_ok'		=> 'The installation process was successful.<br />The final step will now attempt to delete any file and database backup created during the update process.<br />You may choose to skip this step by deleting <kbd>./data/do-install.txt</kbd> manually.',
+	'finish_installation'		=> 'Complete installation',
+	'install_not_deleted'		=> 'Something went wrong; you must delete the file <em>%s</em> manually.',
+);

+ 68 - 0
app/i18n/install.fr.php

@@ -0,0 +1,68 @@
+<?php
+return array (
+	'freshrss_installation'		=> 'Installation · FreshRSS',
+	'freshrss'			=> 'FreshRSS',
+	'installation_step'		=> 'Installation — étape %d · FreshRSS',
+	'steps'				=> 'Étapes',
+	'checks'			=> 'Vérifications',
+	'general_configuration'	=> 'Configuration générale',
+	'bdd_configuration'		=> 'Base de données',
+	'bdd_type'		=> 'Type de base de données',
+	'version_update'		=> 'Mise à jour',
+	'this_is_the_end'		=> 'This is the end',
+
+	'ok'				=> 'Ok !',
+	'congratulations'		=> 'Félicitations !',
+	'attention'			=> 'Attention !',
+	'damn'				=> 'Arf !',
+	'oops'				=> 'Oups !',
+	'next_step'			=> 'Passer à l’étape suivante',
+
+	'language_defined'		=> 'La langue a bien été définie.',
+	'choose_language'		=> 'Choisissez la langue pour FreshRSS',
+
+	'javascript_is_better'		=> 'FreshRSS est plus agréable à utiliser avec JavaScript activé',
+	'php_is_ok'			=> 'Votre version de PHP est la %s, qui est compatible avec FreshRSS',
+	'php_is_nok'			=> 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s',
+	'minz_is_ok'			=> 'Vous disposez du framework Minz',
+	'minz_is_nok'			=> 'Vous ne disposez pas de la librairie Minz. Vous devriez exécuter le script <em>build.sh</em> ou bien <a href="https://github.com/marienfressinaud/MINZ">la télécharger sur Github</a> et installer dans le répertoire <em>%s</em> le contenu de son répertoire <em>/lib</em>.',
+	'curl_is_ok'			=> 'Vous disposez de cURL dans sa version %s',
+	'curl_is_nok'			=> 'Vous ne disposez pas de cURL (paquet php5-curl)',
+	'pdo_is_ok'			=> 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite)',
+	'pdo_is_nok'			=> 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite)',
+	'dom_is_ok'			=> 'Vous disposez du nécessaire pour parcourir le DOM',
+	'dom_is_nok'			=> 'Il manque une librairie pour parcourir le DOM (paquet php-xml)',
+	'pcre_is_ok'			=> 'Vous disposez du nécessaire pour les expressions régulières (PCRE)',
+	'pcre_is_nok'			=> 'Il manque une librairie pour les expressions régulières (php-pcre)',
+	'ctype_is_ok'			=> 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype)',
+	'ctype_is_nok'			=> 'Il manque une librairie pour la vérification des types de caractères (php-ctype)',
+	'cache_is_ok'			=> 'Les droits sur le répertoire de cache sont bons',
+	'log_is_ok'			=> 'Les droits sur le répertoire des logs sont bons',
+	'favicons_is_ok'		=> 'Les droits sur le répertoire des favicons sont bons',
+	'data_is_ok'			=> 'Les droits sur le répertoire de data sont bons',
+	'persona_is_ok'			=> 'Les droits sur le répertoire de Mozilla Persona sont bons',
+	'file_is_nok'			=> 'Veuillez vérifier les droits sur le répertoire <em>%s</em>. Le serveur HTTP doit être capable d’écrire dedans',
+	'http_referer_is_ok'		=> 'Le HTTP REFERER est connu et semble correspondre à votre serveur.',
+	'http_referer_is_nok'		=> 'Veuillez vérifier que vous ne modifiez pas votre HTTP REFERER.',
+	'fix_errors_before'		=> 'Veuillez corriger les erreurs avant de passer à l’étape suivante.',
+
+	'general_conf_is_ok'		=> 'La configuration générale a été enregistrée.',
+	'random_string'			=> 'Chaîne aléatoire',
+	'change_value'			=> 'Vous devriez changer cette valeur par n’importe quelle autre',
+	'base_url'			=> 'Base de l’URL',
+	'do_not_change_if_doubt'	=> 'Laissez tel quel dans le doute',
+
+	'bdd_conf_is_ok'		=> 'La configuration de la base de données a été enregistrée.',
+	'bdd_conf_is_ko'		=> 'Vérifiez les informations d’accès à la base de données.',
+	'host'				=> 'Hôte',
+	'bdd'				=> 'Base de données',
+	'prefix'			=> 'Préfixe des tables',
+
+	'update_start'			=> 'Lancer la mise à jour',
+	'update_long'			=> 'Ce processus peut prendre longtemps, selon la taille de votre base de données. Vous aurez peut-être à attendre que cette page dépasse son temps maximum d’exécution (~5 minutes) puis à la recharger.',
+	'update_end'			=> 'La mise à jour est terminée, vous pouvez maintenant passer à l’étape finale.',
+
+	'installation_is_ok'		=> 'L’installation s’est bien passée.<br />La dernière étape va maintenant tenter de supprimer les fichiers ainsi que d’éventuelles copies de base de données créés durant le processus de mise à jour.<br />Vous pouvez choisir de sauter cette étape en supprimant <kbd>./data/do-install.txt</kbd> manuellement.',
+	'finish_installation'		=> 'Terminer l’installation',
+	'install_not_deleted'		=> 'Quelque chose s’est mal passé, vous devez supprimer le fichier <em>%s</em> à la main.',
+);

+ 13 - 0
app/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>

+ 879 - 0
app/install.php

@@ -0,0 +1,879 @@
+<?php
+if (function_exists('opcache_reset')) {
+	opcache_reset();
+}
+
+define('BCRYPT_COST', 9);
+
+session_name('FreshRSS');
+session_set_cookie_params(0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
+session_start();
+
+if (isset($_GET['step'])) {
+	define('STEP',(int)$_GET['step']);
+} else {
+	define('STEP', 0);
+}
+
+define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+
+if (STEP === 3 && isset($_POST['type'])) {
+	$_SESSION['bd_type'] = $_POST['type'];
+}
+
+if (isset($_SESSION['bd_type'])) {
+	switch ($_SESSION['bd_type']) {
+	case 'mysql':
+		include(APP_PATH . '/SQL/install.sql.mysql.php');
+		break;
+	case 'sqlite':
+		include(APP_PATH . '/SQL/install.sql.sqlite.php');
+		break;
+	}
+}
+
+function param($key, $default = false) {
+	if (isset($_POST[$key])) {
+		return $_POST[$key];
+	} else {
+		return $default;
+	}
+}
+
+
+// gestion internationalisation
+$translates = array();
+$actual = 'en';
+function initTranslate() {
+	global $translates;
+	global $actual;
+
+	$actual = isset($_SESSION['language']) ? $_SESSION['language'] : getBetterLanguage('en');
+
+	$file = APP_PATH . '/i18n/' . $actual . '.php';
+	if (file_exists($file)) {
+		$translates = array_merge($translates, include($file));
+	}
+
+	$file = APP_PATH . '/i18n/install.' . $actual . '.php';
+	if (file_exists($file)) {
+		$translates = array_merge($translates, include($file));
+	}
+}
+
+function getBetterLanguage($fallback) {
+	$available = availableLanguages();
+	$accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
+	$language = strtolower(substr($accept, 0, 2));
+
+	if (isset($available[$language])) {
+		return $language;
+	} else {
+		return $fallback;
+	}
+}
+
+function availableLanguages() {
+	return array(
+		'en' => 'English',
+		'fr' => 'Français'
+	);
+}
+
+function _t($key) {
+	global $translates;
+	$translate = $key;
+	if (isset($translates[$key])) {
+		$translate = $translates[$key];
+	}
+
+	$args = func_get_args();
+	unset($args[0]);
+
+	return vsprintf($translate, $args);
+}
+
+
+/*** SAUVEGARDES ***/
+function saveLanguage() {
+	if (!empty($_POST)) {
+		if (!isset($_POST['language'])) {
+			return false;
+		}
+
+		$_SESSION['language'] = $_POST['language'];
+
+		header('Location: index.php?step=1');
+	}
+}
+
+function saveStep2() {
+	if (!empty($_POST)) {
+		$_SESSION['title'] = substr(trim(param('title', _t('freshrss'))), 0, 25);
+		$_SESSION['old_entries'] = param('old_entries', 3);
+		$_SESSION['auth_type'] = param('auth_type', 'form');
+		$_SESSION['default_user'] = substr(preg_replace('/[^a-zA-Z0-9]/', '', param('default_user', '')), 0, 16);
+		$_SESSION['mail_login'] = filter_var(param('mail_login', ''), FILTER_VALIDATE_EMAIL);
+
+		$password_plain = param('passwordPlain', false);
+		if ($password_plain !== false) {
+			if (!function_exists('password_hash')) {
+				include_once(LIB_PATH . '/password_compat.php');
+			}
+			$passwordHash = password_hash($password_plain, PASSWORD_BCRYPT, array('cost' => BCRYPT_COST));
+			$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
+			$_SESSION['passwordHash'] = $passwordHash;
+		}
+
+		if (empty($_SESSION['title']) ||
+		    empty($_SESSION['old_entries']) ||
+		    empty($_SESSION['auth_type']) ||
+		    empty($_SESSION['default_user'])) {
+			return false;
+		}
+
+		if (($_SESSION['auth_type'] === 'form' && empty($_SESSION['passwordHash'])) ||
+				($_SESSION['auth_type'] === 'persona' && empty($_SESSION['mail_login']))) {
+			return false;
+		}
+
+		$_SESSION['salt'] = sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__)));
+		if ((!ctype_digit($_SESSION['old_entries'])) ||($_SESSION['old_entries'] < 1)) {
+			$_SESSION['old_entries'] = 3;
+		}
+
+		$token = '';
+		if ($_SESSION['mail_login']) {
+			$token = sha1($_SESSION['salt'] . $_SESSION['mail_login']);
+		}
+
+		$config_array = array(
+			'language' => $_SESSION['language'],
+			'theme' => 'Origine',
+			'old_entries' => $_SESSION['old_entries'],
+			'mail_login' => $_SESSION['mail_login'],
+			'passwordHash' => $_SESSION['passwordHash'],
+			'token' => $token,
+		);
+
+		$configPath = DATA_PATH . '/' . $_SESSION['default_user'] . '_user.php';
+		@unlink($configPath);	//To avoid access-rights problems
+		file_put_contents($configPath, "<?php\n return " . var_export($config_array, true) . ';');
+
+		if ($_SESSION['mail_login'] != '') {
+			$personaFile = DATA_PATH . '/persona/' . $_SESSION['mail_login'] . '.txt';
+			@unlink($personaFile);
+			file_put_contents($personaFile, $_SESSION['default_user']);
+		}
+
+		header('Location: index.php?step=3');
+	}
+}
+
+function saveStep3() {
+	if (!empty($_POST)) {
+		if ($_SESSION['bd_type'] === 'sqlite') {
+			$_SESSION['bd_base'] = $_SESSION['default_user'];
+			$_SESSION['bd_host'] = '';
+			$_SESSION['bd_user'] = '';
+			$_SESSION['bd_password'] = '';
+			$_SESSION['bd_prefix'] = '';
+			$_SESSION['bd_prefix_user'] = '';	//No prefix for SQLite
+		} else {
+			if (empty($_POST['type']) ||
+			    empty($_POST['host']) ||
+			    empty($_POST['user']) ||
+			    empty($_POST['base'])) {
+				$_SESSION['bd_error'] = 'Missing parameters!';
+			}
+			$_SESSION['bd_base'] = substr($_POST['base'], 0, 64);
+			$_SESSION['bd_host'] = $_POST['host'];
+			$_SESSION['bd_user'] = $_POST['user'];
+			$_SESSION['bd_password'] = $_POST['pass'];
+			$_SESSION['bd_prefix'] = substr($_POST['prefix'], 0, 16);
+			$_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] .(empty($_SESSION['default_user']) ? '' :($_SESSION['default_user'] . '_'));
+		}
+
+		$ini_array = array(
+			'general' => array(
+				'environment' => empty($_SESSION['environment']) ? 'production' : $_SESSION['environment'],
+				'salt' => $_SESSION['salt'],
+				'base_url' => '',
+				'title' => $_SESSION['title'],
+				'default_user' => $_SESSION['default_user'],
+				'allow_anonymous' => isset($_SESSION['allow_anonymous']) ? $_SESSION['allow_anonymous'] : false,
+				'allow_anonymous_refresh' => isset($_SESSION['allow_anonymous_refresh']) ? $_SESSION['allow_anonymous_refresh'] : false,
+				'auth_type' => $_SESSION['auth_type'],
+				'api_enabled' => isset($_SESSION['api_enabled']) ? $_SESSION['api_enabled'] : false,
+				'unsafe_autologin_enabled' => isset($_SESSION['unsafe_autologin_enabled']) ? $_SESSION['unsafe_autologin_enabled'] : false,
+			),
+			'db' => array(
+				'type' => $_SESSION['bd_type'],
+				'host' => $_SESSION['bd_host'],
+				'user' => $_SESSION['bd_user'],
+				'password' => $_SESSION['bd_password'],
+				'base' => $_SESSION['bd_base'],
+				'prefix' => $_SESSION['bd_prefix'],
+			),
+		);
+
+		@unlink(DATA_PATH . '/config.php');	//To avoid access-rights problems
+		file_put_contents(DATA_PATH . '/config.php', "<?php\n return " . var_export($ini_array, true) . ';');
+
+		$res = checkBD();
+
+		if ($res) {
+			$_SESSION['bd_error'] = '';
+			header('Location: index.php?step=4');
+		} elseif (empty($_SESSION['bd_error'])) {
+			$_SESSION['bd_error'] = 'Unknown error!';
+		}
+	}
+	invalidateHttpCache();
+}
+
+function newPdo() {
+	switch ($_SESSION['bd_type']) {
+	case 'mysql':
+		$str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base'];
+		$driver_options = array(
+			PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
+		);
+		break;
+	case 'sqlite':
+		$str = 'sqlite:' . DATA_PATH . '/' . $_SESSION['default_user'] . '.sqlite';
+		$driver_options = array(
+			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+		);
+		break;
+	default:
+		return false;
+	}
+	return new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
+}
+
+function deleteInstall() {
+	$res = unlink(DATA_PATH . '/do-install.txt');
+
+	if (!$res) {
+		return false;
+	}
+
+	header('Location: index.php');
+}
+
+
+/*** VÉRIFICATIONS ***/
+function checkStep() {
+	$s0 = checkStep0();
+	$s1 = checkStep1();
+	$s2 = checkStep2();
+	$s3 = checkStep3();
+	if (STEP > 0 && $s0['all'] != 'ok') {
+		header('Location: index.php?step=0');
+	} elseif (STEP > 1 && $s1['all'] != 'ok') {
+		header('Location: index.php?step=1');
+	} elseif (STEP > 2 && $s2['all'] != 'ok') {
+		header('Location: index.php?step=2');
+	} elseif (STEP > 3 && $s3['all'] != 'ok') {
+		header('Location: index.php?step=3');
+	}
+	$_SESSION['actualize_feeds'] = true;
+}
+
+function checkStep0() {
+	$languages = availableLanguages();
+	$language = isset($_SESSION['language']) &&
+	            isset($languages[$_SESSION['language']]);
+
+	return array(
+		'language' => $language ? 'ok' : 'ko',
+		'all' => $language ? 'ok' : 'ko'
+	);
+}
+
+function checkStep1() {
+	$php = version_compare(PHP_VERSION, '5.2.1') >= 0;
+	$minz = file_exists(LIB_PATH . '/Minz');
+	$curl = extension_loaded('curl');
+	$pdo_mysql = extension_loaded('pdo_mysql');
+	$pdo_sqlite = extension_loaded('pdo_sqlite');
+	$pdo = $pdo_mysql || $pdo_sqlite;
+	$pcre = extension_loaded('pcre');
+	$ctype = extension_loaded('ctype');
+	$dom = class_exists('DOMDocument');
+	$data = DATA_PATH && is_writable(DATA_PATH);
+	$cache = CACHE_PATH && is_writable(CACHE_PATH);
+	$log = LOG_PATH && is_writable(LOG_PATH);
+	$favicons = is_writable(DATA_PATH . '/favicons');
+	$persona = is_writable(DATA_PATH . '/persona');
+	$http_referer = is_referer_from_same_domain();
+
+	return array(
+		'php' => $php ? 'ok' : 'ko',
+		'minz' => $minz ? 'ok' : 'ko',
+		'curl' => $curl ? 'ok' : 'ko',
+		'pdo-mysql' => $pdo_mysql ? 'ok' : 'ko',
+		'pdo-sqlite' => $pdo_sqlite ? 'ok' : 'ko',
+		'pdo' => $pdo ? 'ok' : 'ko',
+		'pcre' => $pcre ? 'ok' : 'ko',
+		'ctype' => $ctype ? 'ok' : 'ko',
+		'dom' => $dom ? 'ok' : 'ko',
+		'data' => $data ? 'ok' : 'ko',
+		'cache' => $cache ? 'ok' : 'ko',
+		'log' => $log ? 'ok' : 'ko',
+		'favicons' => $favicons ? 'ok' : 'ko',
+		'persona' => $persona ? 'ok' : 'ko',
+		'http_referer' => $http_referer ? 'ok' : 'ko',
+		'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom &&
+		         $data && $cache && $log && $favicons && $persona && $http_referer ?
+		         'ok' : 'ko'
+	);
+}
+
+function checkStep2() {
+	$conf = !empty($_SESSION['title']) &&
+	        !empty($_SESSION['old_entries']) &&
+	        isset($_SESSION['mail_login']) &&
+	        !empty($_SESSION['default_user']);
+
+	$form = (
+		isset($_SESSION['auth_type']) &&
+		($_SESSION['auth_type'] != 'form' || !empty($_SESSION['passwordHash']))
+	);
+
+	$persona = (
+		isset($_SESSION['auth_type']) &&
+		($_SESSION['auth_type'] != 'persona' || !empty($_SESSION['mail_login']))
+	);
+
+	$defaultUser = empty($_POST['default_user']) ? null : $_POST['default_user'];
+	if ($defaultUser === null) {
+		$defaultUser = empty($_SESSION['default_user']) ? '' : $_SESSION['default_user'];
+	}
+	$data = is_writable(DATA_PATH . '/' . $defaultUser . '_user.php');
+
+	return array(
+		'conf' => $conf ? 'ok' : 'ko',
+		'form' => $form ? 'ok' : 'ko',
+		'persona' => $persona ? 'ok' : 'ko',
+		'data' => $data ? 'ok' : 'ko',
+		'all' => $conf && $form && $persona && $data ? 'ok' : 'ko'
+	);
+}
+
+function checkStep3() {
+	$conf = is_writable(DATA_PATH . '/config.php');
+
+	$bd = isset($_SESSION['bd_type']) &&
+	      isset($_SESSION['bd_host']) &&
+	      isset($_SESSION['bd_user']) &&
+	      isset($_SESSION['bd_password']) &&
+	      isset($_SESSION['bd_base']) &&
+	      isset($_SESSION['bd_prefix']) &&
+	      isset($_SESSION['bd_error']);
+	$conn = empty($_SESSION['bd_error']);
+
+	return array(
+		'bd' => $bd ? 'ok' : 'ko',
+		'conn' => $conn ? 'ok' : 'ko',
+		'conf' => $conf ? 'ok' : 'ko',
+		'all' => $bd && $conn && $conf ? 'ok' : 'ko'
+	);
+}
+
+function checkBD() {
+	$ok = false;
+
+	try {
+		$str = '';
+		$driver_options = null;
+		switch ($_SESSION['bd_type']) {
+		case 'mysql':
+			$driver_options = array(
+				PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
+			);
+
+			try {	// on ouvre une connexion juste pour créer la base si elle n'existe pas
+				$str = 'mysql:host=' . $_SESSION['bd_host'] . ';';
+				$c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
+				$sql = sprintf(SQL_CREATE_DB, $_SESSION['bd_base']);
+				$res = $c->query($sql);
+			} catch (PDOException $e) {
+			}
+
+			// on écrase la précédente connexion en sélectionnant la nouvelle BDD
+			$str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base'];
+			break;
+		case 'sqlite':
+			$str = 'sqlite:' . DATA_PATH . '/' . $_SESSION['default_user'] . '.sqlite';
+			$driver_options = array(
+				PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+			);
+			break;
+		default:
+			return false;
+		}
+
+		$c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
+
+		if (defined('SQL_CREATE_TABLES')) {
+			$sql = sprintf(SQL_CREATE_TABLES, $_SESSION['bd_prefix_user'], _t('default_category'));
+			$stm = $c->prepare($sql);
+			$ok = $stm->execute();
+		} else {
+			global $SQL_CREATE_TABLES;
+			if (is_array($SQL_CREATE_TABLES)) {
+				$ok = true;
+				foreach ($SQL_CREATE_TABLES as $instruction) {
+					$sql = sprintf($instruction, $_SESSION['bd_prefix_user'], _t('default_category'));
+					$stm = $c->prepare($sql);
+					$ok &= $stm->execute();
+				}
+			}
+		}
+	} catch (PDOException $e) {
+		$ok = false;
+		$_SESSION['bd_error'] = $e->getMessage();
+	}
+
+	if (!$ok) {
+		@unlink(DATA_PATH . '/config.php');
+	}
+
+	return $ok;
+}
+
+/*** AFFICHAGE ***/
+function printStep0() {
+	global $actual;
+?>
+	<?php $s0 = checkStep0(); if ($s0['all'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('language_defined'); ?></p>
+	<?php } ?>
+
+	<form action="index.php?step=0" method="post">
+		<legend><?php echo _t('choose_language'); ?></legend>
+		<div class="form-group">
+			<label class="group-name" for="language"><?php echo _t('language'); ?></label>
+			<div class="group-controls">
+				<select name="language" id="language">
+				<?php $languages = availableLanguages(); ?>
+				<?php foreach ($languages as $short => $lib) { ?>
+				<option value="<?php echo $short; ?>"<?php echo $actual == $short ? ' selected="selected"' : ''; ?>><?php echo $lib; ?></option>
+				<?php } ?>
+				</select>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo _t('save'); ?></button>
+				<button type="reset" class="btn"><?php echo _t('cancel'); ?></button>
+				<?php if ($s0['all'] == 'ok') { ?>
+				<a class="btn btn-important next-step" href="?step=1"><?php echo _t('next_step'); ?></a>
+				<?php } ?>
+			</div>
+		</div>
+	</form>
+<?php
+}
+
+function printStep1() {
+	$res = checkStep1();
+?>
+	<noscript><p class="alert alert-warn"><span class="alert-head"><?php echo _t('attention'); ?></span> <?php echo _t('javascript_is_better'); ?></p></noscript>
+
+	<?php if ($res['php'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('php_is_ok', PHP_VERSION); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('php_is_nok', PHP_VERSION, '5.2.1'); ?></p>
+	<?php } ?>
+
+	<?php if ($res['minz'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('minz_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('minz_is_nok', LIB_PATH . '/Minz'); ?></p>
+	<?php } ?>
+
+	<?php if ($res['pdo'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('pdo_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('pdo_is_nok'); ?></p>
+	<?php } ?>
+
+	<?php if ($res['curl'] == 'ok') { ?>
+	<?php $version = curl_version(); ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('curl_is_ok', $version['version']); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('curl_is_nok'); ?></p>
+	<?php } ?>
+
+	<?php if ($res['pcre'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('pcre_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('pcre_is_nok'); ?></p>
+	<?php } ?>
+
+	<?php if ($res['ctype'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('ctype_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('ctype_is_nok'); ?></p>
+	<?php } ?>
+
+	<?php if ($res['dom'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('dom_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('dom_is_nok'); ?></p>
+	<?php } ?>
+
+	<?php if ($res['data'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('data_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('file_is_nok', DATA_PATH); ?></p>
+	<?php } ?>
+
+	<?php if ($res['cache'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('cache_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('file_is_nok', CACHE_PATH); ?></p>
+	<?php } ?>
+
+	<?php if ($res['log'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('log_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('file_is_nok', LOG_PATH); ?></p>
+	<?php } ?>
+
+	<?php if ($res['favicons'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('favicons_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('file_is_nok', DATA_PATH . '/favicons'); ?></p>
+	<?php } ?>
+
+	<?php if ($res['persona'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('persona_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('file_is_nok', DATA_PATH . '/persona'); ?></p>
+	<?php } ?>
+
+	<?php if ($res['http_referer'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('http_referer_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('http_referer_is_nok'); ?></p>
+	<?php } ?>
+
+	<?php if ($res['all'] == 'ok') { ?>
+	<a class="btn btn-important next-step" href="?step=2"><?php echo _t('next_step'); ?></a>
+	<?php } else { ?>
+	<p class="alert alert-error"><?php echo _t('fix_errors_before'); ?></p>
+	<?php } ?>
+<?php
+}
+
+function printStep2() {
+?>
+	<?php $s2 = checkStep2(); if ($s2['all'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('general_conf_is_ok'); ?></p>
+	<?php } elseif (!empty($_POST)) { ?>
+	<p class="alert alert-error"><?php echo _t('fix_errors_before'); ?></p>
+	<?php } ?>
+
+	<form action="index.php?step=2" method="post">
+		<legend><?php echo _t('general_configuration'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="title"><?php echo _t('title'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="title" name="title" value="<?php echo isset($_SESSION['title']) ? $_SESSION['title'] : _t('freshrss'); ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="old_entries"><?php echo _t('delete_articles_every'); ?></label>
+			<div class="group-controls">
+				<input type="number" id="old_entries" name="old_entries" required="required" min="1" max="1200" value="<?php echo isset($_SESSION['old_entries']) ? $_SESSION['old_entries'] : '3'; ?>" /> <?php echo _t('month'); ?>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="default_user"><?php echo _t('default_user'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="default_user" name="default_user" required="required" size="16" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" value="<?php echo isset($_SESSION['default_user']) ? $_SESSION['default_user'] : ''; ?>" placeholder="<?php echo httpAuthUser() == '' ? 'user1' : httpAuthUser(); ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="auth_type"><?php echo _t('auth_type'); ?></label>
+			<div class="group-controls">
+				<select id="auth_type" name="auth_type" required="required" onchange="auth_type_change(true)">
+					<?php
+						function no_auth($auth_type) {
+							return !in_array($auth_type, array('form', 'persona', 'http_auth', 'none'));
+						}
+						$auth_type = isset($_SESSION['auth_type']) ? $_SESSION['auth_type'] : '';
+					?>
+					<option value="form"<?php echo $auth_type === 'form' || no_auth($auth_type) ? ' selected="selected"' : '', cryptAvailable() ? '' : ' disabled="disabled"'; ?>><?php echo _t('auth_form'); ?></option>
+					<option value="persona"<?php echo $auth_type === 'persona' ? ' selected="selected"' : ''; ?>><?php echo _t('auth_persona'); ?></option>
+					<option value="http_auth"<?php echo $auth_type === 'http_auth' ? ' selected="selected"' : '', httpAuthUser() == '' ? ' disabled="disabled"' : ''; ?>><?php echo _t('http_auth'); ?>(REMOTE_USER = '<?php echo httpAuthUser(); ?>')</option>
+					<option value="none"<?php echo $auth_type === 'none' ? ' selected="selected"' : ''; ?>><?php echo _t('auth_none'); ?></option>
+				</select>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="passwordPlain"><?php echo _t('password_form'); ?></label>
+			<div class="group-controls">
+				<div class="stick">
+					<input type="password" id="passwordPlain" name="passwordPlain" pattern=".{7,}" autocomplete="off" <?php echo $auth_type === 'form' ? ' required="required"' : ''; ?> />
+					<a class="btn toggle-password" data-toggle="passwordPlain"><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				</div>
+				<noscript><b><?php echo _t('javascript_should_be_activated'); ?></b></noscript>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="mail_login"><?php echo _t('persona_connection_email'); ?></label>
+			<div class="group-controls">
+				<input type="email" id="mail_login" name="mail_login" value="<?php echo isset($_SESSION['mail_login']) ? $_SESSION['mail_login'] : ''; ?>" placeholder="alice@example.net" <?php echo $auth_type === 'persona' ? ' required="required"' : ''; ?> />
+				<noscript><b><?php echo _t('javascript_should_be_activated'); ?></b></noscript>
+			</div>
+		</div>
+
+		<script>
+			function toggle_password() {
+				var button = this;
+				var passwordField = document.getElementById(button.getAttribute('data-toggle'));
+
+				passwordField.setAttribute('type', 'text');
+				button.className += ' active';
+
+				setTimeout(function() {
+					passwordField.setAttribute('type', 'password');
+					button.className = button.className.replace(/(?:^|\s)active(?!\S)/g , '');
+				}, 2000);
+
+				return false;
+			}
+			toggles = document.getElementsByClassName('toggle-password');
+			for (var i = 0 ; i < toggles.length ; i++) {
+				toggles[i].addEventListener('click', toggle_password);
+			}
+
+			function auth_type_change(focus) {
+				var auth_value = document.getElementById('auth_type').value,
+				    password_input = document.getElementById('passwordPlain'),
+				    mail_input = document.getElementById('mail_login');
+
+				if (auth_value === 'form') {
+					password_input.required = true;
+					mail_input.required = false;
+					if (focus) {
+						password_input.focus();
+					}
+				} else if (auth_value === 'persona') {
+					password_input.required = false;
+					mail_input.required = true;
+					if (focus) {
+						mail_input.focus();
+					}
+				} else {
+					password_input.required = false;
+					mail_input.required = false;
+				}
+			}
+			auth_type_change(false);
+		</script>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo _t('save'); ?></button>
+				<button type="reset" class="btn"><?php echo _t('cancel'); ?></button>
+				<?php if ($s2['all'] == 'ok') { ?>
+				<a class="btn btn-important next-step" href="?step=3"><?php echo _t('next_step'); ?></a>
+				<?php } ?>
+			</div>
+		</div>
+	</form>
+<?php
+}
+
+function printStep3() {
+?>
+	<?php $s3 = checkStep3(); if ($s3['all'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('bdd_conf_is_ok'); ?></p>
+	<?php } elseif ($s3['conn'] == 'ko') { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('bdd_conf_is_ko'),(empty($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']); ?></p>
+	<?php } ?>
+
+	<form action="index.php?step=3" method="post">
+		<legend><?php echo _t('bdd_configuration'); ?></legend>
+		<div class="form-group">
+			<label class="group-name" for="type"><?php echo _t('bdd_type'); ?></label>
+			<div class="group-controls">
+				<select name="type" id="type" onchange="mySqlShowHide()">
+				<?php if (extension_loaded('pdo_mysql')) {?>
+				<option value="mysql"
+					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql') ? 'selected="selected"' : ''; ?>>
+					MySQL
+				</option>
+				<?php }?>
+				<?php if (extension_loaded('pdo_sqlite')) {?>
+				<option value="sqlite"
+					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'sqlite') ? 'selected="selected"' : ''; ?>>
+					SQLite
+				</option>
+				<?php }?>
+				</select>
+			</div>
+		</div>
+
+		<div id="mysql">
+		<div class="form-group">
+			<label class="group-name" for="host"><?php echo _t('host'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="host" name="host" pattern="[0-9A-Za-z_.-]{1,64}" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : 'localhost'; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="user"><?php echo _t('username'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="user" name="user" maxlength="16" pattern="[0-9A-Za-z_.-]{1,16}" value="<?php echo isset($_SESSION['bd_user']) ? $_SESSION['bd_user'] : ''; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="pass"><?php echo _t('password'); ?></label>
+			<div class="group-controls">
+				<input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="base"><?php echo _t('bdd'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="base" name="base" maxlength="64" pattern="[0-9A-Za-z_]{1,64}" value="<?php echo isset($_SESSION['bd_base']) ? $_SESSION['bd_base'] : ''; ?>" placeholder="freshrss" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="prefix"><?php echo _t('prefix'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="prefix" name="prefix" maxlength="16" pattern="[0-9A-Za-z_]{1,16}" value="<?php echo isset($_SESSION['bd_prefix']) ? $_SESSION['bd_prefix'] : 'freshrss_'; ?>" />
+			</div>
+		</div>
+		</div>
+		<script>
+			function mySqlShowHide() {
+				document.getElementById('mysql').style.display = document.getElementById('type').value === 'mysql' ? 'block' : 'none';
+			}
+			mySqlShowHide();
+		</script>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo _t('save'); ?></button>
+				<button type="reset" class="btn"><?php echo _t('cancel'); ?></button>
+				<?php if ($s3['all'] == 'ok') { ?>
+				<a class="btn btn-important next-step" href="?step=4"><?php echo _t('next_step'); ?></a>
+				<?php } ?>
+			</div>
+		</div>
+	</form>
+<?php
+}
+
+function printStep4() {
+?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('congratulations'); ?></span> <?php echo _t('installation_is_ok'); ?></p>
+	<a class="btn btn-important next-step" href="?step=5"><?php echo _t('finish_installation'); ?></a>
+<?php
+}
+
+function printStep5() {
+?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('oops'); ?></span> <?php echo _t('install_not_deleted', DATA_PATH . '/do-install.txt'); ?></p>
+<?php
+}
+
+checkStep();
+
+initTranslate();
+
+switch (STEP) {
+case 0:
+default:
+	saveLanguage();
+	break;
+case 1:
+	break;
+case 2:
+	saveStep2();
+	break;
+case 3:
+	saveStep3();
+	break;
+case 4:
+	break;
+case 5:
+	deleteInstall();
+	break;
+}
+?>
+<!DOCTYPE html>
+<html lang="fr">
+	<head>
+		<meta charset="utf-8">
+		<meta name="viewport" content="initial-scale=1.0">
+		<title><?php echo _t('freshrss_installation'); ?></title>
+		<link rel="stylesheet" type="text/css" media="all" href="../themes/base-theme/template.css" />
+		<link rel="stylesheet" type="text/css" media="all" href="../themes/Origine/origine.css" />
+	</head>
+	<body>
+
+<div class="header">
+	<div class="item title">
+		<h1><a href="index.php"><?php echo _t('freshrss'); ?></a></h1>
+		<h2><?php echo _t('installation_step', STEP); ?></h2>
+	</div>
+</div>
+
+<div id="global">
+	<ul class="nav nav-list aside">
+		<li class="nav-header"><?php echo _t('steps'); ?></li>
+		<li class="item<?php echo STEP == 0 ? ' active' : ''; ?>"><a href="?step=0"><?php echo _t('language'); ?></a></li>
+		<li class="item<?php echo STEP == 1 ? ' active' : ''; ?>"><a href="?step=1"><?php echo _t('checks'); ?></a></li>
+		<li class="item<?php echo STEP == 2 ? ' active' : ''; ?>"><a href="?step=2"><?php echo _t('general_configuration'); ?></a></li>
+		<li class="item<?php echo STEP == 3 ? ' active' : ''; ?>"><a href="?step=3"><?php echo _t('bdd_configuration'); ?></a></li>
+		<li class="item<?php echo STEP == 4 ? ' active' : ''; ?>"><a href="?step=5"><?php echo _t('this_is_the_end'); ?></a></li>
+	</ul>
+
+	<div class="post">
+		<?php
+		switch (STEP) {
+		case 0:
+		default:
+			printStep0();
+			break;
+		case 1:
+			printStep1();
+			break;
+		case 2:
+			printStep2();
+			break;
+		case 3:
+			printStep3();
+			break;
+		case 4:
+			printStep4();
+			break;
+		case 5:
+			printStep5();
+			break;
+		}
+		?>
+	</div>
+</div>
+	</body>
+</html>

+ 30 - 10
app/layout/aside_configure.phtml

@@ -1,13 +1,33 @@
-<div class="nav nav-list aside">
-	<li class="nav-header"><?php echo Translate::t ('configuration'); ?></li>
-
-	<li class="item<?php echo Request::actionName () == 'display' ? ' active' : ''; ?>">
-		<a href="<?php echo Url::display (array ('c' => 'configure', 'a' => 'display')); ?>"><?php echo Translate::t ('general_and_reading'); ?></a>
+<ul class="nav nav-list aside">
+	<li class="nav-header"><?php echo _t('configuration'); ?></li>
+	<li class="item<?php echo Minz_Request::actionName() === 'display' ? ' active' : ''; ?>">
+		<a href="<?php echo _url('configure', 'display'); ?>"><?php echo _t('display_configuration'); ?></a>
 	</li>
-	<li class="item<?php echo Request::actionName () == 'categorize' ? ' active' : ''; ?>">
-		<a href="<?php echo Url::display (array ('c' => 'configure', 'a' => 'categorize')); ?>"><?php echo Translate::t ('categories'); ?></a>
+	<li class="item<?php echo Minz_Request::actionName() === 'reading' ? ' active' : ''; ?>">
+		<a href="<?php echo _url('configure', 'reading'); ?>"><?php echo _t('reading_configuration'); ?></a>
 	</li>
-	<li class="item<?php echo Request::actionName () == 'shortcut' ? ' active' : ''; ?>">
-		<a href="<?php echo Url::display (array ('c' => 'configure', 'a' => 'shortcut')); ?>"><?php echo Translate::t ('shortcuts'); ?></a>
+	<li class="item<?php echo Minz_Request::actionName() === 'archiving' ? ' active' : ''; ?>">
+		<a href="<?php echo _url('configure', 'archiving'); ?>"><?php echo _t('archiving_configuration'); ?></a>
 	</li>
-</div>
+	<li class="item<?php echo Minz_Request::actionName() === 'sharing' ? ' active' : ''; ?>">
+		<a href="<?php echo _url('configure', 'sharing'); ?>"><?php echo _t('sharing'); ?></a>
+	</li>
+	<li class="item<?php echo Minz_Request::actionName() === 'shortcut' ? ' active' : ''; ?>">
+		<a href="<?php echo _url('configure', 'shortcut'); ?>"><?php echo _t('shortcuts'); ?></a>
+	</li>
+	<li class="item<?php echo Minz_Request::actionName() === 'queries' ? ' active' : ''; ?>">
+		<a href="<?php echo _url('configure', 'queries'); ?>"><?php echo _t('queries'); ?></a>
+	</li>
+	<li class="separator"></li>
+	<li class="item<?php echo Minz_Request::actionName() === 'users' ? ' active' : ''; ?>">
+		<a href="<?php echo _url('configure', 'users'); ?>"><?php echo _t('users'); ?></a>
+	</li>
+	<?php
+		$current_user = Minz_Session::param('currentUser', '');
+		if (Minz_Configuration::isAdmin($current_user)) {
+	?>
+	<li class="item<?php echo Minz_Request::controllerName() === 'update' ? ' active' : ''; ?>">
+		<a href="<?php echo _url('update', 'index'); ?>"><?php echo _t('update'); ?></a>
+	</li>
+	<?php } ?>
+</ul>

+ 32 - 15
app/layout/aside_feed.phtml

@@ -1,58 +1,75 @@
 <ul class="nav nav-list aside aside_feed">
-	<li class="nav-header"><?php echo Translate::t ('your_rss_feeds'); ?></li>
+	<li class="nav-header"><?php echo Minz_Translate::t ('your_rss_feeds'); ?></li>
 
-	<li class="nav-form"><form id="add_rss" method="post" action="<?php echo Url::display (array ('c' => 'feed', 'a' => 'add')); ?>">
+	<li class="nav-form"><form id="add_rss" method="post" action="<?php echo Minz_Url::display (array ('c' => 'feed', 'a' => 'add')); ?>" autocomplete="off">
 		<div class="stick">
-			<input type="url" name="url_rss" placeholder="<?php echo Translate::t ('add_rss_feed'); ?>" />
+			<input type="url" name="url_rss" placeholder="<?php echo Minz_Translate::t ('add_rss_feed'); ?>" />
 			<div class="dropdown">
 				<div id="dropdown-cat" class="dropdown-target"></div>
 
-				<a class="dropdown-toggle btn" href="#dropdown-cat"><i class="icon i_down"></i></a>
+				<a class="dropdown-toggle btn" href="#dropdown-cat"><?php echo FreshRSS_Themes::icon('down'); ?></a>
 				<ul class="dropdown-menu">
-					<li class="dropdown-close"><a href="#close">&nbsp;</a></li>
+					<li class="dropdown-close"><a href="#close"></a></li>
 
-					<li class="dropdown-header"><?php echo Translate::t ('category'); ?></li>
+					<li class="dropdown-header"><?php echo Minz_Translate::t ('category'); ?></li>
 
 					<li class="input">
 						<select name="category" id="category">
 						<?php foreach ($this->categories as $cat) { ?>
-						<option value="<?php echo $cat->id (); ?>"<?php echo $cat->id () == '000000' ? ' selected="selected"' : ''; ?>>
+						<option value="<?php echo $cat->id (); ?>"<?php echo $cat->id () == 1 ? ' selected="selected"' : ''; ?>>
 							<?php echo $cat->name (); ?>
 						</option>
 						<?php } ?>
+						<option value="nc"><?php echo Minz_Translate::t ('new_category'); ?></option>
 						</select>
 					</li>
 
+					<li class="input" style="display:none">
+						<input type="text" name="new_category[name]" id="new_category_name" autocomplete="off" placeholder="<?php echo Minz_Translate::t ('new_category'); ?>" />
+					</li>
+
 					<li class="separator"></li>
 
-					<li class="dropdown-header"><?php echo Translate::t ('http_authentication'); ?></li>
+					<li class="dropdown-header"><?php echo Minz_Translate::t ('http_authentication'); ?></li>
 					<li class="input">
-						<input type="text" name="username" id="username" placeholder="<?php echo Translate::t ('username'); ?>" />
+						<input type="text" name="http_user" id="http_user_add" autocomplete="off" placeholder="<?php echo Minz_Translate::t ('username'); ?>" />
 					</li>
 					<li class="input">
-						<input type="password" name="password" id="password" placeholder="<?php echo Translate::t ('password'); ?>" />
+						<input type="password" name="http_pass" id="http_pass_add" autocomplete="off" placeholder="<?php echo Minz_Translate::t ('password'); ?>" />
 					</li>
 				</ul>
 			</div>
-			<button class="btn" type="submit"><i class="icon i_add"></i></button>
+			<button class="btn" type="submit"><?php echo FreshRSS_Themes::icon('add'); ?></button>
 		</div>
 	</form></li>
 
-	<li class="item<?php echo Request::actionName () == 'importExport' ? ' active' : ''; ?>"><a href="<?php echo _url ('configure', 'importExport'); ?>"><?php echo Translate::t ('import_export_opml'); ?></a></li>
+	<li class="item">
+		<a onclick="return false;" href="javascript:(function(){var%20url%20=%20location.href;window.open('<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'add'), 'html', true); ?>&amp;url_rss='+encodeURIComponent(url), '_blank');})();">
+			<?php echo Minz_Translate::t('bookmark'); ?>
+		</a>
+	</li>
+
+	<li class="item<?php echo Minz_Request::controllerName () == 'importExport' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('importExport', 'index'); ?>"><?php echo Minz_Translate::t ('import_export'); ?></a>
+	</li>
+
+	<li class="item<?php echo Minz_Request::actionName () == 'categorize' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('configure', 'categorize'); ?>"><?php echo Minz_Translate::t ('categories_management'); ?></a>
+	</li>
 
 	<li class="separator"></li>
 
 	<?php if (!empty ($this->feeds)) { ?>
 	<?php foreach ($this->feeds as $feed) { ?>
 	<?php $nbEntries = $feed->nbEntries (); ?>
-	<li class="item<?php echo ($this->flux && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>">
+	<li class="item<?php echo (isset($this->flux) && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>">
 		<a href="<?php echo _url ('configure', 'feed', 'id', $feed->id ()); ?>">
-			<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" />
+			<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" />
 			<?php echo $feed->name (); ?>
 		</a>
 	</li>
 	<?php } ?>
 	<?php } else { ?>
-	<li class="item disable"><?php echo Translate::t ('no_rss_feed'); ?></li>
+	<li class="item disable"><?php echo Minz_Translate::t ('no_rss_feed'); ?></li>
 	<?php } ?>
 </ul>

+ 82 - 85
app/layout/aside_flux.phtml

@@ -1,106 +1,103 @@
-<div class="aside aside_flux" id="aside_flux">
-	<a class="toggle_aside" href="#close"><i class="icon i_close"></i></a>
+<div class="aside aside_flux<?php if ($this->conf->hide_read_feeds && ($this->state & FreshRSS_Entry::STATE_NOT_READ) && !($this->state & FreshRSS_Entry::STATE_READ)) echo ' state_unread'; ?>" id="aside_flux">
+	<a class="toggle_aside" href="#close"><?php echo _i('close'); ?></a>
 
 	<ul class="categories">
-		<?php
-			$params = Request::params ();
-			$params['output'] = 'rss';
-			if (isset ($params['search'])) {
-				$params['search'] = urlencode ($params['search']);
-			}
-
-			$token = $this->conf->token ();
-			if (login_is_conf($this->conf) && $token != '') {
-				$params['token'] = $token;
-			}
+		<?php if ($this->loginOk) { ?>
+		<form id="mark-read-aside" method="post" style="display: none"></form>
 
-			$url = array (
-				'c' => 'index',
-				'a' => 'index',
-				'params' => $params
-			);
-		?>
-		<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
 		<li>
-			<div class="stick">
-				<a class="btn btn-important" href="<?php echo _url ('configure', 'feed'); ?>"><?php echo Translate::t ('subscription_management'); ?></a>
-				<a class="btn btn-important" href="<?php echo Url::display ($url); ?>"><i class="icon i_rss"></i></a>
+			<div class="stick configure-feeds">
+				<a class="btn btn-important" href="<?php echo _url('configure', 'feed'); ?>"><?php echo _t('subscription_management'); ?></a>
+				<a class="btn btn-important" href="<?php echo _url('configure', 'categorize'); ?>" title="<?php echo _t('categories_management'); ?>"><?php echo _i('category-white'); ?></a>
 			</div>
 		</li>
+		<?php } elseif (Minz_Configuration::needsLogin()) { ?>
+		<li><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about_freshrss'); ?></a></li>
 		<?php } ?>
 
+		<?php
+			$arUrl = array('c' => 'index', 'a' => 'index', 'params' => array());
+			if ($this->conf->view_mode !== Minz_Request::param('output', 'normal')) {
+				$arUrl['params']['output'] = 'normal';
+			}
+		?>
 		<li>
-			<div class="all">
-				<a class="btn<?php echo $this->get_c == 'all' ? ' active' : ''; ?>" href="<?php echo _url ('index', 'index'); ?>">
-					<i class="icon i_all"></i>
-					<?php echo Translate::t ('all_feeds', $this->nb_total); ?>
-					<?php if ($this->nb_not_read > 0) { ?>
-					<span class="notRead"><?php echo $this->nb_not_read > 1 ? Translate::t ('not_reads', $this->nb_not_read) : Translate::t ('not_read', $this->nb_not_read); ?></span>
-					<?php } ?>
+			<div class="category all<?php echo $this->get_c == 'a' ? ' active' : ''; ?>">
+				<a data-unread="<?php echo formatNumber($this->nb_not_read); ?>" class="btn<?php echo $this->get_c == 'a' ? ' active' : ''; ?>" href="<?php echo Minz_Url::display($arUrl); ?>">
+					<?php echo _i('all'); ?>
+					<?php echo _t('main_stream'); ?>
 				</a>
 			</div>
 		</li>
 
 		<li>
-			<div class="favorites">
-				<a class="btn<?php echo $this->get_c == 'favoris' ? ' active' : ''; ?>" href="<?php echo _url ('index', 'index', 'get', 'favoris'); ?>">
-					<i class="icon i_bookmark"></i>
-					<?php echo Translate::t ('favorite_feeds', $this->nb_favorites); ?>
+			<div class="category favorites<?php echo $this->get_c == 's' ? ' active' : ''; ?>">
+				<a data-unread="<?php echo formatNumber($this->nb_favorites['unread']); ?>" class="btn<?php echo $this->get_c == 's' ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 's'; echo Minz_Url::display($arUrl); ?>">
+					<?php echo _i('bookmark'); ?>
+					<?php echo _t('favorite_feeds', formatNumber($this->nb_favorites['all'])); ?>
 				</a>
 			</div>
 		</li>
 
-		<?php foreach ($this->cat_aside as $cat) { ?>
-		<?php $feeds = $cat->feeds (); $catNotRead = $cat->nbNotRead (); ?>
-		<?php if (!empty ($feeds)) { ?>
-		<li>
-			<?php $c_active = false; if ($this->get_c == $cat->id ()) { $c_active = true; } ?>
-			<div class="category<?php echo $c_active ? ' active' : ''; ?>">
-				<a class="btn<?php echo $c_active ? ' active' : ''; ?>" href="<?php echo _url ('index', 'index', 'get', 'c_' . $cat->id ()); ?>">
-					<?php echo $cat->name (); ?>
-					<?php if ($catNotRead > 0) { ?>
-					<span class="notRead" title="<?php echo $catNotRead > 1 ? Translate::t ('not_reads', $catNotRead) : Translate::t ('not_read', $catNotRead); ?>"><?php echo $catNotRead ; ?></span>
-					<?php } ?>
-				</a>
-			</div>
-
-			<ul class="feeds<?php echo $c_active ? ' active' : ''; ?>">
-				<?php foreach ($feeds as $feed) { ?>
-				<?php $nbEntries = $feed->nbEntries (); ?>
-				<?php $f_active = false; if ($this->get_f == $feed->id ()) { $f_active = true; } ?>
-				<li class="item<?php echo $f_active ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>">
-					<div class="dropdown">
-						<div id="dropdown-<?php echo $feed->id(); ?>" class="dropdown-target"></div>
-						<a class="dropdown-toggle" href="#dropdown-<?php echo $feed->id(); ?>"><i class="icon i_configure"></i></a>
-						<ul class="dropdown-menu">
-							<li class="dropdown-close"><a href="#close">&nbsp;</a></li>
-							<li class="item"><a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>"><?php echo Translate::t ('filter'); ?></a></li>
-							<li class="item"><a target="_blank" href="<?php echo $feed->website (); ?>"><?php echo Translate::t ('see_website'); ?></a></li>
-							<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
-							<li class="separator"></li>
-
-							<li class="item"><a href="<?php echo _url ('configure', 'feed', 'id', $feed->id ()); ?>"><?php echo Translate::t ('administration'); ?></a></li>
-							<li class="item"><a href="<?php echo _url ('feed', 'actualize', 'id', $feed->id ()); ?>"><?php echo Translate::t ('actualize'); ?></a></li>
-							<li class="item"><a href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', 'f_' . $feed->id ()); ?>"><?php echo Translate::t ('mark_read'); ?></a></li>
-							<?php } ?>
-						</ul>
-					</div>
-
-					<?php $not_read = $feed->nbNotRead (); ?>
+		<?php
+		foreach ($this->cat_aside as $cat) {
+			$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 _i($c_show ? 'up' : 'down'); ?></a><?php
+				?></div><?php
+				?><ul class="feeds<?php echo $c_show ? ' active' : ''; ?>"><?php
+				foreach ($feeds as $feed) {
+					$feed_id = $feed->id();
+					$nbEntries = $feed->nbEntries();
+					$f_active = ($this->get_f == $feed_id);
+					?><li id="f_<?php echo $feed_id; ?>" class="item<?php echo $f_active ? ' active' : ''; ?><?php echo $feed->inError() ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>" data-unread="<?php echo $feed->nbNotRead(); ?>"><?php
+						?><div class="dropdown"><?php
+							?><div class="dropdown-target"></div><?php
+							?><a class="dropdown-toggle" data-fweb="<?php echo $feed->website(); ?>"><?php echo _i('configure'); ?></a><?php
+							/* feed_config_template */
+						?></div><?php
+						?> <img class="favicon" src="<?php echo $feed->favicon(); ?>" alt="✇" /> <?php
+						?><a class="feed" data-unread="<?php echo formatNumber($feed->nbNotRead()); ?>" data-priority="<?php echo $feed->priority(); ?>" href="<?php $arUrl['params']['get'] = 'f_' . $feed_id; echo Minz_Url::display($arUrl); ?>"><?php echo $feed->name(); ?></a><?php
+					?></li><?php
+				}
+				?></ul><?php
+				?></li><?php
+			}
+		} ?>
+	</ul>
+	<span class="aside_flux_ender"><!-- For fixed menu --></span>
+</div>
 
-					<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" />
-					<?php echo $not_read > 0 ? '<b>' : ''; ?>
-					<a class="feed" href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>">
-						<?php echo $not_read > 0 ? '(' . $not_read . ') ' : ''; ?>
-						<?php echo $feed->name(); ?>
-					</a>
-					<?php echo $not_read > 0 ? '</b>' : ''; ?>
-				</li>
-				<?php } ?>
-			</ul>
+<script id="feed_config_template" type="text/html">
+	<ul class="dropdown-menu">
+		<li class="dropdown-close"><a href="#close">❌</a></li>
+		<li class="item"><a href="<?php echo _url('index', 'index', 'get', 'f_!!!!!!'); ?>"><?php echo _t('filter'); ?></a></li>
+		<?php if ($this->loginOk) { ?>
+		<li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '!!!!!!'); ?>"><?php echo _t('stats'); ?></a></li>
+		<?php } ?>
+		<li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('see_website'); ?></a></li>
+		<?php if ($this->loginOk) { ?>
+		<li class="separator"></li>
+		<li class="item"><a href="<?php echo _url('configure', 'feed', 'id', '!!!!!!'); ?>"><?php echo _t('administration'); ?></a></li>
+		<li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '!!!!!!'); ?>"><?php echo _t('actualize'); ?></a></li>
+		<li class="item">
+			<?php $confirm = $this->conf->reading_confirm ? 'confirm' : ''; ?>
+			<button class="read_all as-link <?php echo $confirm; ?>"
+			        form="mark-read-aside"
+			        formaction="<?php echo _url('entry', 'read', 'get', 'f_!!!!!!'); ?>"
+			        type="submit"><?php echo _t('mark_read'); ?></button>
 		</li>
-		<?php } } ?>
+		<?php } ?>
 	</ul>
-
-	<span class="aside_flux_ender"><!-- hack for fix menus, if it can be helpful ;) --></span>
-</div>
+</script>

+ 12 - 0
app/layout/aside_stats.phtml

@@ -0,0 +1,12 @@
+<ul class="nav nav-list aside">
+	<li class="nav-header"><?php echo Minz_Translate::t ('stats'); ?></li>
+	<li class="item<?php echo Minz_Request::actionName () == 'index' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('stats', 'index'); ?>"><?php echo Minz_Translate::t ('stats_main'); ?></a>
+	</li>
+	<li class="item<?php echo Minz_Request::actionName () == 'idle' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('stats', 'idle'); ?>"><?php echo Minz_Translate::t ('stats_idle'); ?></a>
+	</li>
+	<li class="item<?php echo Minz_Request::actionName () == 'repartition' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('stats', 'repartition'); ?>"><?php echo Minz_Translate::t ('stats_repartition'); ?></a>
+	</li>
+</ul>

+ 83 - 44
app/layout/header.phtml

@@ -1,76 +1,115 @@
-<?php if (login_is_conf ($this->conf)) { ?>
-<ul class="nav nav-head nav-login">
-	<?php if (!is_logged ()) { ?>
-	<li class="item"><i class="icon i_login"></i> <a class="signin" href="#"><?php echo Translate::t ('login'); ?></a></li>
-	<?php } else { ?>
-	<li class="item"><i class="icon i_logout"></i> <a class="signout" href="#"><?php echo Translate::t ('logout'); ?></a></li>
-	<?php } ?>
-</ul>
-<?php } ?>
+<?php
+if (Minz_Configuration::canLogIn()) {
+	?><ul class="nav nav-head nav-login"><?php
+	switch (Minz_Configuration::authType()) {
+	case 'form':
+		if ($this->loginOk) {
+			?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="<?php echo _url('index', 'formLogout'); ?>"><?php echo _t('logout'); ?></a></li><?php
+		} else {
+			?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="<?php echo _url('index', 'formLogin'); ?>"><?php echo _t('login'); ?></a></li><?php
+		}
+		break;
+	case 'persona':
+		if ($this->loginOk) {
+			?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="#"><?php echo _t('logout'); ?></a></li><?php
+		} else {
+			?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="#"><?php echo _t('login'); ?></a></li><?php
+		}
+		break;
+	}
+	?></ul><?php
+}
+?>
 
 <div class="header">
 	<div class="item title">
-		<img class="logo" src="<?php echo Url::display ('/icons/icon-32.png'); ?>" alt="" />
-		<h1><a href="<?php echo _url ('index', 'index'); ?>"><?php echo Configuration::title (); ?></a></h1>
+		<h1>
+			<a href="<?php echo _url('index', 'index'); ?>">
+				<img class="logo" src="<?php echo _i('icon', true); ?>" alt="⊚" />
+				<?php echo Minz_Configuration::title(); ?>
+			</a>
+		</h1>
 	</div>
 
 	<div class="item search">
-		<?php if(!login_is_conf ($this->conf) ||
-		         is_logged() ||
-		         $this->conf->anonAccess() == 'yes') { ?>
-		<form action="<?php echo _url ('index', 'index'); ?>" method="get">
+		<?php if ($this->loginOk || Minz_Configuration::allowAnonymous()) { ?>
+		<form action="<?php echo _url('index', 'index'); ?>" method="get">
 			<div class="stick">
-				<?php $search = Request::param ('search', ''); ?>
-				<input type="text" name="search" id="search" value="<?php echo $search; ?>" placeholder="<?php echo Translate::t ('search'); ?>" />
+				<?php $search = Minz_Request::param('search', ''); ?>
+				<input type="search" name="search" id="search" class="extend" value="<?php echo $search; ?>" placeholder="<?php echo _t('search'); ?>" />
 
-				<?php $get = Request::param ('get', ''); ?>
-				<?php if($get != '') { ?>
+				<?php $get = Minz_Request::param('get', ''); ?>
+				<?php if ($get != '') { ?>
 				<input type="hidden" name="get" value="<?php echo $get; ?>" />
 				<?php } ?>
 
-				<?php $order = Request::param ('order', ''); ?>
-				<?php if($order != '') { ?>
+				<?php $order = Minz_Request::param('order', ''); ?>
+				<?php if ($order != '') { ?>
 				<input type="hidden" name="order" value="<?php echo $order; ?>" />
 				<?php } ?>
 
-				<?php $state = Request::param ('state', ''); ?>
-				<?php if($state != '') { ?>
+				<?php $state = Minz_Request::param('state', ''); ?>
+				<?php if ($state != '') { ?>
 				<input type="hidden" name="state" value="<?php echo $state; ?>" />
 				<?php } ?>
 
-				<button class="btn" type="submit"><i class="icon i_search"></i></button>
+				<button class="btn" type="submit"><?php echo _i('search'); ?></button>
 			</div>
 		</form>
 		<?php } ?>
 	</div>
 
-	<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
+	<?php if ($this->loginOk) { ?>
 	<div class="item configure">
 		<div class="dropdown">
 			<div id="dropdown-configure" class="dropdown-target"></div>
-
-			<a class="btn dropdown-toggle" href="#dropdown-configure"><i class="icon i_configure"></i></a>
+			<a class="btn dropdown-toggle" href="#dropdown-configure"><?php echo _i('configure'); ?></a>
 			<ul class="dropdown-menu">
-				<li class="dropdown-close"><a href="#close">&nbsp;</a></li>
-				<li class="dropdown-header"><?php echo Translate::t ('configuration'); ?></li>
-				<li class="item"><a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Translate::t ('general_and_reading'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('configure', 'categorize'); ?>"><?php echo Translate::t ('categories'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('configure', 'shortcut'); ?>"><?php echo Translate::t ('shortcuts'); ?></a></li>
-				<li class="separator"></li>
-				<li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Translate::t ('about'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('index', 'logs'); ?>"><?php echo Translate::t ('logs'); ?></a></li>
-				<?php if (login_is_conf ($this->conf) && is_logged ()) { ?>
+				<li class="dropdown-close"><a href="#close">❌</a></li>
+				<li class="dropdown-header"><?php echo _t('configuration'); ?></li>
+				<li class="item"><a href="<?php echo _url('configure', 'display'); ?>"><?php echo _t('display_configuration'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'reading'); ?>"><?php echo _t('reading_configuration'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'archiving'); ?>"><?php echo _t('archiving_configuration'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'sharing'); ?>"><?php echo _t('sharing'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'shortcut'); ?>"><?php echo _t('shortcuts'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'queries'); ?>"><?php echo _t('queries'); ?></a></li>
 				<li class="separator"></li>
-				<li class="item"><a class="signout" href="#"><i class="icon i_logout"></i> <?php echo Translate::t ('logout'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('configure', 'users'); ?>"><?php echo _t('users'); ?></a></li>
+				<?php
+					$current_user = Minz_Session::param('currentUser', '');
+					if (Minz_Configuration::isAdmin($current_user)) {
+				?>
+				<li class="item"><a href="<?php echo _url('update', 'index'); ?>"><?php echo _t('update'); ?></a></li>
 				<?php } ?>
+				<li class="separator"></li>
+				<li class="item"><a href="<?php echo _url('stats', 'index'); ?>"><?php echo _t('stats'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('index', 'logs'); ?>"><?php echo _t('logs'); ?></a></li>
+				<li class="item"><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about'); ?></a></li>
+				<?php
+				if (Minz_Configuration::canLogIn()) {
+					?><li class="separator"></li><?php
+					switch (Minz_Configuration::authType()) {
+					case 'form':
+						?><li class="item"><a class="signout" href="<?php echo _url('index', 'formLogout'); ?>"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php
+						break;
+					case 'persona':
+						?><li class="item"><a class="signout" href="#"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php
+						break;
+					}
+				} ?>
 			</ul>
 		</div>
 	</div>
-	<?php }
-
-	if (login_is_conf ($this->conf) && !is_logged ()) { ?>
-	<div class="item configure">
-		<i class="icon i_login"></i> <a class="signin" href="#"><?php echo Translate::t ('login'); ?></a>
-	</div>
-	<?php } ?>
+	<?php } elseif (Minz_Configuration::canLogIn()) {
+		?><div class="item configure"><?php
+		switch (Minz_Configuration::authType()) {
+		case 'form':
+			echo _i('login'); ?><a class="signin" href="<?php echo _url('index', 'formLogin'); ?>"><?php echo _t('login'); ?></a></li><?php
+			break;
+		case 'persona':
+			echo _i('login'); ?><a class="signin" href="#"><?php echo _t('login'); ?></a></li><?php
+			break;
+		}
+		?></div><?php
+	} ?>
 </div>

+ 51 - 18
app/layout/layout.phtml

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

+ 4 - 4
app/layout/nav_entries.phtml

@@ -1,5 +1,5 @@
-<ul class="nav_entries">
-	<li class="item"><a class="previous_entry" href="#"><i class="icon i_prev"></i></a></li>
-	<li class="item"><a class="up" href="#"><i class="icon i_up"></i></a></li>
-	<li class="item"><a class="next_entry" href="#"><i class="icon i_next"></i></a></li>
+<ul id="nav_entries">
+	<li class="item"><a class="previous_entry" href="#"><?php echo FreshRSS_Themes::icon('prev'); ?></a></li>
+	<li class="item"><a class="up" href="#"><?php echo FreshRSS_Themes::icon('up'); ?></a></li>
+	<li class="item"><a class="next_entry" href="#"><?php echo FreshRSS_Themes::icon('next'); ?></a></li>
 </ul>

+ 266 - 128
app/layout/nav_menu.phtml

@@ -1,168 +1,306 @@
+<?php
+	$actual_view = Minz_Request::param('output', 'normal');
+?>
 <div class="nav_menu">
-	<a class="btn toggle_aside" href="#aside_flux"><i class="icon i_category"></i></a>
+	<?php if ($actual_view === 'normal') { ?>
+	<a class="btn toggle_aside" href="#aside_flux"><?php echo _i('category'); ?></a>
+	<?php } ?>
+
+	<?php if ($this->loginOk) { ?>
+	<div id="nav_menu_actions" class="stick">
+		<?php
+			$url_state = $this->url;
+
+			if ($this->state & FreshRSS_Entry::STATE_READ) {
+				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_READ;
+				$checked = 'true';
+				$class = 'active';
+			} else {
+				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_READ;
+				$checked = 'false';
+				$class = '';
+			}
+		?>
+		<a id="toggle-read"
+		   class="btn <?php echo $class; ?>"
+		   aria-checked="<?php echo $checked; ?>"
+		   href="<?php echo Minz_Url::display($url_state); ?>"
+		   title="<?php echo _t('show_read'); ?>">
+			<?php echo _i('read'); ?>
+		</a>
+
+		<?php
+			if ($this->state & FreshRSS_Entry::STATE_NOT_READ) {
+				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_READ;
+				$checked = 'true';
+				$class = 'active';
+			} else {
+				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_NOT_READ;
+				$checked = 'false';
+				$class = '';
+			}
+		?>
+		<a id="toggle-unread"
+		   class="btn <?php echo $class; ?>"
+		   aria-checked="<?php echo $checked; ?>"
+		   href="<?php echo Minz_Url::display($url_state); ?>"
+		   title="<?php echo _t('show_not_reads'); ?>">
+			<?php echo _i('unread'); ?>
+		</a>
+
+		<?php
+			if ($this->state & FreshRSS_Entry::STATE_FAVORITE || $this->get_c == 's') {
+				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_FAVORITE;
+				$checked = 'true';
+				$class = 'active';
+			} else {
+				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_FAVORITE;
+				$checked = 'false';
+				$class = '';
+			}
+		?>
+		<a id="toggle-favorite"
+		   class="btn <?php echo $class; ?>"
+		   aria-checked="<?php echo $checked; ?>"
+		   href="<?php echo Minz_Url::display($url_state); ?>"
+		   title="<?php echo _t('show_favorite'); ?>">
+			<?php echo _i('starred'); ?>
+		</a>
+
+		<?php
+			if ($this->state & FreshRSS_Entry::STATE_NOT_FAVORITE) {
+				$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_FAVORITE;
+				$checked = 'true';
+				$class = 'active';
+			} else {
+				$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_NOT_FAVORITE;
+				$checked = 'false';
+				$class = '';
+			}
+		?>
+		<a id="toggle-not-favorite"
+		   class="btn <?php echo $class; ?>"
+		   aria-checked="<?php echo $checked; ?>"
+		   href="<?php echo Minz_Url::display($url_state); ?>"
+		   title="<?php echo _t('show_not_favorite'); ?>">
+			<?php echo _i('non-starred'); ?>
+		</a>
+
+		<div class="dropdown">
+			<div id="dropdown-query" class="dropdown-target"></div>
+
+			<a class="dropdown-toggle btn" href="#dropdown-query"><?php echo _i('down'); ?></a>
+			<ul class="dropdown-menu">
+				<li class="dropdown-close"><a href="#close">❌</a></li>
 
-	<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
-	<a id="actualize" class="btn" href="<?php echo _url ('feed', 'actualize'); ?>"><i class="icon i_refresh"></i></a>
+				<li class="dropdown-header">
+					<?php echo _t('queries'); ?>
+					<a class="no-mobile" href="<?php echo _url('configure', 'queries'); ?>"><?php echo _i('configure'); ?></a>
+				</li>
 
+				<?php foreach ($this->conf->queries as $query) { ?>
+				<li class="item query">
+					<a href="<?php echo $query['url']; ?>"><?php echo $query['name']; ?></a>
+				</li>
+				<?php } ?>
+
+				<?php if (count($this->conf->queries) > 0) { ?>
+				<li class="separator no-mobile"></li>
+				<?php } ?>
+
+				<?php
+					$url_query = $this->url;
+					$url_query['c'] = 'configure';
+					$url_query['a'] = 'addQuery';
+				?>
+				<li class="item no-mobile"><a href="<?php echo Minz_Url::display($url_query); ?>"><?php echo _i('bookmark-add'); ?> <?php echo _t('add_query'); ?></a></li>
+			</ul>
+		</div>
+	</div>
 	<?php
 		$get = false;
-		$string_mark = Translate::t ('mark_all_read');
+		$string_mark = _t('mark_all_read');
 		if ($this->get_f) {
 			$get = 'f_' . $this->get_f;
-			$string_mark = Translate::t ('mark_feed_read');
-		} elseif ($this->get_c &&
-		          $this->get_c != 'all' &&
-		          $this->get_c != 'favoris' &&
-		          $this->get_c != 'public') {
-			$get = 'c_' . $this->get_c;
-			$string_mark = Translate::t ('mark_cat_read');
+			$string_mark = _t('mark_feed_read');
+		} elseif ($this->get_c && $this->get_c != 'a') {
+			if ($this->get_c === 's') {
+				$get = 's';
+			} else {
+				$get = 'c_' . $this->get_c;
+			}
+			$string_mark = _t('mark_cat_read');
 		}
 		$nextGet = $get;
-		if (($this->conf->onread_jump_next () === 'yes') && (strlen ($get) > 2)) {
+		if ($this->conf->onread_jump_next && strlen($get) > 2) {
 			$anotherUnreadId = '';
 			$foundCurrent = false;
 			switch ($get[0]) {
-				case 'c':
-					foreach ($this->cat_aside as $cat) {
-						if ($cat->id () === $this->get_c) {
-							$foundCurrent = true;
-							continue;
-						}
-						if ($cat->nbNotRead () <= 0) continue;
-						$anotherUnreadId = $cat->id ();
-						if ($foundCurrent) break;
+			case 'c':
+				foreach ($this->cat_aside as $cat) {
+					if ($cat->id() == $this->get_c) {
+						$foundCurrent = true;
+						continue;
 					}
-					$nextGet = strlen ($anotherUnreadId) > 1 ? 'c_' . $anotherUnreadId : 'all';
-					break;
-				case 'f':
-					foreach ($this->cat_aside as $cat) {
-						if ($cat->id () === $this->get_c) {
-							foreach ($cat->feeds () as $feed) {
-								if ($feed->id () === $this->get_f) {
-									$foundCurrent = true;
-									continue;
-								}
-								if ($feed->nbNotRead () <= 0) continue;
-								$anotherUnreadId = $feed->id ();
-								if ($foundCurrent) break;
+					if ($cat->nbNotRead() <= 0) continue;
+					$anotherUnreadId = $cat->id();
+					if ($foundCurrent) break;
+				}
+				$nextGet = empty($anotherUnreadId) ? 'a' : 'c_' . $anotherUnreadId;
+				break;
+			case 'f':
+				foreach ($this->cat_aside as $cat) {
+					if ($cat->id() == $this->get_c) {
+						foreach ($cat->feeds() as $feed) {
+							if ($feed->id() == $this->get_f) {
+								$foundCurrent = true;
+								continue;
 							}
-							break;
+							if ($feed->nbNotRead() <= 0) continue;
+							$anotherUnreadId = $feed->id();
+							if ($foundCurrent) break;
 						}
+						break;
 					}
-					$nextGet = strlen ($anotherUnreadId) > 1 ? 'f_' . $anotherUnreadId : 'c_' . $this->get_c;
-					break;
+				}
+				$nextGet = empty($anotherUnreadId) ? 'c_' . $this->get_c : 'f_' . $anotherUnreadId;
+				break;
+			}
+		}
+
+		$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));
+		$output = Minz_Request::param('output', '');
+		if ($output != '' && $this->conf->view_mode !== $output) {
+			$arUrl['params']['output'] = $output;
+		}
+		$markReadUrl = Minz_Url::display($arUrl);
+		Minz_Session::_param('markReadUrl', $markReadUrl);
 	?>
 
+	<form id="mark-read-menu" method="post" style="display: none"></form>
+
 	<div class="stick" id="nav_menu_read_all">
-		<a class="read_all btn" href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', $get, 'nextGet', $nextGet); ?>"><?php echo Translate::t ('mark_read'); ?></a>
+		<?php $confirm = $this->conf->reading_confirm ? 'confirm' : ''; ?>
+		<button class="read_all btn <?php echo $confirm; ?>"
+		        form="mark-read-menu"
+		        formaction="<?php echo $markReadUrl; ?>"
+		        type="submit"><?php echo _t('mark_read'); ?></button>
+
 		<div class="dropdown">
 			<div id="dropdown-read" class="dropdown-target"></div>
 
-			<a class="dropdown-toggle btn" href="#dropdown-read"><i class="icon i_down"></i></a>
+			<a class="dropdown-toggle btn" href="#dropdown-read"><?php echo _i('down'); ?></a>
 			<ul class="dropdown-menu">
-				<li class="dropdown-close"><a href="#close">&nbsp;</a></li>
+				<li class="dropdown-close"><a href="#close"></a></li>
 
-				<li class="item"><a href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', $get, 'nextGet', $nextGet); ?>"><?php echo $string_mark; ?></a></li> 
+				<li class="item">
+					<button class="as-link <?php echo $confirm; ?>"
+					        form="mark-read-menu"
+					        formaction="<?php echo $markReadUrl; ?>"
+					        type="submit"><?php echo $string_mark; ?></button>
+				</li>
 				<li class="separator"></li>
 <?php
-	$date = getdate ();
-	$today = mktime (0, 0, 0, $date['mon'], $date['mday'], $date['year']);
-	$one_week = $today - 604800;
+	$mark_before_today = $arUrl;
+	$mark_before_today['params']['idMax'] = $this->today . '000000';
+	$mark_before_one_week = $arUrl;
+	$mark_before_one_week['params']['idMax'] = ($this->today - 604800) . '000000';
 ?>
-				<li class="item"><a href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', $get, 'dateMax', $today); ?>"><?php echo Translate::t ('before_one_day'); ?></a></li>
-				<li class="item"><a href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', $get, 'dateMax', $one_week); ?>"><?php echo Translate::t ('before_one_week'); ?></a></li>
+				<li class="item">
+					<button class="as-link <?php echo $confirm; ?>"
+					        form="mark-read-menu"
+					        formaction="<?php echo Minz_Url::display($mark_before_today); ?>"
+					        type="submit"><?php echo _t('before_one_day'); ?></button>
+				</li>
+				<li class="item">
+					<button class="as-link <?php echo $confirm; ?>"
+					        form="mark-read-menu"
+					        formaction="<?php echo Minz_Url::display($mark_before_one_week); ?>"
+					        type="submit"><?php echo _t('before_one_week'); ?></button>
+				</li>
 			</ul>
 		</div>
 	</div>
 	<?php } ?>
 
-	<?php
-		$params = Request::params ();
-		if (isset ($params['search'])) {
-			$params['search'] = urlencode ($params['search']);
-		}
-		$url = array (
-			'c' => 'index',
-			'a' => 'index',
-			'params' => $params
-		);
-	?>
-	<div class="dropdown" id="nav_menu_views">
-		<div id="dropdown-views" class="dropdown-target"></div>
-		<a class="dropdown-toggle btn" href="#dropdown-views"><?php echo Translate::t ('display'); ?> <i class="icon i_down"></i></a>
-		<ul class="dropdown-menu">
-			<li class="dropdown-close"><a href="#close">&nbsp;</a></li>
-
-			<?php
-				$url_output = $url;
-				$actual_view = Request::param('output', 'normal');
-			?>
-			<?php if($actual_view != 'normal') { ?>
-			<li class="item">
-				<?php $url_output['params']['output'] = 'normal'; ?>
-				<a class="view_normal" href="<?php echo Url::display ($url_output); ?>">
-					<?php echo Translate::t ('normal_view'); ?>
-				</a>
-			</li>
-			<?php } if($actual_view != 'reader') { ?>
-			<li class="item">
-				<?php $url_output['params']['output'] = 'reader'; ?>
-				<a class="view_normal" href="<?php echo Url::display ($url_output); ?>">
-					<?php echo Translate::t ('reader_view'); ?>
-				</a>
-			</li>
-			<?php } if($actual_view != 'global') { ?>
-			<li class="item">
-				<?php $url_output['params']['output'] = 'global'; ?>
-				<a class="view_normal" href="<?php echo Url::display ($url_output); ?>">
-					<?php echo Translate::t ('global_view'); ?>
-				</a>
-			</li>
-			<?php } ?>
+	<?php $url_output = $this->url; ?>
+	<div class="stick" id="nav_menu_views">
+		<?php $url_output['params']['output'] = 'normal'; ?>
+		<a class="view_normal btn <?php echo $actual_view == 'normal'? 'active' : ''; ?>" title="<?php echo _t('normal_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo _i("view-normal"); ?>
+		</a>
 
-			<li class="separator"></li>
+		<?php $url_output['params']['output'] = 'global'; ?>
+		<a class="view_global btn <?php echo $actual_view == 'global'? 'active' : ''; ?>" title="<?php echo _t('global_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo _i("view-global"); ?>
+		</a>
 
-			<li class="item">
-				<?php
-					$url_state = $url;
-					if ($this->state == 'not_read') {
-						$url_state['params']['state'] = 'all';
-				?>
-				<a class="print_all" href="<?php echo Url::display ($url_state); ?>">
-					<?php echo Translate::t ('show_all_articles'); ?>
-				</a>
-				<?php
-					} else {
-						$url_state['params']['state'] = 'not_read';
-				?>
-				<a class="print_non_read" href="<?php echo Url::display ($url_state); ?>">
-					<?php echo Translate::t ('show_not_reads'); ?>
-				</a>
-				<?php } ?>
-			</li>
+		<?php $url_output['params']['output'] = 'reader'; ?>
+		<a class="view_reader btn <?php echo $actual_view == 'reader'? 'active' : ''; ?>" title="<?php echo _t('reader_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo _i("view-reader"); ?>
+		</a>
+
+		<?php
+			$url_output['params']['output'] = 'rss';
+			if ($this->conf->token) {
+				$url_output['params']['token'] = $this->conf->token;
+			}
+		?>
+		<a class="view_rss btn" target="_blank" title="<?php echo _t('rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo _i('rss'); ?>
+		</a>
+	</div>
 
-			<li class="separator"></li>
+	<div class="item search">
+		<form action="<?php echo _url('index', 'index'); ?>" method="get">
+			<?php $search = Minz_Request::param('search', ''); ?>
+			<input type="search" name="search" class="extend" value="<?php echo $search; ?>" placeholder="<?php echo _t('search_short'); ?>" />
 
-			<li class="item">
-				<?php
-					$url_order = $url;
-					if ($this->order == 'low_to_high') {
-						$url_order['params']['order'] = 'high_to_low';
-				?>
-				<a href="<?php echo Url::display ($url_order); ?>">
-					<?php echo Translate::t ('older_first'); ?>
-				</a>
-				<?php
-					} else {
-						$url_order['params']['order'] = 'low_to_high';
-				?>
-				<a href="<?php echo Url::display ($url_order); ?>">
-					<?php echo Translate::t ('newer_first'); ?>
-				</a>
-				<?php } ?>
-			</li>
-		</ul>
+			<?php $get = Minz_Request::param('get', ''); ?>
+			<?php if($get != '') { ?>
+			<input type="hidden" name="get" value="<?php echo $get; ?>" />
+			<?php } ?>
+
+			<?php $order = Minz_Request::param('order', ''); ?>
+			<?php if($order != '') { ?>
+			<input type="hidden" name="order" value="<?php echo $order; ?>" />
+			<?php } ?>
+
+			<?php $state = Minz_Request::param('state', ''); ?>
+			<?php if($state != '') { ?>
+			<input type="hidden" name="state" value="<?php echo $state; ?>" />
+			<?php } ?>
+		</form>
 	</div>
+	
+	<?php
+		if ($this->order === 'DESC') {
+			$order = 'ASC';
+			$icon = 'up';
+			$title = 'older_first';
+		} else {
+			$order = 'DESC';
+			$icon = 'down';
+			$title = 'newer_first';
+		}
+		$url_order = $this->url;
+		$url_order['params']['order'] = $order;
+	?>
+	<a id="toggle-order" class="btn" href="<?php echo Minz_Url::display($url_order); ?>" title="<?php echo _t($title); ?>">
+		<?php echo _i($icon); ?>
+	</a>
+	
+	<?php if ($this->loginOk || Minz_Configuration::allowAnonymousRefresh()) { ?>
+	<a id="actualize" class="btn" href="<?php echo _url('feed', 'actualize'); ?>"><?php echo _i('refresh'); ?></a>
+	<?php } ?>
 </div>

+ 0 - 68
app/layout/persona.phtml

@@ -1,68 +0,0 @@
-<?php if (login_is_conf ($this->conf)) { ?>
-
-<?php
-	$mail = Session::param ('mail', 'null');
-	if ($mail != 'null') {
-		$mail = '\'' . $mail . '\'';
-	}
-?>
-
-<script type="text/javascript">
-url = "<?php echo Url::display (); ?>"
-login_url = "<?php echo Url::display (array ('a' => 'login')); ?>";
-logout_url = "<?php echo Url::display (array ('a' => 'logout')); ?>";
-currentUser = <?php echo $mail; ?>;
-
-$('a.signin').click(function() {
-	navigator.id.request();
-	return false;
-});
-
-$('a.signout').click(function() {
-	navigator.id.logout();
-	return false;
-});
-
-navigator.id.watch({
-	loggedInUser: currentUser,
-	onlogin: function(assertion) {
-		// A user has logged in! Here you need to:
-		// 1. Send the assertion to your backend for verification and to create a session.
-		// 2. Update your UI.
-		$.ajax ({
-			type: 'POST',
-			url: login_url,
-			data: {assertion: assertion},
-			success: function(res, status, xhr) {
-				var res_obj = jQuery.parseJSON(res);
-				
-				if (res_obj.status == 'failure') {
-					//alert (res_obj.reason);
-				} else if (res_obj.status == 'okay') {
-					location.href = url;
-				}
-			},
-			error: function(res, status, xhr) {
-				alert("login failure : " + res);
-			}
-		});
-	},
-	onlogout: function() {
-		// A user has logged out! Here you need to:
-		// Tear down the user's session by redirecting the user or making a call to your backend.
-		// Also, make sure loggedInUser will get set to null on the next page load.
-		// (That's a literal JavaScript null. Not false, 0, or undefined. null.)
-		$.ajax ({
-			type: 'POST',
-			url: logout_url,
-			success: function(res, status, xhr) {
-				location.href = url;
-			},
-			error: function(res, status, xhr) {
-				//alert("logout failure" + res);
-			}
-		});
-	}
-});
-</script>
-<?php } ?>

+ 0 - 325
app/models/Category.php

@@ -1,325 +0,0 @@
-<?php
-
-class Category extends Model {
-	private $id = false;
-	private $name;
-	private $color;
-	private $nbFeed = -1;
-	private $nbNotRead = -1;
-	private $feeds = null;
-
-	public function __construct ($name = '', $color = '#0062BE', $feeds = null) {
-		$this->_name ($name);
-		$this->_color ($color);
-		if (!empty($feeds)) {
-			$this->_feeds ($feeds);
-			$this->nbFeed = 0;
-			$this->nbNotRead = 0;
-			foreach ($feeds as $feed) {
-				$this->nbFeed++;
-				$this->nbNotRead += $feed->nbNotRead ();
-			}
-		}
-	}
-
-	public function id () {
-		if (!$this->id) {
-			return small_hash ($this->name . time () . Configuration::selApplication ());
-		} else {
-			return $this->id;
-		}
-	}
-	public function name () {
-		return $this->name;
-	}
-	public function color () {
-		return $this->color;
-	}
-	public function nbFeed () {
-		if ($this->nbFeed < 0) {
-		$catDAO = new CategoryDAO ();
-			$this->nbFeed = $catDAO->countFeed ($this->id ());
-		}
-
-		return $this->nbFeed;
-	}
-	public function nbNotRead () {
-		if ($this->nbNotRead < 0) {
-		$catDAO = new CategoryDAO ();
-			$this->nbNotRead = $catDAO->countNotRead ($this->id ());
-		}
-
-		return $this->nbNotRead;
-	}
-	public function feeds () {
-		if (is_null ($this->feeds)) {
-			$feedDAO = new FeedDAO ();
-			$this->feeds = $feedDAO->listByCategory ($this->id ());
-			$this->nbFeed = 0;
-			$this->nbNotRead = 0;
-			foreach ($this->feeds as $feed) {
-				$this->nbFeed++;
-				$this->nbNotRead += $feed->nbNotRead ();
-			}
-		}
-
-		return $this->feeds;
-	}
-
-	public function _id ($value) {
-		$this->id = $value;
-	}
-	public function _name ($value) {
-		$this->name = $value;
-	}
-	public function _color ($value) {
-		if (preg_match ('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
-			$this->color = $value;
-		} else {
-			$this->color = '#0062BE';
-		}
-	}
-	public function _feeds ($values) {
-		if (!is_array ($values)) {
-			$values = array ($values);
-		}
-
-		$this->feeds = $values;
-	}
-}
-
-class CategoryDAO extends Model_pdo {
-	public function addCategory ($valuesTmp) {
-		$sql = 'INSERT INTO ' . $this->prefix . 'category (id, name, color) VALUES(?, ?, ?)';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array (
-			$valuesTmp['id'],
-			$valuesTmp['name'],
-			$valuesTmp['color'],
-		);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function updateCategory ($id, $valuesTmp) {
-		$sql = 'UPDATE ' . $this->prefix . 'category SET name=?, color=? WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array (
-			$valuesTmp['name'],
-			$valuesTmp['color'],
-			$id
-		);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function deleteCategory ($id) {
-		$sql = 'DELETE FROM ' . $this->prefix . 'category WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array ($id);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function searchById ($id) {
-		$sql = 'SELECT * FROM ' . $this->prefix . 'category WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array ($id);
-
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		$cat = HelperCategory::daoToCategory ($res);
-
-		if (isset ($cat[0])) {
-			return $cat[0];
-		} else {
-			return false;
-		}
-	}
-	public function searchByName ($name) {
-		$sql = 'SELECT * FROM ' . $this->prefix . 'category WHERE name=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array ($name);
-
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		$cat = HelperCategory::daoToCategory ($res);
-
-		if (isset ($cat[0])) {
-			return $cat[0];
-		} else {
-			return false;
-		}
-	}
-
-	public function listCategories ($prePopulateFeeds = true) {	//TODO: Search code-base for places where $prePopulateFeeds should be false
-		if ($prePopulateFeeds) {
-			$sql = 'SELECT c.id as c_id, c.name as c_name, c.color as c_color, count(e.id) as nbNotRead, f.* '
-			     . 'FROM  ' . $this->prefix . 'category c '
-			     . 'LEFT OUTER JOIN ' . $this->prefix . 'feed f ON f.category = c.id '
-			     . 'LEFT OUTER JOIN  ' . $this->prefix . 'entry e ON e.id_feed = f.id AND e.is_read = 0 '
-			     . 'GROUP BY f.id '
-			     . 'ORDER BY c.name, f.name';
-			$stm = $this->bd->prepare ($sql);
-			$stm->execute ();
-			return HelperCategory::daoToCategoryPrepopulated ($stm->fetchAll (PDO::FETCH_ASSOC));
-		} else {
-			$sql = 'SELECT * FROM ' . $this->prefix . 'category ORDER BY name';
-			$stm = $this->bd->prepare ($sql);
-			$stm->execute ();
-			return HelperCategory::daoToCategory ($stm->fetchAll (PDO::FETCH_ASSOC));
-		}
-	}
-
-	public function getDefault () {
-		$sql = 'SELECT * FROM ' . $this->prefix . 'category WHERE id="000000"';
-		$stm = $this->bd->prepare ($sql);
-
-		$stm->execute ();
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		$cat = HelperCategory::daoToCategory ($res);
-
-		if (isset ($cat[0])) {
-			return $cat[0];
-		} else {
-			return false;
-		}
-	}
-	public function checkDefault () {
-		$def_cat = $this->searchById ('000000');
-
-		if ($def_cat === false) {
-			$cat = new Category (Translate::t ('default_category'));
-			$cat->_id ('000000');
-
-			$values = array (
-				'id' => $cat->id (),
-				'name' => $cat->name (),
-				'color' => $cat->color ()
-			);
-
-			$this->addCategory ($values);
-		}
-	}
-
-	public function count () {
-		$sql = 'SELECT COUNT(*) AS count FROM ' . $this->prefix . 'category';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-
-		return $res[0]['count'];
-	}
-
-	public function countFeed ($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM ' . $this->prefix . 'feed WHERE category=?';
-		$stm = $this->bd->prepare ($sql);
-		$values = array ($id);
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-
-		return $res[0]['count'];
-	}
-
-	public function countNotRead ($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM ' . $this->prefix . 'entry e INNER JOIN ' . $this->prefix . 'feed f ON e.id_feed = f.id WHERE category=? AND e.is_read=0';
-		$stm = $this->bd->prepare ($sql);
-		$values = array ($id);
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-
-		return $res[0]['count'];
-	}
-}
-
-class HelperCategory {
-	public static function findFeed($categories, $feed_id) {
-		foreach ($categories as $category) {
-			foreach ($category->feeds () as $feed) {
-				if ($feed->id () === $feed_id) {
-					return $feed;
-				}
-			}
-		}
-		return null;
-	}
-
-	public static function daoToCategoryPrepopulated ($listDAO) {
-		$list = array ();
-
-		if (!is_array ($listDAO)) {
-			$listDAO = array ($listDAO);
-		}
-
-		$previousLine = null;
-		$feedsDao = array();
-		$nbLinesMinus1 = count($listDAO) - 1;
-		for ($i = 0; $i <= $nbLinesMinus1; $i++) {
-			$line = $listDAO[$i];
-			$cat_id = $line['c_id'];
-			if (($i > 0) && (($cat_id !== $previousLine['c_id']) || ($i === $nbLinesMinus1))) {	//End of current category
-				if ($i === $nbLinesMinus1) {	//End of table
-					$feedsDao[] = $line;
-				}
-				$cat = new Category (
-					$previousLine['c_name'],
-					$previousLine['c_color'],
-					HelperFeed::daoToFeed ($feedsDao)
-				);
-				$cat->_id ($previousLine['c_id']);
-				$list[] = $cat;
-
-				$feedsDao = array();	//Prepare for next category
-				$previousLine = $line;
-				$feedsDao[] = $line;
-			} else {
-				$previousLine = $line;
-				$feedsDao[] = $line;
-			}
-		}
-
-		return $list;
-	}
-
-	public static function daoToCategory ($listDAO) {
-		$list = array ();
-
-		if (!is_array ($listDAO)) {
-			$listDAO = array ($listDAO);
-		}
-
-		foreach ($listDAO as $key => $dao) {
-			$cat = new Category (
-				$dao['name'],
-				$dao['color']
-			);
-			$cat->_id ($dao['id']);
-			$list[$key] = $cat;
-		}
-
-		return $list;
-	}
-}

+ 0 - 156
app/models/EntriesGetter.php

@@ -1,156 +0,0 @@
-<?php
-
-class EntriesGetter {
-	private $type = array (
-		'type' => 'all',
-		'id' => 'all'
-	);
-	private $state = 'all';
-	private $filter = array (
-		'words' => array (),
-		'tags' => array (),
-	);
-	private $order = 'high_to_low';
-	private $entries = array ();
-
-	private $nb = 1;
-	private $first = '';
-	private $next = '';
-
-	public function __construct ($type, $state, $filter, $order, $nb, $first = '') {
-		$this->_type ($type);
-		$this->_state ($state);
-		$this->_filter ($filter);
-		$this->_order ($order);
-		$this->nb = $nb;
-		$this->first = $first;
-	}
-
-	public function type () {
-		return $this->type;
-	}
-	public function state () {
-		return $this->state;
-	}
-	public function filter () {
-		return $this->filter;
-	}
-	public function order () {
-		return $this->order;
-	}
-	public function entries () {
-		return $this->entries;
-	}
-
-	public function _type ($value) {
-		if (!is_array ($value) ||
-		    !isset ($value['type']) ||
-		    !isset ($value['id'])) {
-			throw new EntriesGetterException ('Bad type line ' . __LINE__ . ' in file ' . __FILE__);
-		}
-
-		$type = $value['type'];
-		$id = $value['id'];
-
-		if ($type != 'all' && $type != 'favoris' && $type != 'public' && $type != 'c' && $type != 'f') {
-			throw new EntriesGetterException ('Bad type line ' . __LINE__ . ' in file ' . __FILE__);
-		}
-
-		if (($type == 'all' || $type == 'favoris' || $type == 'public') &&
-		    ($type != $id)) {
-			throw new EntriesGetterException ('Bad type line ' . __LINE__ . ' in file ' . __FILE__);
-		}
-
-		$this->type = $value;
-	}
-	public function _state ($value) {
-		if ($value != 'all' && $value != 'not_read' && $value != 'read') {
-			throw new EntriesGetterException ('Bad state line ' . __LINE__ . ' in file ' . __FILE__);
-		}
-
-		$this->state = $value;
-	}
-	public function _filter ($value) {
-		$value = trim ($value);
-		$terms = explode (' ', $value);
-
-		foreach ($terms as $word) {
-			if (!empty ($word) && $word[0] == '#' && isset ($word[1])) {
-				$tag = substr ($word, 1);
-				$this->filter['tags'][$tag] = $tag;
-			} elseif (!empty ($word)) {
-				$this->filter['words'][$word] = $word;
-			}
-		}
-	}
-	public function _order ($value) {
-		if ($value != 'high_to_low' && $value != 'low_to_high') {
-			throw new EntriesGetterException ('Bad order line ' . __LINE__ . ' in file ' . __FILE__);
-		}
-
-		$this->order = $value;
-	}
-
-	public function execute () {
-		$entryDAO = new EntryDAO ();
-
-		HelperEntry::$nb = $this->nb;	//TODO: Update: Now done in SQL
-		HelperEntry::$first = $this->first;	//TODO: Update: Now done in SQL
-		HelperEntry::$filter = $this->filter;
-
-		$sqlLimit = (empty ($this->filter['words']) && empty ($this->filter['tags'])) ? $this->nb : '';	//Disable SQL LIMIT optimisation during search	//TODO: Do better!
-
-		switch ($this->type['type']) {
-		case 'all':
-			list ($this->entries, $this->next) = $entryDAO->listEntries (
-				$this->state,
-				$this->order,
-				$this->first,
-				$sqlLimit
-			);
-			break;
-		case 'favoris':
-			list ($this->entries, $this->next) = $entryDAO->listFavorites (
-				$this->state,
-				$this->order,
-				$this->first,
-				$sqlLimit
-			);
-			break;
-		case 'public':
-			list ($this->entries, $this->next) = $entryDAO->listPublic (
-				$this->state,
-				$this->order,
-				$this->first,
-				$sqlLimit
-			);
-			break;
-		case 'c':
-			list ($this->entries, $this->next) = $entryDAO->listByCategory (
-				$this->type['id'],
-				$this->state,
-				$this->order,
-				$this->first,
-				$sqlLimit
-			);
-			break;
-		case 'f':
-			list ($this->entries, $this->next) = $entryDAO->listByFeed (
-				$this->type['id'],
-				$this->state,
-				$this->order,
-				$this->first,
-				$sqlLimit
-			);
-			break;
-		default:
-			throw new EntriesGetterException ('Bad type line ' . __LINE__ . ' in file ' . __FILE__);
-		}
-	}
-
-	public function getPaginator () {
-		$paginator = new RSSPaginator ($this->entries, $this->next);
-
-		return $paginator;
-	}
-}

+ 0 - 584
app/models/Entry.php

@@ -1,584 +0,0 @@
-<?php
-
-class Entry extends Model {
-
-	private $id = null;
-	private $guid;
-	private $title;
-	private $author;
-	private $content;
-	private $link;
-	private $date;
-	private $is_read;
-	private $is_favorite;
-	private $feed;
-	private $tags;
-
-	public function __construct ($feed = '', $guid = '', $title = '', $author = '', $content = '',
-	                             $link = '', $pubdate = 0, $is_read = false, $is_favorite = false) {
-		$this->_guid ($guid);
-		$this->_title ($title);
-		$this->_author ($author);
-		$this->_content ($content);
-		$this->_link ($link);
-		$this->_date ($pubdate);
-		$this->_isRead ($is_read);
-		$this->_isFavorite ($is_favorite);
-		$this->_feed ($feed);
-		$this->_tags (array ());
-	}
-
-	public function id () {
-		if(is_null($this->id)) {
-			return small_hash ($this->guid . Configuration::selApplication ());
-		} else {
-			return $this->id;
-		}
-	}
-	public function guid () {
-		return $this->guid;
-	}
-	public function title () {
-		return $this->title;
-	}
-	public function author () {
-		if (is_null ($this->author)) {
-			return '';
-		} else {
-			return $this->author;
-		}
-	}
-	public function content () {
-		return $this->content;
-	}
-	public function link () {
-		return $this->link;
-	}
-	public function date ($raw = false) {
-		if ($raw) {
-			return $this->date;
-		} else {
-			return timestamptodate ($this->date);
-		}
-	}
-	public function isRead () {
-		return $this->is_read;
-	}
-	public function isFavorite () {
-		return $this->is_favorite;
-	}
-	public function feed ($object = false) {
-		if ($object) {
-			$feedDAO = new FeedDAO ();
-			return $feedDAO->searchById ($this->feed);
-		} else {
-			return $this->feed;
-		}
-	}
-	public function tags ($inString = false) {
-		if ($inString) {
-			if (!empty ($this->tags)) {
-				return '#' . implode(' #', $this->tags);
-			} else {
-				return '';
-			}
-		} else {
-			return $this->tags;
-		}
-	}
-
-	public function _id ($value) {
-		$this->id = $value;
-	}
-	public function _guid ($value) {
-		$this->guid = $value;
-	}
-	public function _title ($value) {
-		$this->title = $value;
-	}
-	public function _author ($value) {
-		$this->author = $value;
-	}
-	public function _content ($value) {
-		$this->content = $value;
-	}
-	public function _link ($value) {
-		$this->link = $value;
-	}
-	public function _date ($value) {
-		if (is_int (intval ($value))) {
-			$this->date = $value;
-		} else {
-			$this->date = time ();
-		}
-	}
-	public function _isRead ($value) {
-		$this->is_read = $value;
-	}
-	public function _isFavorite ($value) {
-		$this->is_favorite = $value;
-	}
-	public function _feed ($value) {
-		$this->feed = $value;
-	}
-	public function _tags ($value) {
-		if (!is_array ($value)) {
-			$value = array ($value);
-		}
-
-		foreach ($value as $key => $t) {
-			if (!$t) {
-				unset ($value[$key]);
-			}
-		}
-
-		$this->tags = $value;
-	}
-
-	public function isDay ($day) {
-		$date = getdate ();
-		$today = mktime (0, 0, 0, $date['mon'], $date['mday'], $date['year']);
-		$yesterday = $today - 86400;
-
-		if ($day == Days::TODAY &&
-		    $this->date >= $today && $this->date < $today + 86400) {
-			return true;
-		} elseif ($day == Days::YESTERDAY &&
-		    $this->date >= $yesterday && $this->date < $yesterday + 86400) {
-			return true;
-		} elseif ($day == Days::BEFORE_YESTERDAY && $this->date < $yesterday) {
-			return true;
-		} else {
-			return false;
-		}
-	}
-
-	public function loadCompleteContent($pathEntries) {
-		// Gestion du contenu
-		// On cherche à récupérer les articles en entier... même si le flux ne le propose pas
-		if ($pathEntries) {
-			$entryDAO = new EntryDAO();
-			$entry = $entryDAO->searchByGuid($this->feed, $this->guid);
-
-			if($entry) {
-				// l'article existe déjà en BDD, en se contente de recharger ce contenu
-				$this->content = $entry->content();
-			} else {
-				try {
-					// l'article n'est pas en BDD, on va le chercher sur le site
-					$this->content = get_content_by_parsing(
-						$this->link(), $pathEntries
-					);
-				} catch (Exception $e) {
-					// rien à faire, on garde l'ancien contenu (requête a échoué)
-				}
-			}
-		}
-	}
-
-	public function toArray () {
-		return array (
-			'id' => $this->id (),
-			'guid' => $this->guid (),
-			'title' => $this->title (),
-			'author' => $this->author (),
-			'content' => $this->content (),
-			'link' => $this->link (),
-			'date' => $this->date (true),
-			'is_read' => $this->isRead (),
-			'is_favorite' => $this->isFavorite (),
-			'id_feed' => $this->feed (),
-			'tags' => $this->tags (true)
-		);
-	}
-}
-
-class EntryDAO extends Model_pdo {
-	public function addEntry ($valuesTmp) {
-		$sql = 'INSERT INTO ' . $this->prefix . 'entry(id, guid, title, author, content, link, date, is_read, is_favorite, id_feed, tags) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array (
-			$valuesTmp['id'],
-			$valuesTmp['guid'],
-			$valuesTmp['title'],
-			$valuesTmp['author'],
-			base64_encode (gzdeflate (serialize ($valuesTmp['content']))),
-			$valuesTmp['link'],
-			$valuesTmp['date'],
-			$valuesTmp['is_read'],
-			$valuesTmp['is_favorite'],
-			$valuesTmp['id_feed'],
-			$valuesTmp['tags'],
-		);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			if ((int)($info[0] / 1000) !== 23) {	//Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries
-				Minz_Log::record ('SQL error ' . $info[0] . ': ' . $info[1] . ' ' . $info[2], Minz_Log::NOTICE);	//TODO: Consider adding a Minz_Log::DEBUG level
-			}
-			return false;
-		}
-	}
-
-	public function updateEntry ($id, $valuesTmp) {
-		if (isset ($valuesTmp['content'])) {
-			$valuesTmp['content'] = base64_encode (gzdeflate (serialize ($valuesTmp['content'])));
-		}
-
-		$set = '';
-		foreach ($valuesTmp as $key => $v) {
-			$set .= $key . '=?, ';
-		}
-		$set = substr ($set, 0, -2);
-
-		$sql = 'UPDATE ' . $this->prefix . 'entry SET ' . $set . ' WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
-
-		foreach ($valuesTmp as $v) {
-			$values[] = $v;
-		}
-		$values[] = $id;
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function markReadEntries ($read, $dateMax = 0) {
-		$sql = 'UPDATE ' . $this->prefix . 'entry e INNER JOIN ' . $this->prefix . 'feed f ON e.id_feed = f.id SET is_read = ? WHERE priority > 0';
-
-		$values = array ($read);
-		if ($dateMax > 0) {
-			$sql .= ' AND date < ?';
-			$values[] = $dateMax;
-		}
-
-		$stm = $this->bd->prepare ($sql);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-	public function markReadCat ($id, $read, $dateMax = 0) {
-		$sql = 'UPDATE ' . $this->prefix . 'entry e INNER JOIN ' . $this->prefix . 'feed f ON e.id_feed = f.id SET is_read = ? WHERE category = ?';
-
-		$values = array ($read, $id);
-		if ($dateMax > 0) {
-			$sql .= ' AND date < ?';
-			$values[] = $dateMax;
-		}
-
-		$stm = $this->bd->prepare ($sql);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-	public function markReadFeed ($id, $read, $dateMax = 0) {
-		$sql = 'UPDATE ' . $this->prefix . 'entry SET is_read = ? WHERE id_feed = ?';
-
-		$values = array ($read, $id);
-		if ($dateMax > 0) {
-			$sql .= ' AND date < ?';
-			$values[] = $dateMax;
-		}
-
-		$stm = $this->bd->prepare ($sql);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function updateEntries ($valuesTmp) {
-		if (isset ($valuesTmp['content'])) {
-			$valuesTmp['content'] = base64_encode (gzdeflate (serialize ($valuesTmp['content'])));
-		}
-
-		$set = '';
-		foreach ($valuesTmp as $key => $v) {
-			$set .= $key . '=?, ';
-		}
-		$set = substr ($set, 0, -2);
-
-		$sql = 'UPDATE ' . $this->prefix . 'entry SET ' . $set;
-		$stm = $this->bd->prepare ($sql);
-
-		foreach ($valuesTmp as $v) {
-			$values[] = $v;
-		}
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function cleanOldEntries ($nb_month) {
-		$date = 60 * 60 * 24 * 30 * $nb_month;
-		$sql = 'DELETE e.* FROM ' . $this->prefix . 'entry e INNER JOIN ' . $this->prefix . 'feed f ON e.id_feed = f.id WHERE e.date <= ? AND e.is_favorite = 0 AND f.keep_history = 0';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array (
-			time () - $date
-		);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function searchByGuid ($feed_id, $id) {
-		// un guid est unique pour un flux donné
-		$sql = 'SELECT * FROM ' . $this->prefix . 'entry WHERE id_feed=? AND guid=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array (
-			$feed_id,
-			$id
-		);
-
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		list ($entry, $next) = HelperEntry::daoToEntry ($res);
-
-		if (isset ($entry[0])) {
-			return $entry[0];
-		} else {
-			return false;
-		}
-	}
-
-	public function searchById ($id) {
-		$sql = 'SELECT * FROM ' . $this->prefix . 'entry WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array ($id);
-
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		list ($entry, $next) = HelperEntry::daoToEntry ($res);
-
-		if (isset ($entry[0])) {
-			return $entry[0];
-		} else {
-			return false;
-		}
-	}
-
-	private function listWhere ($where, $state, $order, $limitFromId = '', $limitCount = '', $values = array ()) {
-		if ($state == 'not_read') {
-			$where .= ' AND is_read = 0';
-		} elseif ($state == 'read') {
-			$where .= ' AND is_read = 1';
-		}
-		if (!empty($limitFromId)) {	//TODO: Consider using LPAD(e.date, 11)	//CONCAT is for cases when many entries have the same date
-			$where .= ' AND CONCAT(e.date, e.id) ' . ($order === 'low_to_high' ? '<=' : '>=') . ' (SELECT CONCAT(s.date, s.id) FROM ' . $this->prefix . 'entry s WHERE s.id = "' . $limitFromId . '")';
-		}
-
-		if ($order == 'low_to_high') {
-			$order = ' DESC';
-		} else {
-			$order = '';
-		}
-
-		$sql = 'SELECT e.* FROM ' . $this->prefix . 'entry e'
-		     . ' INNER JOIN  ' . $this->prefix . 'feed f ON e.id_feed = f.id' . $where
-		     . ' ORDER BY e.date' . $order . ', e.id' . $order;
-
-		if (!empty($limitCount)) {
-			$sql .= ' LIMIT ' . ($limitCount + 2);	//TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
-		}
-
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ($values);
-
-		return HelperEntry::daoToEntry ($stm->fetchAll (PDO::FETCH_ASSOC));
-	}
-	public function listEntries ($state, $order = 'high_to_low', $limitFromId = '', $limitCount = '') {
-		return $this->listWhere (' WHERE priority > 0', $state, $order, $limitFromId, $limitCount);
-	}
-	public function listFavorites ($state, $order = 'high_to_low', $limitFromId = '', $limitCount = '') {
-		return $this->listWhere (' WHERE is_favorite = 1', $state, $order, $limitFromId, $limitCount);
-	}
-	public function listPublic ($state, $order = 'high_to_low', $limitFromId = '', $limitCount = '') {
-		return $this->listWhere (' WHERE is_public = 1', $state, $order, $limitFromId, $limitCount);
-	}
-	public function listByCategory ($cat, $state, $order = 'high_to_low', $limitFromId = '', $limitCount = '') {
-		return $this->listWhere (' WHERE category = ?', $state, $order, $limitFromId, $limitCount, array ($cat));
-	}
-	public function listByFeed ($feed, $state, $order = 'high_to_low', $limitFromId = '', $limitCount = '') {
-		return $this->listWhere (' WHERE id_feed = ?', $state, $order, $limitFromId, $limitCount, array ($feed));
-	}
-
-	public function countUnreadRead () {
-		$sql = 'SELECT is_read, COUNT(*) AS count FROM ' . $this->prefix . 'entry e INNER JOIN ' . $this->prefix . 'feed f ON e.id_feed = f.id WHERE priority > 0 GROUP BY is_read';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-
-		$readUnread = array('unread' => 0, 'read' => 0);
-		foreach ($res as $line) {
-			switch (intval($line['is_read'])) {
-				case 0: $readUnread['unread'] = intval($line['count']); break;
-				case 1: $readUnread['read'] = intval($line['count']); break;
-			}
-		}
-		return $readUnread;
-	}
-	public function count () {	//Deprecated: use countUnreadRead() instead
-		$unreadRead = $this->countUnreadRead ();	//This makes better use of caching
-		return $unreadRead['unread'] + $unreadRead['read'];
-	}
-	public function countNotRead () {	//Deprecated: use countUnreadRead() instead
-		$unreadRead = $this->countUnreadRead ();	//This makes better use of caching
-		return $unreadRead['unread'];
-	}
-
-	public function countUnreadReadFavorites () {
-		$sql = 'SELECT is_read, COUNT(*) AS count FROM ' . $this->prefix . 'entry WHERE is_favorite=1 GROUP BY is_read';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		$readUnread = array('unread' => 0, 'read' => 0);
-		foreach ($res as $line) {
-			switch (intval($line['is_read'])) {
-				case 0: $readUnread['unread'] = intval($line['count']); break;
-				case 1: $readUnread['read'] = intval($line['count']); break;
-			}
-		}
-		return $readUnread;
-	}
-
-	public function countFavorites () {	//Deprecated: use countUnreadReadFavorites() instead
-		$unreadRead = $this->countUnreadReadFavorites ();	//This makes better use of caching
-		return $unreadRead['unread'] + $unreadRead['read'];
-	}
-
-	public function optimizeTable() {
-		$sql = 'OPTIMIZE TABLE ' . $this->prefix . 'entry';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-	}
-}
-
-class HelperEntry {
-	public static $nb = 1;
-	public static $first = '';
-
-	public static $filter = array (
-		'words' => array (),
-		'tags' => array (),
-	);
-
-	public static function daoToEntry ($listDAO) {
-		$list = array ();
-
-		if (!is_array ($listDAO)) {
-			$listDAO = array ($listDAO);
-		}
-
-		$count = 0;
-		$first_is_found = false;
-		$break_after = false;
-		$next = '';
-		foreach ($listDAO as $key => $dao) {
-			$dao['content'] = unserialize (gzinflate (base64_decode ($dao['content'])));
-			$dao['tags'] = preg_split('/[\s#]/', $dao['tags']);
-
-			if (self::tagsMatchEntry ($dao) &&
-			    self::searchMatchEntry ($dao)) {
-				if ($break_after) {
-					$next = $dao['id'];
-					break;
-				}
-				if ($first_is_found || $dao['id'] == self::$first || self::$first == '') {
-					$list[$key] = self::createEntry ($dao);
-
-					$count++;
-					$first_is_found = true;	//TODO: Update: Now done in SQL
-				}
-				if ($count >= self::$nb) {
-					$break_after = true;
-				}
-			}
-		}
-
-		unset ($listDAO);
-
-		return array ($list, $next);
-	}
-
-	private static function createEntry ($dao) {
-		$entry = new Entry (
-			$dao['id_feed'],
-			$dao['guid'],
-			$dao['title'],
-			$dao['author'],
-			$dao['content'],
-			$dao['link'],
-			$dao['date'],
-			$dao['is_read'],
-			$dao['is_favorite']
-		);
-
-		$entry->_tags ($dao['tags']);
-
-		if (isset ($dao['id'])) {
-			$entry->_id ($dao['id']);
-		}
-
-		return $entry;
-	}
-
-	private static function tagsMatchEntry ($dao) {
-		$tags = self::$filter['tags'];
-		foreach ($tags as $tag) {
-			if (!in_array ($tag, $dao['tags'])) {
-				return false;
-			}
-		}
-
-		return true;
-	}
-	private static function searchMatchEntry ($dao) {
-		$words = self::$filter['words'];
-
-		foreach ($words as $word) {
-			$word = strtolower ($word);
-			if (strpos (strtolower ($dao['title']), $word) === false &&
-			    strpos (strtolower ($dao['content']), $word) === false &&
-			    strpos (strtolower ($dao['link']), $word) === false) {
-				return false;
-			}
-		}
-
-		return true;
-	}
-}

+ 0 - 19
app/models/Exception/FeedException.php

@@ -1,19 +0,0 @@
-<?php
-
-class FeedException extends Exception {
-	public function __construct ($message) {
-		parent::__construct ($message);
-	}
-}
-
-class BadUrlException extends FeedException {
-	public function __construct ($url) {
-		parent::__construct ('`' . $url . '` is not a valid URL');
-	}
-}
-
-class OpmlException extends FeedException {
-	public function __construct ($name_file) {
-		parent::__construct ('OPML file is invalid');
-	}
-}

+ 0 - 541
app/models/Feed.php

@@ -1,541 +0,0 @@
-<?php
-
-class Feed extends Model {
-	private $id = null;
-	private $url;
-	private $category = '000000';
-	private $nbEntries = -1;
-	private $nbNotRead = -1;
-	private $entries = null;
-	private $name = '';
-	private $website = '';
-	private $description = '';
-	private $lastUpdate = 0;
-	private $priority = 10;
-	private $pathEntries = '';
-	private $httpAuth = '';
-	private $error = false;
-	private $keep_history = false;
-
-	public function __construct ($url) {
-		$this->_url ($url);
-	}
-
-	public function id () {
-		if(is_null($this->id)) {
-			return small_hash ($this->url . Configuration::selApplication ());
-		} else {
-			return $this->id;
-		}
-	}
-	public function url () {
-		return $this->url;
-	}
-	public function category () {
-		return $this->category;
-	}
-	public function entries () {
-		if (!is_null ($this->entries)) {
-			return $this->entries;
-		} else {
-			return array ();
-		}
-	}
-	public function name () {
-		return $this->name;
-	}
-	public function website () {
-		return $this->website;
-	}
-	public function description () {
-		return $this->description;
-	}
-	public function lastUpdate () {
-		return $this->lastUpdate;
-	}
-	public function priority () {
-		return $this->priority;
-	}
-	public function pathEntries () {
-		return $this->pathEntries;
-	}
-	public function httpAuth ($raw = true) {
-		if ($raw) {
-			return $this->httpAuth;
-		} else {
-			$pos_colon = strpos ($this->httpAuth, ':');
-			$user = substr ($this->httpAuth, 0, $pos_colon);
-			$pass = substr ($this->httpAuth, $pos_colon + 1);
-
-			return array (
-				'username' => $user,
-				'password' => $pass
-			);
-		}
-	}
-	public function inError () {
-		return $this->error;
-	}
-	public function keepHistory () {
-		return $this->keep_history;
-	}
-	public function nbEntries () {
-		if ($this->nbEntries < 0) {
-			$feedDAO = new FeedDAO ();
-			$this->nbEntries = $feedDAO->countEntries ($this->id ());
-		}
-
-		return $this->nbEntries;
-	}
-	public function nbNotRead () {
-		if ($this->nbNotRead < 0) {
-			$feedDAO = new FeedDAO ();
-			$this->nbNotRead = $feedDAO->countNotRead ($this->id ());
-		}
-
-		return $this->nbNotRead;
-	}
-	public function favicon () {
-		$file = '/data/favicons/' . $this->id () . '.ico';
-
-		$favicon_url = Url::display ($file);
-		if (!file_exists (PUBLIC_PATH . $file)) {
-			$favicon_url = dowload_favicon ($this->website (), $this->id ());
-		}
-
-		return $favicon_url;
-	}
-
-	public function _id ($value) {
-		$this->id = $value;
-	}
-	public function _url ($value) {
-		if (!is_null ($value) && !preg_match ('#^https?://#', $value)) {
-			$value = 'http://' . $value;
-		}
-
-		if (!is_null ($value) && filter_var ($value, FILTER_VALIDATE_URL)) {
-			$this->url = $value;
-		} else {
-			throw new BadUrlException ($value);
-		}
-	}
-	public function _category ($value) {
-		$this->category = $value;
-	}
-	public function _name ($value) {
-		if (is_null ($value)) {
-			$value = '';
-		}
-		$this->name = $value;
-	}
-	public function _website ($value) {
-		if (is_null ($value)) {
-			$value = '';
-		}
-		$this->website = $value;
-	}
-	public function _description ($value) {
-		if (is_null ($value)) {
-			$value = '';
-		}
-		$this->description = $value;
-	}
-	public function _lastUpdate ($value) {
-		$this->lastUpdate = $value;
-	}
-	public function _priority ($value) {
-		if (!is_int (intval ($value))) {
-			$value = 10;
-		}
-		$this->priority = $value;
-	}
-	public function _pathEntries ($value) {
-		$this->pathEntries = $value;
-	}
-	public function _httpAuth ($value) {
-		$this->httpAuth = $value;
-	}
-	public function _error ($value) {
-		if ($value) {
-			$value = true;
-		} else {
-			$value = false;
-		}
-		$this->error = $value;
-	}
-	public function _keepHistory ($value) {
-		if ($value) {
-			$value = true;
-		} else {
-			$value = false;
-		}
-		$this->keep_history = $value;
-	}
-	public function _nbNotRead ($value) {
-		if (!is_int ($value)) {
-			$value = -1;
-		}
-
-		$this->nbNotRead = intval ($value);
-	}
-
-	public function load () {
-		if (!is_null ($this->url)) {
-			if (CACHE_PATH === false) {
-				throw new FileNotExistException (
-					'CACHE_PATH',
-					MinzException::ERROR
-				);
-			} else {
-				$feed = new SimplePie ();
-				$url = str_replace ('&amp;', '&', $this->url);
-				if ($this->httpAuth != '') {
-					$url = preg_replace ('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url);
-				}
-
-				$feed->set_feed_url ($url);
-				$feed->set_cache_location (CACHE_PATH);
-				$feed->strip_htmltags (array (
-					'base', 'blink', 'body', 'doctype',
-					'font', 'form', 'frame', 'frameset', 'html',
-					'input', 'marquee', 'meta', 'noscript',
-					'param', 'script', 'style'
-				));
-				$feed->init ();
-
-				if ($feed->error ()) {
-					throw new FeedException ($feed->error);
-				}
-
-				// si on a utilisé l'auto-discover, notre url va avoir changé
-				$subscribe_url = $feed->subscribe_url ();
-				if (!is_null ($subscribe_url) && $subscribe_url != $this->url) {
-					if ($this->httpAuth != '') {
-						// on enlève les id si authentification HTTP
-						$subscribe_url = preg_replace ('#((.+)://)((.+)@)(.+)#', '${1}${5}', $subscribe_url);
-					}
-					$this->_url ($subscribe_url);
-				}
-
-				if (empty($this->name)) {	// May come from OPML
-					$title = $feed->get_title ();
-					$this->_name (!is_null ($title) ? $title : $this->url);
-				}
-
-				$this->_website ($feed->get_link ());
-				$this->_description ($feed->get_description ());
-
-				// et on charge les articles du flux
-				$this->loadEntries ($feed);
-			}
-		}
-	}
-	private function loadEntries ($feed) {
-		$entries = array ();
-
-		foreach ($feed->get_items () as $item) {
-			$title = strip_tags($item->get_title ());
-			$author = $item->get_author ();
-			$link = $item->get_permalink ();
-			$date = strtotime ($item->get_date ());
-
-			// gestion des tags (catégorie == tag)
-			$tags_tmp = $item->get_categories ();
-			$tags = array ();
-			if (!is_null ($tags_tmp)) {
-				foreach ($tags_tmp as $tag) {
-					$tags[] = $tag->get_label ();
-				}
-			}
-
-			$content = $item->get_content ();
-
-			$entry = new Entry (
-				$this->id (),
-				$item->get_id (),
-				!is_null ($title) ? $title : '',
-				!is_null ($author) ? $author->name : '',
-				!is_null ($content) ? $content : '',
-				!is_null ($link) ? $link : '',
-				$date ? $date : time ()
-			);
-			$entry->_tags ($tags);
-			// permet de récupérer le contenu des flux tronqués
-			$entry->loadCompleteContent($this->pathEntries());
-
-			$entries[$entry->id ()] = $entry;
-		}
-
-		$this->entries = $entries;
-	}
-}
-
-class FeedDAO extends Model_pdo {
-	public function addFeed ($valuesTmp) {
-		$sql = 'INSERT INTO ' . $this->prefix . 'feed (id, url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history) VALUES(?, ?, ?, ?, ?, ?, ?, 10, ?, 0, 0)';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array (
-			$valuesTmp['id'],
-			$valuesTmp['url'],
-			$valuesTmp['category'],
-			$valuesTmp['name'],
-			$valuesTmp['website'],
-			$valuesTmp['description'],
-			$valuesTmp['lastUpdate'],
-			base64_encode ($valuesTmp['httpAuth']),
-		);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function updateFeed ($id, $valuesTmp) {
-		$set = '';
-		foreach ($valuesTmp as $key => $v) {
-			$set .= $key . '=?, ';
-
-			if ($key == 'httpAuth') {
-				$valuesTmp[$key] = base64_encode ($v);
-			}
-		}
-		$set = substr ($set, 0, -2);
-
-		$sql = 'UPDATE ' . $this->prefix . 'feed SET ' . $set . ' WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
-
-		foreach ($valuesTmp as $v) {
-			$values[] = $v;
-		}
-		$values[] = $id;
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function updateLastUpdate ($id) {
-		$sql = 'UPDATE ' . $this->prefix . 'feed SET lastUpdate=?, error=0 WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array (
-			time (),
-			$id
-		);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function isInError ($id) {
-		$sql = 'UPDATE ' . $this->prefix . 'feed SET error=1 WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array (
-			$id
-		);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function changeCategory ($idOldCat, $idNewCat) {
-		$catDAO = new CategoryDAO ();
-		$newCat = $catDAO->searchById ($idNewCat);
-		if (!$newCat) {
-			$newCat = $catDAO->getDefault ();
-		}
-
-		$sql = 'UPDATE ' . $this->prefix . 'feed SET category=? WHERE category=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array (
-			$newCat->id (),
-			$idOldCat
-		);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function deleteFeed ($id) {
-		$sql = 'DELETE FROM ' . $this->prefix . 'feed WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array ($id);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-	public function deleteFeedByCategory ($id) {
-		$sql = 'DELETE FROM ' . $this->prefix . 'feed WHERE category=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array ($id);
-
-		if ($stm && $stm->execute ($values)) {
-			return true;
-		} else {
-			$info = $stm->errorInfo();
-			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
-			return false;
-		}
-	}
-
-	public function searchById ($id) {
-		$sql = 'SELECT * FROM ' . $this->prefix . 'feed WHERE id=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array ($id);
-
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		$feed = HelperFeed::daoToFeed ($res);
-
-		if (isset ($feed[$id])) {
-			return $feed[$id];
-		} else {
-			return false;
-		}
-	}
-	public function searchByUrl ($url) {
-		$sql = 'SELECT * FROM ' . $this->prefix . 'feed WHERE url=?';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array ($url);
-
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-		$feed = current (HelperFeed::daoToFeed ($res));
-
-		if (isset ($feed)) {
-			return $feed;
-		} else {
-			return false;
-		}
-	}
-
-	public function listFeeds () {
-		$sql = 'SELECT * FROM ' . $this->prefix . 'feed ORDER BY name';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-
-		return HelperFeed::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
-	}
-
-	public function listFeedsOrderUpdate () {
-		$sql = 'SELECT * FROM ' . $this->prefix . 'feed ORDER BY lastUpdate';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-
-		return HelperFeed::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
-	}
-
-	public function listByCategory ($cat) {
-		$sql = 'SELECT * FROM ' . $this->prefix . 'feed WHERE category=? ORDER BY name';
-		$stm = $this->bd->prepare ($sql);
-
-		$values = array ($cat);
-
-		$stm->execute ($values);
-
-		return HelperFeed::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
-	}
-
-	public function count () {
-		$sql = 'SELECT COUNT(*) AS count FROM ' . $this->prefix . 'feed';
-		$stm = $this->bd->prepare ($sql);
-		$stm->execute ();
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-
-		return $res[0]['count'];
-	}
-
-	public function countEntries ($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM ' . $this->prefix . 'entry WHERE id_feed=?';
-		$stm = $this->bd->prepare ($sql);
-		$values = array ($id);
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-
-		return $res[0]['count'];
-	}
-	public function countNotRead ($id) {
-		$sql = 'SELECT COUNT(*) AS count FROM ' . $this->prefix . 'entry WHERE is_read=0 AND id_feed=?';
-		$stm = $this->bd->prepare ($sql);
-		$values = array ($id);
-		$stm->execute ($values);
-		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
-
-		return $res[0]['count'];
-	}
-}
-
-class HelperFeed {
-	public static function daoToFeed ($listDAO) {
-		$list = array ();
-
-		if (!is_array ($listDAO)) {
-			$listDAO = array ($listDAO);
-		}
-
-		foreach ($listDAO as $key => $dao) {
-			if (empty ($dao['url'])) {
-				continue;
-			}
-			if (isset ($dao['id'])) {
-				$key = $dao['id'];
-			}
-
-			$list[$key] = new Feed ($dao['url']);
-			$list[$key]->_category ($dao['category']);
-			$list[$key]->_name ($dao['name']);
-			$list[$key]->_website ($dao['website']);
-			$list[$key]->_description ($dao['description']);
-			$list[$key]->_lastUpdate ($dao['lastUpdate']);
-			$list[$key]->_priority ($dao['priority']);
-			$list[$key]->_pathEntries ($dao['pathEntries']);
-			$list[$key]->_httpAuth (base64_decode ($dao['httpAuth']));
-			$list[$key]->_error ($dao['error']);
-			$list[$key]->_keepHistory ($dao['keep_history']);
-			if (isset ($dao['nbNotRead'])) {
-				$list[$key]->_nbNotRead ($dao['nbNotRead']);
-			}
-			if (isset ($dao['id'])) {
-				$list[$key]->_id ($dao['id']);
-			}
-		}
-
-		return $list;
-	}
-}

+ 0 - 47
app/models/Log.php

@@ -1,47 +0,0 @@
-<?php
-
-class Log_Model extends Model {
-	private $date;
-	private $level;
-	private $information;
-
-	public function date () {
-		return $this->date;
-	}
-	public function level () {
-		return $this->level;
-	}
-	public function info () {
-		return $this->information;
-	}
-	public function _date ($date) {
-		$this->date = $date;
-	}
-	public function _level ($level) {
-		$this->level = $level;
-	}
-	public function _info ($information) {
-		$this->information = $information;
-	}
-}
-
-class LogDAO extends Model_txt {
-	public function __construct () {
-		parent::__construct (LOG_PATH . '/application.log', 'r+');
-	}
-	
-	public function lister () {
-		$logs = array ();
-
-		$i = 0;
-		while (($line = $this->readLine ()) !== false) {
-			$logs[$i] = new Log_Model ();
-			$logs[$i]->_date (preg_replace ("'\[(.*?)\] \[(.*?)\] --- (.*?)'U", "\\1", $line));
-			$logs[$i]->_level (preg_replace ("'\[(.*?)\] \[(.*?)\] --- (.*?)'U", "\\2", $line));
-			$logs[$i]->_info (preg_replace ("'\[(.*?)\] \[(.*?)\] --- (.*?)'U", "\\3", $line));
-			$i++;
-		}
-
-		return $logs;
-	}
-}

+ 0 - 321
app/models/RSSConfiguration.php

@@ -1,321 +0,0 @@
-<?php
-
-class RSSConfiguration extends Model {
-	private $available_languages = array (
-		'en' => 'English',
-		'fr' => 'Français',
-	);
-	private $language;
-	private $posts_per_page;
-	private $view_mode;
-	private $default_view;
-	private $display_posts;
-	private $onread_jump_next; 
-	private $lazyload;
-	private $sort_order;
-	private $old_entries;
-	private $shortcuts = array ();
-	private $mail_login = '';
-	private $mark_when = array ();
-	private $url_shaarli = '';
-	private $theme;
-	private $anon_access;
-	private $token;
-	private $auto_load_more;
-	
-	public function __construct () {
-		$confDAO = new RSSConfigurationDAO ();
-		$this->_language ($confDAO->language);
-		$this->_postsPerPage ($confDAO->posts_per_page);
-		$this->_viewMode ($confDAO->view_mode);
-		$this->_defaultView ($confDAO->default_view);
-		$this->_displayPosts ($confDAO->display_posts);
-		$this->_onread_jump_next ($confDAO->onread_jump_next); 
-		$this->_lazyload ($confDAO->lazyload);
-		$this->_sortOrder ($confDAO->sort_order);
-		$this->_oldEntries ($confDAO->old_entries);
-		$this->_shortcuts ($confDAO->shortcuts);
-		$this->_mailLogin ($confDAO->mail_login);
-		$this->_markWhen ($confDAO->mark_when);
-		$this->_urlShaarli ($confDAO->url_shaarli);
-		$this->_theme ($confDAO->theme);
-		$this->_anonAccess ($confDAO->anon_access);
-		$this->_token ($confDAO->token);
-		$this->_autoLoadMore ($confDAO->auto_load_more);
-	}
-	
-	public function availableLanguages () {
-		return $this->available_languages;
-	}
-	public function language () {
-		return $this->language;
-	}
-	public function postsPerPage () {
-		return $this->posts_per_page;
-	}
-	public function viewMode () {
-		return $this->view_mode;
-	}
-	public function defaultView () {
-		return $this->default_view;
-	}
-	public function displayPosts () {
-		return $this->display_posts;
-	}
-	public function onread_jump_next () {
-		return $this->onread_jump_next;
-	}
-	public function lazyload () {
-		return $this->lazyload;
-	}
-	public function sortOrder () {
-		return $this->sort_order;
-	}
-	public function oldEntries () {
-		return $this->old_entries;
-	}
-	public function shortcuts () {
-		return $this->shortcuts;
-	}
-	public function mailLogin () {
-		return $this->mail_login;
-	}
-	public function markWhen () {
-		return $this->mark_when;
-	}
-	public function markWhenArticle () {
-		return $this->mark_when['article'];
-	}
-	public function markWhenSite () {
-		return $this->mark_when['site'];
-	}
-	public function markWhenScroll () {
-		return $this->mark_when['scroll'];
-	}
-	public function urlShaarli () {
-		return $this->url_shaarli;
-	}
-	public function theme () {
-		return $this->theme;
-	}
-	public function anonAccess () {
-		return $this->anon_access;
-	}
-	public function token () {
-		return $this->token;
-	}
-	public function autoLoadMore () {
-		return $this->auto_load_more;
-	}
-
-	public function _language ($value) {
-		if (!isset ($this->available_languages[$value])) {
-			$value = 'en';
-		}
-		$this->language = $value;
-	}
-	public function _postsPerPage ($value) {
-		if (is_int (intval ($value)) && $value > 0) {
-			$this->posts_per_page = $value;
-		} else {
-			$this->posts_per_page = 10;
-		}
-	}
-	public function _viewMode ($value) {
-		if ($value == 'global' || $value == 'reader') {
-			$this->view_mode = $value;
-		} else {
-			$this->view_mode = 'normal';
-		}
-	}
-	public function _defaultView ($value) {
-		if ($value == 'not_read') {
-			$this->default_view = 'not_read';
-		} else {
-			$this->default_view = 'all';
-		}
-	}
-	public function _displayPosts ($value) {
-		if ($value == 'yes') {
-			$this->display_posts = 'yes';
-		} else {
-			$this->display_posts = 'no';
-		}
-	}
-	public function _onread_jump_next ($value) {
-		if ($value == 'no') {
-			$this->onread_jump_next = 'no';
-		} else {
-			$this->onread_jump_next = 'yes';
-		}
-	}
-	public function _lazyload ($value) {
-		if ($value == 'no') {
-			$this->lazyload = 'no';
-		} else {
-			$this->lazyload = 'yes';
-		}
-	}
-	public function _sortOrder ($value) {
-		if ($value == 'high_to_low') {
-			$this->sort_order = 'high_to_low';
-		} else {
-			$this->sort_order = 'low_to_high';
-		}
-	}
-	public function _oldEntries ($value) {
-		if (is_int (intval ($value)) && $value > 0) {
-			$this->old_entries = $value;
-		} else {
-			$this->old_entries = 3;
-		}
-	}
-	public function _shortcuts ($values) {
-		foreach ($values as $key => $value) {
-			$this->shortcuts[$key] = $value;
-		}
-	}
-	public function _mailLogin ($value) {
-		if (filter_var ($value, FILTER_VALIDATE_EMAIL)) {
-			$this->mail_login = $value;
-		} elseif ($value == false) {
-			$this->mail_login = false;
-		}
-	}
-	public function _markWhen ($values) {
-		if(!isset($values['article'])) {
-			$values['article'] = 'yes';
-		}
-		if(!isset($values['site'])) {
-			$values['site'] = 'yes';
-		}
-		if(!isset($values['scroll'])) {
-			$values['scroll'] = 'yes';
-		}
-
-		$this->mark_when['article'] = $values['article'];
-		$this->mark_when['site'] = $values['site'];
-		$this->mark_when['scroll'] = $values['scroll'];
-	}
-	public function _urlShaarli ($value) {
-		$this->url_shaarli = '';
-		if (filter_var ($value, FILTER_VALIDATE_URL)) {
-			$this->url_shaarli = $value;
-		}
-	}
-	public function _theme ($value) {
-		$this->theme = $value;
-	}
-	public function _anonAccess ($value) {
-		if ($value == 'yes') {
-			$this->anon_access = 'yes';
-		} else {
-			$this->anon_access = 'no';
-		}
-	}
-	public function _token ($value) {
-		$this->token = $value;
-	}
-	public function _autoLoadMore ($value) {
-		if ($value == 'yes') {
-			$this->auto_load_more = 'yes';
-		} else {
-			$this->auto_load_more = 'no';
-		}
-	}
-}
-
-class RSSConfigurationDAO extends Model_array {
-	public $language = 'en';
-	public $posts_per_page = 20;
-	public $view_mode = 'normal';
-	public $default_view = 'not_read';
-	public $display_posts = 'no';
-	public $onread_jump_next = 'yes';
-	public $lazyload = 'yes';
-	public $sort_order = 'low_to_high';
-	public $old_entries = 3;
-	public $shortcuts = array (
-		'mark_read' => 'r',
-		'mark_favorite' => 'f',
-		'go_website' => 'space',
-		'next_entry' => 'j',
-		'prev_entry' => 'k'
-	);
-	public $mail_login = '';
-	public $mark_when = array (
-		'article' => 'yes',
-		'site' => 'yes',
-		'scroll' => 'no'
-	);
-	public $url_shaarli = '';
-	public $theme = 'default';
-	public $anon_access = 'no';
-	public $token = '';
-	public $auto_load_more = 'no';
-
-	public function __construct () {
-		parent::__construct (PUBLIC_PATH . '/data/Configuration.array.php');
-
-		// TODO : simplifier ce code, une boucle for() devrait suffir !
-		if (isset ($this->array['language'])) {
-			$this->language = $this->array['language'];
-		}
-		if (isset ($this->array['posts_per_page'])) {
-			$this->posts_per_page = $this->array['posts_per_page'];
-		}
-		if (isset ($this->array['view_mode'])) {
-			$this->view_mode = $this->array['view_mode'];
-		}
-		if (isset ($this->array['default_view'])) {
-			$this->default_view = $this->array['default_view'];
-		}
-		if (isset ($this->array['display_posts'])) {
-			$this->display_posts = $this->array['display_posts'];
-		}
-		if (isset ($this->array['onread_jump_next'])) {
-			$this->onread_jump_next = $this->array['onread_jump_next'];
-		}
-		if (isset ($this->array['lazyload'])) {
-			$this->lazyload = $this->array['lazyload'];
-		}
-		if (isset ($this->array['sort_order'])) {
-			$this->sort_order = $this->array['sort_order'];
-		}
-		if (isset ($this->array['old_entries'])) {
-			$this->old_entries = $this->array['old_entries'];
-		}
-		if (isset ($this->array['shortcuts'])) {
-			$this->shortcuts = $this->array['shortcuts'];
-		}
-		if (isset ($this->array['mail_login'])) {
-			$this->mail_login = $this->array['mail_login'];
-		}
-		if (isset ($this->array['mark_when'])) {
-			$this->mark_when = $this->array['mark_when'];
-		}
-		if (isset ($this->array['url_shaarli'])) {
-			$this->url_shaarli = $this->array['url_shaarli'];
-		}
-		if (isset ($this->array['theme'])) {
-			$this->theme = $this->array['theme'];
-		}
-		if (isset ($this->array['anon_access'])) {
-			$this->anon_access = $this->array['anon_access'];
-		}
-		if (isset ($this->array['token'])) {
-			$this->token = $this->array['token'];
-		}
-		if (isset ($this->array['auto_load_more'])) {
-			$this->auto_load_more = $this->array['auto_load_more'];
-		}
-	}
-	
-	public function update ($values) {
-		foreach ($values as $key => $value) {
-			$this->array[$key] = $value;
-		}
-	
-		$this->writeFile($this->array);
-	}
-}

+ 0 - 29
app/models/RSSPaginator.php

@@ -1,29 +0,0 @@
-<?php
-
-// Un système de pagination beaucoup plus simple que Paginator
-// mais mieux adapté à nos besoins
-class RSSPaginator {
-	private $items = array ();
-	private $next = '';
-
-	public function __construct ($items, $next) {
-		$this->items = $items;
-		$this->next = $next;
-	}
-
-	public function isEmpty () {
-		return empty ($this->items);
-	}
-
-	public function items () {
-		return $this->items;
-	}
-
-	public function render ($view, $getteur) {
-		$view = APP_PATH . '/views/helpers/'.$view;
-
-		if (file_exists ($view)) {
-			include ($view);
-		}
-	}
-}

+ 0 - 47
app/models/RSSThemes.php

@@ -1,47 +0,0 @@
-<?php
-
-class RSSThemes extends Model {
-	private static $themes_dir = '/themes';
-
-	private static $list = array();
-
-	public static function init() {
-		$basedir = PUBLIC_PATH . self::$themes_dir;
-
-		$themes_list = array_diff(
-			scandir($basedir),
-			array('..', '.')
-		);
-
-		foreach ($themes_list as $theme_dir) {
-			$json_filename = $basedir . '/' . $theme_dir . '/metadata.json';
-			if(file_exists($json_filename)) {
-				$content = file_get_contents($json_filename);
-				$res = json_decode($content, true);
-
-				if($res &&
-					isset($res['name']) &&
-					isset($res['author']) &&
-					isset($res['description']) &&
-					isset($res['version']) &&
-					isset($res['files']) && is_array($res['files'])) {
-					$theme = $res;
-					$theme['path'] = $theme_dir;
-					self::$list[$theme_dir] = $theme;
-				}
-			}
-		}
-	}
-
-	public static function get() {
-		return self::$list;
-	}
-
-	public static function get_infos($theme_id) {
-		if (isset(self::$list[$theme_id])) {
-			return self::$list[$theme_id];
-		}
-
-		return false;
-	}
-}

+ 79 - 0
app/views/configure/archiving.phtml

@@ -0,0 +1,79 @@
+<?php $this->partial('aside_configure'); ?>
+
+<div class="post">
+	<a href="<?php echo _url('index', 'index'); ?>"><?php echo Minz_Translate::t('back_to_rss_feeds'); ?></a>
+
+	<form method="post" action="<?php echo _url('configure', 'archiving'); ?>">
+		<legend><?php echo Minz_Translate::t('archiving_configuration'); ?></legend>
+		<p><?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('archiving_configuration_help'); ?></p>
+
+		<div class="form-group">
+			<label class="group-name" for="old_entries"><?php echo Minz_Translate::t('delete_articles_every'); ?></label>
+			<div class="group-controls">
+				<input type="number" id="old_entries" name="old_entries" min="1" max="1200" value="<?php echo $this->conf->old_entries; ?>" /> <?php echo Minz_Translate::t('month'); ?>
+				  <a class="btn confirm" href="<?php echo _url('entry', 'purge'); ?>"><?php echo Minz_Translate::t('purge_now'); ?></a>
+			</div>
+		</div>
+		<div class="form-group">
+			<label class="group-name" for="keep_history_default"><?php echo Minz_Translate::t('keep_history'), ' ', Minz_Translate::t('by_feed'); ?></label>
+			<div class="group-controls">
+				<select class="number" name="keep_history_default" id="keep_history_default" required="required"><?php
+					foreach (array('' => '', 0 => '0', 10 => '10', 50 => '50', 100 => '100', 500 => '500', 1000 => '1 000', 5000 => '5 000', 10000 => '10 000', -1 => '∞') as $v => $t) {
+						echo '<option value="' . $v . ($this->conf->keep_history_default == $v ? '" selected="selected' : '') . '">' . $t . ' </option>';
+					}
+				?></select> (<?php echo Minz_Translate::t('by_default'); ?>)
+			</div>
+		</div>
+		<div class="form-group">
+			<label class="group-name" for="ttl_default"><?php echo Minz_Translate::t('ttl'); ?></label>
+			<div class="group-controls">
+				<select class="number" name="ttl_default" id="ttl_default" required="required"><?php
+					$found = false;
+					foreach (array(1200 => '20min', 1500 => '25min', 1800 => '30min', 2700 => '45min',
+					                3600 => '1h', 5400 => '1.5h', 7200 => '2h', 10800 => '3h', 14400 => '4h', 18800 => '5h', 21600 => '6h', 25200 => '7h', 28800 => '8h',
+					                36000 => '10h', 43200 => '12h', 64800 => '18h',
+					                86400 => '1d', 129600 => '1.5d', 172800 => '2d', 259200 => '3d', 345600 => '4d', 432000 => '5d', 518400 => '6d',
+					                604800 => '1wk', -1 => '∞') as $v => $t) {
+						echo '<option value="' . $v . ($this->conf->ttl_default == $v ? '" selected="selected' : '') . '">' . $t . '</option>';
+						if ($this->conf->ttl_default == $v) {
+							$found = true;
+						}
+					}
+					if (!$found) {
+						echo '<option value="' . intval($this->conf->ttl_default) . '" selected="selected">' . intval($this->conf->ttl_default) . 's</option>';
+					}
+				?></select> (<?php echo Minz_Translate::t('by_default'); ?>)
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
+			</div>
+		</div>
+	</form>
+
+	<form method="post" action="<?php echo _url('entry', 'optimize'); ?>">
+		<legend><?php echo Minz_Translate::t ('advanced'); ?></legend>
+
+		<div class="form-group">
+		<p class="group-name"><?php echo Minz_Translate::t('current_user'); ?></p>
+			<div class="group-controls">
+				<p><?php echo formatNumber($this->nb_total), ' ', Minz_Translate::t('articles'), ', ', formatBytes($this->size_user); ?></p>
+				<input type="hidden" name="optimiseDatabase" value="1" />
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('optimize_bdd'); ?></button>
+				<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('optimize_todo_sometimes'); ?>
+			</div>
+		</div>
+
+		<?php if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { ?>
+		<div class="form-group">
+			<p class="group-name"><?php echo Minz_Translate::t('users'); ?></p>
+			<div class="group-controls">
+				<p><?php echo formatBytes($this->size_total); ?></p>
+			</div>
+		</div>
+		<?php } ?>
+	</form>
+</div>

+ 27 - 15
app/views/configure/categorize.phtml

@@ -1,43 +1,55 @@
-<?php $this->partial ('aside_configure'); ?>
+<?php $this->partial ('aside_feed'); ?>
 
 <div class="post">
-	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Translate::t ('back_to_rss_feeds'); ?></a>
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
 
 	<form method="post" action="<?php echo _url ('configure', 'categorize'); ?>">
-		<legend><?php echo Translate::t ('categories_management'); ?> - <a href="<?php echo _url ('configure', 'feed'); ?>"><?php echo Translate::t ('rss_feed_management'); ?></a></legend>
+		<legend><?php echo Minz_Translate::t ('categories_management'); ?></legend>
 
-		<p class="alert alert-warn"><?php echo Translate::t ('feeds_moved_category_deleted', $this->defaultCategory->name ()); ?></p>
+		<p class="alert alert-warn"><?php echo Minz_Translate::t ('feeds_moved_category_deleted', $this->defaultCategory->name ()); ?></p>
 
 		<?php $i = 0; foreach ($this->categories as $cat) { $i++; ?>
 		<div class="form-group">
 			<label class="group-name" for="cat_<?php echo $cat->id (); ?>">
-				<?php echo Translate::t ('category_number', $i); ?>
+				<?php echo Minz_Translate::t ('category_number', $i); ?>
 			</label>
 			<div class="group-controls">
-				<input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" />
-				<a class="confirm" href="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Translate::t ('ask_empty'); ?></a> (<?php echo Translate::t ('number_feeds', $cat->nbFeed ()); ?>)
-				<?php if ($cat->id () == $this->defaultCategory->id ()) { ?>
-				<i class="icon i_help"></i> <?php echo Translate::t ('can_not_be_deleted'); ?>
+				<div class="stick">
+					<input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" />
+
+					<?php if ($cat->nbFeed () > 0) { ?>
+					<a class="btn" href="<?php echo _url('index', 'index', 'get', 'c_' . $cat->id ()); ?>">
+						<?php echo _i('link'); ?>
+					</a>
+					<button formaction="<?php echo _url('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"
+					        class="btn btn-attention confirm"
+					        data-str-confirm="<?php echo _t('confirm_action_feed_cat'); ?>"
+					        type="submit"><?php echo _t('ask_empty'); ?></button>
+					<?php } ?>
+				</div>
+				(<?php echo Minz_Translate::t ('number_feeds', $cat->nbFeed ()); ?>)
+
+				<?php if ($cat->id () === $this->defaultCategory->id ()) { ?>
+				<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t ('can_not_be_deleted'); ?>
 				<?php } ?>
+
 				<input type="hidden" name="ids[]" value="<?php echo $cat->id (); ?>" />
 			</div>
 		</div>
 		<?php } ?>
 
 		<div class="form-group">
-			<label class="group-name" for="new_category"><?php echo Translate::t ('add_category'); ?></label>
+			<label class="group-name" for="new_category"><?php echo Minz_Translate::t ('add_category'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="new_category" name="new_category" placeholder="<?php echo Translate::t ('new_category'); ?>" />
+				<input type="text" id="new_category" name="new_category" placeholder="<?php echo Minz_Translate::t ('new_category'); ?>" />
 			</div>
 		</div>
 
 		<div class="form-group form-actions">
 			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo Translate::t ('save'); ?></button>
-				<button type="reset" class="btn"><?php echo Translate::t ('cancel'); ?></button>
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
 			</div>
 		</div>
 	</form>
 </div>
-
-<?php $this->renderHelper ('confirm_action_script'); ?>

+ 71 - 147
app/views/configure/display.phtml

@@ -1,184 +1,108 @@
 <?php $this->partial ('aside_configure'); ?>
 
 <div class="post">
-	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Translate::t ('back_to_rss_feeds'); ?></a>
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
 
 	<form method="post" action="<?php echo _url ('configure', 'display'); ?>">
-		<legend><?php echo Translate::t ('general_configuration'); ?></legend>
+		<legend><?php echo Minz_Translate::t ('display_configuration'); ?></legend>
 
 		<div class="form-group">
-			<label class="group-name" for="language"><?php echo Translate::t ('language'); ?></label>
+			<label class="group-name" for="language"><?php echo Minz_Translate::t ('language'); ?></label>
 			<div class="group-controls">
 				<select name="language" id="language">
 				<?php $languages = $this->conf->availableLanguages (); ?>
 				<?php foreach ($languages as $short => $lib) { ?>
-				<option value="<?php echo $short; ?>"<?php echo $this->conf->language () == $short ? ' selected="selected"' : ''; ?>><?php echo $lib; ?></option>
+				<option value="<?php echo $short; ?>"<?php echo $this->conf->language === $short ? ' selected="selected"' : ''; ?>><?php echo $lib; ?></option>
 				<?php } ?>
 				</select>
 			</div>
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="theme"><?php echo Translate::t ('theme'); ?></label>
+			<label class="group-name" for="theme"><?php echo Minz_Translate::t ('theme'); ?></label>
 			<div class="group-controls">
-				<select name="theme" id="theme">
-				<?php foreach ($this->themes as $theme) { ?>
-				<option value="<?php echo $theme['path']; ?>"<?php echo $this->conf->theme () == $theme['path'] ? ' selected="selected"' : ''; ?>>
-					<?php echo $theme['name'] . ' ' . Translate::t ('by') . ' ' . $theme['author']; ?> 
-				</option>
-				<?php } ?>
-				</select>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name" for="old_entries"><?php echo Translate::t ('delete_articles_every'); ?></label>
-			<div class="group-controls">
-				<input type="number" id="old_entries" name="old_entries" value="<?php echo $this->conf->oldEntries (); ?>" /> <?php echo Translate::t ('month'); ?>
+				<select name="theme" id="theme" required=""><?php
+					$found = false;
+					foreach ($this->themes as $theme) {
+						?><option value="<?php echo $theme['id']; ?>"<?php if ($this->conf->theme === $theme['id']) { echo ' selected="selected"'; $found = true; } ?>><?php
+							echo $theme['name'] . ' — ' . Minz_Translate::t ('by') . ' ' . $theme['author'];
+						?></option><?php
+					}
+					if (!$found) {
+						?><option selected="selected"></option><?php
+					}
+				?></select>
 			</div>
 		</div>
 
+		<?php $width = $this->conf->content_width; ?>
 		<div class="form-group">
-			<label class="group-name" for="mail_login"><?php echo Translate::t ('persona_connection_email'); ?></label>
-			<?php $mail = $this->conf->mailLogin (); ?>
-			<div class="group-controls">
-				<input type="email" id="mail_login" name="mail_login" value="<?php echo $mail ? $mail : ''; ?>" placeholder="<?php echo Translate::t ('blank_to_disable'); ?>" />
-				<noscript><b><?php echo Translate::t ('javascript_should_be_activated'); ?></b></noscript>
-				<label class="checkbox" for="anon_access">
-					<input type="checkbox" name="anon_access" id="anon_access" value="yes"<?php echo $this->conf->anonAccess () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('allow_anonymous'); ?>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name" for="token"><?php echo Translate::t ('auth_token'); ?></label>
-			<?php $token = $this->conf->token (); ?>
-			<div class="group-controls">
-				<input type="text" id="token" name="token" value="<?php echo $token; ?>"  placeholder="<?php echo Translate::t ('blank_to_disable'); ?>"/>
-				<i class="icon i_help"></i> <?php echo Translate::t('explain_token', Url::display(), $token); ?>
-			</div>
-		</div>
-	
-		<legend><?php echo Translate::t ('reading_configuration'); ?></legend>
-
-		<div class="form-group">
-			<label class="group-name" for="posts_per_page"><?php echo Translate::t ('articles_per_page'); ?></label>
-			<div class="group-controls">
-				<input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo $this->conf->postsPerPage (); ?>" />
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name" for="sort_order"><?php echo Translate::t ('sort_order'); ?></label>
-			<div class="group-controls">
-				<select name="sort_order" id="sort_order">
-					<option value="low_to_high"<?php echo $this->conf->sortOrder () == 'low_to_high' ? ' selected="selected"' : ''; ?>><?php echo Translate::t ('newer_first'); ?></option>
-					<option value="high_to_low"<?php echo $this->conf->sortOrder () == 'high_to_low' ? ' selected="selected"' : ''; ?>><?php echo Translate::t ('older_first'); ?></option>
-				</select>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name" for="view_mode"><?php echo Translate::t ('default_view'); ?></label>
-			<div class="group-controls">
-				<select name="view_mode" id="view_mode">
-					<option value="normal"<?php echo $this->conf->viewMode () == 'normal' ? ' selected="selected"' : ''; ?>><?php echo Translate::t ('normal_view'); ?></option>
-					<option value="reader"<?php echo $this->conf->viewMode () == 'reader' ? ' selected="selected"' : ''; ?>><?php echo Translate::t ('reader_view'); ?></option>
-					<option value="global"<?php echo $this->conf->viewMode () == 'global' ? ' selected="selected"' : ''; ?>><?php echo Translate::t ('global_view'); ?></option>
+			<label class="group-name" for="content_width"><?php echo Minz_Translate::t('content_width'); ?></label>
+			<div class="group-controls">
+				<select name="content_width" id="content_width" required="">
+					<option value="thin" <?php echo $width === 'thin'? 'selected="selected"' : ''; ?>>
+						<?php echo Minz_Translate::t('width_thin'); ?>
+					</option>
+					<option value="medium" <?php echo $width === 'medium'? 'selected="selected"' : ''; ?>>
+						<?php echo Minz_Translate::t('width_medium'); ?>
+					</option>
+					<option value="large" <?php echo $width === 'large'? 'selected="selected"' : ''; ?>>
+						<?php echo Minz_Translate::t('width_large'); ?>
+					</option>
+					<option value="no_limit" <?php echo $width === 'no_limit'? 'selected="selected"' : ''; ?>>
+						<?php echo Minz_Translate::t('width_no_limit'); ?>
+					</option>
 				</select>
-				<label class="radio" for="radio_all">
-					<input type="radio" name="default_view" id="radio_all" value="all"<?php echo $this->conf->defaultView () == 'all' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('show_all_articles'); ?>
-				</label>
-				<label class="radio" for="radio_not_read">
-					<input type="radio" name="default_view" id="radio_not_read" value="not_read"<?php echo $this->conf->defaultView () == 'not_read' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('show_not_reads'); ?>
-				</label>
 			</div>
 		</div>
 
 		<div class="form-group">
-			<div class="group-controls">
-				<label class="checkbox" for="auto_load_more">
-					<input type="checkbox" name="auto_load_more" id="auto_load_more" value="yes"<?php echo $this->conf->autoLoadMore () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('auto_load_more'); ?>
-					<?php echo $this->conf->displayPosts () == 'no' ? '<noscript> - <b>' . Translate::t ('javascript_should_be_activated') . '</b></noscript>' : ''; ?>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<div class="group-controls">
-				<label class="checkbox" for="display_posts">
-					<input type="checkbox" name="display_posts" id="display_posts" value="yes"<?php echo $this->conf->displayPosts () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('display_articles_unfolded'); ?>
-					<?php echo $this->conf->displayPosts () == 'no' ? '<noscript> - <b>' . Translate::t ('javascript_should_be_activated') . '</b></noscript>' : ''; ?>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<div class="group-controls">
-				<label class="checkbox" for="lazyload">
-					<input type="checkbox" name="lazyload" id="lazyload" value="yes"<?php echo $this->conf->lazyload () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('img_with_lazyload'); ?>
-					<?php echo $this->conf->lazyload () == 'yes' ? '<noscript> - <b>' . Translate::t ('javascript_should_be_activated') . '</b></noscript>' : ''; ?>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('auto_read_when'); ?></label>
-			<div class="group-controls">
-				<label class="checkbox" for="check_open_article">
-					<input type="checkbox" name="mark_open_article" id="check_open_article" value="yes"<?php echo $this->conf->markWhenArticle () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('article_selected'); ?>
-				</label>
-				<label class="checkbox" for="check_open_site">
-					<input type="checkbox" name="mark_open_site" id="check_open_site" value="yes"<?php echo $this->conf->markWhenSite () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('article_open_on_website'); ?>
-				</label>
-				<label class="checkbox" for="check_scroll">
-					<input type="checkbox" name="mark_scroll" id="check_scroll" value="yes"<?php echo $this->conf->markWhenScroll () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('scroll'); ?>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('after_onread'); ?></label>
-			<div class="group-controls">
-				<label class="checkbox" for="onread_jump_next">
-					<input type="checkbox" name="onread_jump_next" id="onread_jump_next" value="yes"<?php echo $this->conf->onread_jump_next () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('jump_next'); ?>
-				</label>
-			</div>
-		</div>
-
-		<legend><?php echo Translate::t ('sharing'); ?></legend>
-		<div class="form-group">
-			<label class="group-name" for="shaarli"><?php echo Translate::t ('your_shaarli'); ?></label>
-			<div class="group-controls">
-				<input type="text" id="shaarli" name="shaarli" value="<?php echo $this->conf->urlShaarli (); ?>" placeholder="<?php echo Translate::t ('blank_to_disable'); ?>"/>
-			</div>
-		</div>
-
-		<legend><?php echo Translate::t ('advanced'); ?></legend>
+			<label class="group-name" for="theme"><?php echo Minz_Translate::t ('article_icons'); ?></label>
+			<table>
+				<thead>
+					<tr>
+						<th> </th>
+						<th title="<?php echo Minz_Translate::t ('mark_read'); ?>"><?php echo FreshRSS_Themes::icon('read'); ?></th>
+						<th title="<?php echo Minz_Translate::t ('mark_favorite'); ?>"><?php echo FreshRSS_Themes::icon('bookmark'); ?></th>
+						<th><?php echo Minz_Translate::t ('sharing'); ?></th>
+						<th><?php echo Minz_Translate::t ('related_tags'); ?></th>
+						<th><?php echo Minz_Translate::t ('publication_date'); ?></th>
+						<th><?php echo FreshRSS_Themes::icon('link'); ?></th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr>
+						<th><?php echo Minz_Translate::t ('top_line'); ?></th>
+						<td><input type="checkbox" name="topline_read" value="1"<?php echo $this->conf->topline_read ? ' checked="checked"' : ''; ?> /></td>
+						<td><input type="checkbox" name="topline_favorite" value="1"<?php echo $this->conf->topline_favorite ? ' checked="checked"' : ''; ?> /></td>
+						<td><input type="checkbox" disabled="disabled" /></td>
+						<td><input type="checkbox" disabled="disabled" /></td>
+						<td><input type="checkbox" name="topline_date" value="1"<?php echo $this->conf->topline_date ? ' checked="checked"' : ''; ?> /></td>
+						<td><input type="checkbox" name="topline_link" value="1"<?php echo $this->conf->topline_link ? ' checked="checked"' : ''; ?> /></td>
+					</tr><tr>
+						<th><?php echo Minz_Translate::t ('bottom_line'); ?></th>
+						<td><input type="checkbox" name="bottomline_read" value="1"<?php echo $this->conf->bottomline_read ? ' checked="checked"' : ''; ?> /></td>
+						<td><input type="checkbox" name="bottomline_favorite" value="1"<?php echo $this->conf->bottomline_favorite ? ' checked="checked"' : ''; ?> /></td>
+						<td><input type="checkbox" name="bottomline_sharing" value="1"<?php echo $this->conf->bottomline_sharing ? ' checked="checked"' : ''; ?> /></td>
+						<td><input type="checkbox" name="bottomline_tags" value="1"<?php echo $this->conf->bottomline_tags ? ' checked="checked"' : ''; ?> /></td>
+						<td><input type="checkbox" name="bottomline_date" value="1"<?php echo $this->conf->bottomline_date ? ' checked="checked"' : ''; ?> /></td>
+						<td><input type="checkbox" name="bottomline_link" value="1"<?php echo $this->conf->bottomline_link ? ' checked="checked"' : ''; ?> /></td>
+					</tr>
+				</tbody>
+			</table><br />
+		</div>
+		
 		<div class="form-group">
-			<label class="group-name"></label>
+			<label class="group-name" for="posts_per_page"><?php echo Minz_Translate::t ('html5_notif_timeout'); ?></label>
 			<div class="group-controls">
-				<a class="btn" href="<?php echo _url('entry', 'optimize'); ?>">
-					<?php echo Translate::t('optimize_bdd'); ?>
-				</a>
-				<i class="icon i_help"></i> <?php echo Translate::t('optimize_todo_sometimes'); ?>
+				<input type="number" id="html5_notif_timeout" name="html5_notif_timeout" value="<?php echo $this->conf->html5_notif_timeout; ?>" /> <?php echo Minz_Translate::t ('seconds_(0_means_no_timeout)'); ?>
 			</div>
 		</div>
 
 		<div class="form-group form-actions">
 			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo Translate::t ('save'); ?></button>
-				<button type="reset" class="btn"><?php echo Translate::t ('cancel'); ?></button>
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
 			</div>
 		</div>
 	</form>

+ 113 - 50
app/views/configure/feed.phtml

@@ -2,62 +2,55 @@
 
 <?php if ($this->flux) { ?>
 <div class="post">
-	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Translate::t ('back_to_rss_feeds'); ?></a> <?php echo Translate::t ('or'); ?> <a href="<?php echo _url ('index', 'index', 'get', 'f_' . $this->flux->id ()); ?>"><?php echo Translate::t ('filter'); ?></a>
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a> <?php echo Minz_Translate::t ('or'); ?> <a href="<?php echo _url ('index', 'index', 'get', 'f_' . $this->flux->id ()); ?>"><?php echo Minz_Translate::t ('filter'); ?></a>
 
 	<h1><?php echo $this->flux->name (); ?></h1>
 	<?php echo $this->flux->description (); ?>
 
+	<?php $nbEntries = $this->flux->nbEntries (); ?>
+
 	<?php if ($this->flux->inError ()) { ?>
-	<p class="alert alert-error"><span class="alert-head"><?php echo Translate::t ('damn'); ?></span> <?php echo Translate::t ('feed_in_error'); ?></p>
+	<p class="alert alert-error"><span class="alert-head"><?php echo Minz_Translate::t ('damn'); ?></span> <?php echo Minz_Translate::t ('feed_in_error'); ?></p>
+	<?php } elseif ($nbEntries === 0) { ?>
+	<p class="alert alert-warn"><?php echo Minz_Translate::t ('feed_empty'); ?></p>
 	<?php } ?>
 
-	<form method="post" action="<?php echo _url ('configure', 'feed', 'id', $this->flux->id ()); ?>">
-		<legend><?php echo Translate::t ('informations'); ?></legend>
-		<div class="form-group">
-			<label class="group-name" for="name"><?php echo Translate::t ('title'); ?></label>
-			<div class="group-controls">
-				<input type="text" name="name" id="name" value="<?php echo $this->flux->name () ; ?>" />
-			</div>
-		</div>
+	<form method="post" action="<?php echo _url ('configure', 'feed', 'id', $this->flux->id ()); ?>" autocomplete="off">
+		<legend><?php echo Minz_Translate::t ('informations'); ?></legend>
 		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('website_url'); ?></label>
+			<label class="group-name" for="name"><?php echo Minz_Translate::t ('title'); ?></label>
 			<div class="group-controls">
-				<span class="control">
-					<?php echo $this->flux->website (); ?>
-					<a target="_blank" href="<?php echo $this->flux->website (); ?>"><i class="icon i_link"></i></a>
-				</span>
+				<input type="text" name="name" id="name" class="extend" value="<?php echo $this->flux->name () ; ?>" />
 			</div>
 		</div>
 		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('feed_url'); ?></label>
+			<label class="group-name" for="description"><?php echo Minz_Translate::t ('feed_description'); ?></label>
 			<div class="group-controls">
-				<span class="control">
-					<?php echo $this->flux->url (); ?>
-					<a target="_blank" href="<?php echo $this->flux->url (); ?>"><i class="icon i_link"></i></a>
-				</span>
+				<textarea name="description" id="description"><?php echo htmlspecialchars($this->flux->description(), ENT_NOQUOTES, 'UTF-8'); ?></textarea>
 			</div>
 		</div>
 		<div class="form-group">
-			<label class="group-name"></label>
+			<label class="group-name" for="website"><?php echo Minz_Translate::t ('website_url'); ?></label>
 			<div class="group-controls">
-				<a class="btn" href="<?php echo _url ('feed', 'actualize', 'id', $this->flux->id ()); ?>">
-					<i class="icon i_refresh"></i> <?php echo Translate::t('actualize'); ?>
-				</a>
+				<div class="stick">
+					<input type="text" name="website" id="website" class="extend" value="<?php echo $this->flux->website (); ?>" />
+					<a class="btn" target="_blank" href="<?php echo $this->flux->website (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+				</div>
 			</div>
 		</div>
 		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('number_articles'); ?></label>
+			<label class="group-name" for="url"><?php echo Minz_Translate::t ('feed_url'); ?></label>
 			<div class="group-controls">
-				<span class="control"><?php echo $this->flux->nbEntries (); ?></span>
-				<label class="checkbox" for="keep_history">
-					<input type="checkbox" name="keep_history" id="keep_history" value="yes"<?php echo $this->flux->keepHistory () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('keep_history'); ?>
-				</label>
+				<div class="stick">
+					<input type="text" name="url" id="url" class="extend" value="<?php echo $this->flux->url (); ?>" />
+					<a class="btn" target="_blank" href="<?php echo $this->flux->url (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+				</div>
+
+				<a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->flux->url (); ?>"><?php echo Minz_Translate::t ('feed_validator'); ?></a>
 			</div>
 		</div>
-
 		<div class="form-group">
-			<label class="group-name" for="category"><?php echo Translate::t ('category'); ?></label>
+			<label class="group-name" for="category"><?php echo Minz_Translate::t ('category'); ?></label>
 			<div class="group-controls">
 				<select name="category" id="category">
 				<?php foreach ($this->categories as $cat) { ?>
@@ -66,54 +59,124 @@
 				</option>
 				<?php } ?>
 				</select>
-				<a href="<?php echo _url ('configure', 'categorize'); ?>"><?php echo Translate::t ('categories_management'); ?></a>
 			</div>
 		</div>
-
-		<legend><?php echo Translate::t ('advanced'); ?></legend>
 		<div class="form-group">
-			<label class="group-name" for="priority"><?php echo Translate::t ('show_in_all_flux'); ?></label>
+			<label class="group-name" for="priority"><?php echo Minz_Translate::t ('show_in_all_flux'); ?></label>
 			<div class="group-controls">
 				<label class="checkbox" for="priority">
 					<input type="checkbox" name="priority" id="priority" value="10"<?php echo $this->flux->priority () > 0 ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('yes'); ?>
+					<?php echo Minz_Translate::t ('yes'); ?>
 				</label>
 			</div>
 		</div>
 		<div class="form-group">
-			<label class="group-name" for="path_entries"><?php echo Translate::t ('css_path_on_website'); ?></label>
 			<div class="group-controls">
-				<input type="text" name="path_entries" id="path_entries" value="<?php echo $this->flux->pathEntries (); ?>" placeholder="<?php echo Translate::t ('blank_to_disable'); ?>" />
-				<i class="icon i_help"></i> <?php echo Translate::t ('retrieve_truncated_feeds'); ?>
+				<a href="<?php echo _url('stats', 'repartition', 'id', $this->flux->id()); ?>">
+					<?php echo _i('stats'); ?> <?php echo _t('stats'); ?>
+				</a>
+			</div>
+		</div>
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button class="btn btn-important"><?php echo _t('save'); ?></button>
+				<button class="btn btn-attention confirm"
+				        data-str-confirm="<?php echo _t('confirm_action_feed_cat'); ?>"
+				        formaction="<?php echo _url('feed', 'delete', 'id', $this->flux->id ()); ?>"
+				        formmethod="post"><?php echo _t('delete'); ?></button>
 			</div>
 		</div>
 
+		<legend><?php echo Minz_Translate::t ('archiving_configuration'); ?></legend>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<div class="stick">
+					<input type="text" value="<?php echo _t('number_articles', $nbEntries); ?>" disabled="disabled" />
+					<a class="btn" href="<?php echo _url('feed', 'actualize', 'id', $this->flux->id ()); ?>">
+						<?php echo _i('refresh'); ?> <?php echo _t('actualize'); ?>
+					</a>
+				</div>
+			</div>
+		</div>
+		<div class="form-group">
+			<label class="group-name" for="keep_history"><?php echo Minz_Translate::t ('keep_history'); ?></label>
+			<div class="group-controls">
+				<select class="number" name="keep_history" id="keep_history" required="required"><?php
+					foreach (array('' => '', -2 => Minz_Translate::t('by_default'), 0 => '0', 10 => '10', 50 => '50', 100 => '100', 500 => '500', 1000 => '1 000', 5000 => '5 000', 10000 => '10 000', -1 => '∞') as $v => $t) {
+						echo '<option value="' . $v . ($this->flux->keepHistory() === $v ? '" selected="selected' : '') . '">' . $t . '</option>';
+					}
+				?></select>
+			</div>
+		</div>
+		<div class="form-group">
+			<label class="group-name" for="ttl"><?php echo Minz_Translate::t('ttl'); ?></label>
+			<div class="group-controls">
+				<select class="number" name="ttl" id="ttl" required="required"><?php
+					$found = false;
+					foreach (array(-2 => Minz_Translate::t('by_default'), 900 => '15min', 1200 => '20min', 1500 => '25min', 1800 => '30min', 2700 => '45min',
+					                3600 => '1h', 5400 => '1.5h', 7200 => '2h', 10800 => '3h', 14400 => '4h', 18800 => '5h', 21600 => '6h', 25200 => '7h', 28800 => '8h',
+					                36000 => '10h', 43200 => '12h', 64800 => '18h',
+					                86400 => '1d', 129600 => '1.5d', 172800 => '2d', 259200 => '3d', 345600 => '4d', 432000 => '5d', 518400 => '6d',
+					                604800 => '1wk', 1209600 => '2wk', 1814400 => '3wk', 2419200 => '4wk', 2629744 => '1mo', -1 => '∞') as $v => $t) {
+						echo '<option value="' . $v . ($this->flux->ttl() === $v ? '" selected="selected' : '') . '">' . $t . '</option>';
+						if ($this->flux->ttl() == $v) {
+							$found = true;
+						}
+					}
+					if (!$found) {
+						echo '<option value="' . intval($this->flux->ttl()) . '" selected="selected">' . intval($this->flux->ttl()) . 's</option>';
+					}
+				?></select>
+			</div>
+		</div>
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
+				<button class="btn btn-attention confirm" formmethod="post" formaction="<?php echo Minz_Url::display (array ('c' => 'feed', 'a' => 'truncate', 'params' => array ('id' => $this->flux->id ()))); ?>"><?php echo Minz_Translate::t ('truncate'); ?></button>
+			</div>
+		</div>
+
+		<legend><?php echo Minz_Translate::t ('login_configuration'); ?></legend>
 		<?php $auth = $this->flux->httpAuth (false); ?>
 		<div class="form-group">
-			<label class="group-name" for="http_user"><?php echo Translate::t ('http_username'); ?></label>
+			<label class="group-name" for="http_user"><?php echo Minz_Translate::t ('http_username'); ?></label>
 			<div class="group-controls">
-				<input type="text" name="http_user" id="http_user" value="<?php echo $auth['username']; ?>" />
-				<i class="icon i_help"></i> <?php echo Translate::t ('access_protected_feeds'); ?>
+				<input type="text" name="http_user" id="http_user" class="extend" value="<?php echo $auth['username']; ?>" autocomplete="off" />
+				<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t ('access_protected_feeds'); ?>
 			</div>
 
-			<label class="group-name" for="http_pass"><?php echo Translate::t ('http_password'); ?></label>
+			<label class="group-name" for="http_pass"><?php echo Minz_Translate::t ('http_password'); ?></label>
 			<div class="group-controls">
-				<input type="password" name="http_pass" id="http_pass" value="<?php echo $auth['password']; ?>" />
+				<input type="password" name="http_pass" id="http_pass" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="off" />
 			</div>
 		</div>
 
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
+			</div>
+		</div>
+
+		<legend><?php echo Minz_Translate::t ('advanced'); ?></legend>
+		<div class="form-group">
+			<label class="group-name" for="path_entries"><?php echo Minz_Translate::t ('css_path_on_website'); ?></label>
+			<div class="group-controls">
+				<input type="text" name="path_entries" id="path_entries" class="extend" value="<?php echo $this->flux->pathEntries (); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" />
+				<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t ('retrieve_truncated_feeds'); ?>
+			</div>
+		</div>
 
 		<div class="form-group form-actions">
 			<div class="group-controls">
-				<button class="btn btn-important"><?php echo Translate::t ('save'); ?></button>
-				<button class="btn btn-attention confirm" formaction="<?php echo Url::display (array ('c' => 'feed', 'a' => 'delete', 'params' => array ('id' => $this->flux->id ()))); ?>"><?php echo Translate::t ('delete'); ?></button>
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
 			</div>
 		</div>
 	</form>
 </div>
 
-<?php $this->renderHelper ('confirm_action_script'); ?>
-
 <?php } else { ?>
-<div class="alert alert-warn"><span class="alert-head"><?php echo Translate::t ('no_selected_feed'); ?></span> <?php echo Translate::t ('think_to_add'); ?></div>
+<div class="alert alert-warn"><span class="alert-head"><?php echo Minz_Translate::t ('no_selected_feed'); ?></span> <?php echo Minz_Translate::t ('think_to_add'); ?></div>
 <?php } ?>

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

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

+ 97 - 0
app/views/configure/queries.phtml

@@ -0,0 +1,97 @@
+<?php $this->partial('aside_configure'); ?>
+
+<div class="post">
+	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a>
+
+	<form method="post" action="<?php echo _url('configure', 'queries'); ?>">
+		<legend><?php echo _t('queries'); ?></legend>
+
+		<?php foreach ($this->conf->queries as $key => $query) { ?>
+		<div class="form-group" id="query-group-<?php echo $key; ?>">
+			<label class="group-name" for="queries_<?php echo $key; ?>_name">
+				<?php echo _t('query_number', $key + 1); ?>
+			</label>
+
+			<div class="group-controls">
+				<input type="hidden" id="queries_<?php echo $key; ?>_search" name="queries[<?php echo $key; ?>][search]" value="<?php echo isset($query['search']) ? $query['search'] : ""; ?>"/>
+				<input type="hidden" id="queries_<?php echo $key; ?>_state" name="queries[<?php echo $key; ?>][state]" value="<?php echo isset($query['state']) ? $query['state'] : ""; ?>"/>
+				<input type="hidden" id="queries_<?php echo $key; ?>_order" name="queries[<?php echo $key; ?>][order]" value="<?php echo isset($query['order']) ? $query['order'] : ""; ?>"/>
+				<input type="hidden" id="queries_<?php echo $key; ?>_get" name="queries[<?php echo $key; ?>][get]" value="<?php echo isset($query['get']) ? $query['get'] : ""; ?>"/>
+
+				<div class="stick">
+					<input class="extend"
+					       type="text"
+					       id="queries_<?php echo $key; ?>_name"
+					       name="queries[<?php echo $key; ?>][name]"
+					       value="<?php echo $query['name']; ?>"
+					/>
+
+					<a class="btn" href="<?php echo $query['url']; ?>">
+						<?php echo _i('link'); ?>
+					</a>
+
+					<a class="btn btn-attention remove" href="#" data-remove="query-group-<?php echo $key; ?>">
+						<?php echo _i('close'); ?>
+					</a>
+				</div>
+
+				<?php
+					$exist = (isset($query['search']) ? 1 : 0)
+						   + (isset($query['state']) ? 1 : 0)
+						   + (isset($query['order']) ? 1 : 0)
+						   + (isset($query['get']) ? 1 : 0);
+					// If the only filter is "all" articles, we consider there is no filter
+					$exist = ($exist === 1 && isset($query['get']) && $query['get'] === 'a') ? 0 : $exist;
+
+					$deprecated = (isset($this->query_get[$key]) &&
+					               $this->query_get[$key]['deprecated']);
+				?>
+
+				<?php if ($exist === 0) { ?>
+				<div class="alert alert-warn">
+					<div class="alert-head"><?php echo _t('no_query_filter'); ?></div>
+				</div>
+				<?php } elseif ($deprecated) { ?>
+				<div class="alert alert-error">
+					<div class="alert-head"><?php echo _t('query_deprecated'); ?></div>
+				</div>
+				<?php } else { ?>
+				<div class="alert alert-success">
+					<div class="alert-head"><?php echo _t('query_filter'); ?></div>
+
+					<ul>
+						<?php if (isset($query['search'])) { ?>
+						<li class="item"><?php echo _t('query_search', $query['search']); ?></li>
+						<?php } ?>
+
+						<?php if (isset($query['state'])) { ?>
+						<li class="item"><?php echo _t('query_state_' . $query['state']); ?></li>
+						<?php } ?>
+
+						<?php if (isset($query['order'])) { ?>
+						<li class="item"><?php echo _t('query_order_' . strtolower($query['order'])); ?></li>
+						<?php } ?>
+
+						<?php if (isset($query['get'])) { ?>
+						<li class="item"><?php echo _t('query_get_' . $this->query_get[$key]['type'], $this->query_get[$key]['name']); ?></li>
+						<?php } ?>
+					</ul>
+				</div>
+				<?php } ?>
+			</div>
+		</div>
+		<?php } ?>
+
+		<?php if (count($this->conf->queries) > 0) { ?>
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo _t('save'); ?></button>
+				<button type="reset" class="btn"><?php echo _t('cancel'); ?></button>
+			</div>
+		</div>
+		<?php } else { ?>
+		<p class="alert alert-warn"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('no_query'); ?></p>
+		<?php } ?>
+	</form>
+
+</div>

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

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

+ 59 - 0
app/views/configure/sharing.phtml

@@ -0,0 +1,59 @@
+<?php $this->partial ('aside_configure'); ?>
+
+<div class="post">
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
+
+	<form method="post" action="<?php echo _url ('configure', 'sharing'); ?>"
+		data-simple='<div class="form-group" id="group-share-##key##"><label class="group-name">##label##</label><div class="group-controls"><a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo FreshRSS_Themes::icon('close'); ?></a>
+			<input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" /></div></div>'
+		data-advanced='<div class="form-group" id="group-share-##key##"><label class="group-name">##label##</label><div class="group-controls">
+			<input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" />
+			<div class="stick">
+			<input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
+			<input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
+			<a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo FreshRSS_Themes::icon('close'); ?></a></div>
+			<a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="##help##"><?php echo FreshRSS_Themes::icon('help'); ?></a>
+			</div></div>'>
+		<legend><?php echo Minz_Translate::t ('sharing'); ?></legend>
+		<?php foreach ($this->conf->sharing as $key => $sharing): ?>
+			<?php $share = $this->conf->shares[$sharing['type']]; ?>
+			<div class="form-group" id="group-share-<?php echo $key; ?>">
+				<label class="group-name">
+					<?php echo Minz_Translate::t ($sharing['type']); ?>
+				</label>
+				<div class="group-controls">
+					<input type='hidden' id='share_<?php echo $key;?>_type' name="share[<?php echo $key;?>][type]" value='<?php echo $sharing['type']?>' />
+					<?php if ($share['form'] === 'advanced') { ?>
+						<div class="stick">
+							<input type="text" id="share_<?php echo $key;?>_name" name="share[<?php echo $key;?>][name]" class="extend" value="<?php echo $sharing['name']?>" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
+							<input type="url" id="share_<?php echo $key;?>_url" name="share[<?php echo $key;?>][url]" class="extend" value="<?php echo $sharing['url']?>" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
+							<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo FreshRSS_Themes::icon('close'); ?></a>
+						</div>
+
+						<a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="<?php echo $share['help']?>"><?php echo FreshRSS_Themes::icon('help'); ?></a>
+					<?php } else { ?>
+					<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo FreshRSS_Themes::icon('close'); ?></a>
+					<?php } ?>
+				</div>
+			</div>
+		<?php endforeach;?>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<select>
+					<?php foreach($this->conf->shares as $key => $params):?>
+						<option value='<?php echo $key?>' data-form='<?php echo $params['form']?>' data-help='<?php if (!empty($params['help'])) {echo $params['help'];}?>'><?php echo Minz_Translate::t($key) ?></option>
+					<?php endforeach; ?>
+				</select>
+				<a href='#' class='share add btn'><?php echo FreshRSS_Themes::icon('add'); ?></a>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
+			</div>
+		</div>
+	</form>
+</div>

+ 80 - 16
app/views/configure/shortcut.phtml

@@ -1,7 +1,7 @@
 <?php $this->partial ('aside_configure'); ?>
 
 <div class="post">
-	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Translate::t ('back_to_rss_feeds'); ?></a>
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
 
 	<datalist id="keys">
 		<?php foreach ($this->list_keys as $key) { ?>
@@ -9,55 +9,119 @@
 		<?php } ?>
 	</datalist>
 
-	<?php $s = $this->conf->shortcuts (); ?>
+	<?php $s = $this->conf->shortcuts; ?>
 
 	<form method="post" action="<?php echo _url ('configure', 'shortcut'); ?>">
-		<legend><?php echo Translate::t ('shortcuts_management'); ?></legend>
+		<legend><?php echo Minz_Translate::t ('shortcuts'); ?></legend>
 
-		<noscript><p class="alert alert-error"><?php echo Translate::t ('javascript_for_shortcuts'); ?></p></noscript>
+		<noscript><p class="alert alert-error"><?php echo Minz_Translate::t ('javascript_for_shortcuts'); ?></p></noscript>
+
+		<legend><?php echo Minz_Translate::t ('shortcuts_navigation'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="next_entry"><?php echo Minz_Translate::t ('next_article'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="next_entry" name="shortcuts[next_entry]" list="keys" value="<?php echo $s['next_entry']; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="prev_entry"><?php echo Minz_Translate::t ('previous_article'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="prev_entry" name="shortcuts[prev_entry]" list="keys" value="<?php echo $s['prev_entry']; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="first_entry"><?php echo Minz_Translate::t ('first_article'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="first_entry" name="shortcuts[first_entry]" list="keys" value="<?php echo $s['first_entry']; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="last_entry"><?php echo Minz_Translate::t ('last_article'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="last_entry" name="shortcuts[last_entry]" list="keys" value="<?php echo $s['last_entry']; ?>" />
+			</div>
+		</div>
+
+		<div><?php echo Minz_Translate::t ('shortcuts_navigation_help');?></div>
+
+		<legend><?php echo Minz_Translate::t ('shortcuts_article_action');?></legend>
 
 		<div class="form-group">
-			<label class="group-name" for="mark_read"><?php echo Translate::t ('mark_read'); ?></label>
+			<label class="group-name" for="mark_read"><?php echo Minz_Translate::t ('mark_read'); ?></label>
 			<div class="group-controls">
 				<input type="text" id="mark_read" name="shortcuts[mark_read]" list="keys" value="<?php echo $s['mark_read']; ?>" />
-				<?php echo Translate::t ('shift_for_all_read'); ?>
+				<?php echo Minz_Translate::t ('shift_for_all_read'); ?>
 			</div>
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="mark_favorite"><?php echo Translate::t ('mark_favorite'); ?></label>
+			<label class="group-name" for="mark_favorite"><?php echo Minz_Translate::t ('mark_favorite'); ?></label>
 			<div class="group-controls">
 				<input type="text" id="mark_favorite" name="shortcuts[mark_favorite]" list="keys" value="<?php echo $s['mark_favorite']; ?>" />
 			</div>
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="go_website"><?php echo Translate::t ('see_on_website'); ?></label>
+			<label class="group-name" for="go_website"><?php echo Minz_Translate::t ('see_on_website'); ?></label>
 			<div class="group-controls">
 				<input type="text" id="go_website" name="shortcuts[go_website]" list="keys" value="<?php echo $s['go_website']; ?>" />
 			</div>
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="next_entry"><?php echo Translate::t ('next_article'); ?></label>
+			<label class="group-name" for="auto_share_shortcut"><?php echo Minz_Translate::t ('auto_share'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="next_entry" name="shortcuts[next_entry]" list="keys" value="<?php echo $s['next_entry']; ?>" />
-				<?php echo Translate::t ('shift_for_last'); ?>
+				<input type="text" id="auto_share_shortcut" name="shortcuts[auto_share]" list="keys" value="<?php echo $s['auto_share']; ?>" />
+				<?php echo Minz_Translate::t ('auto_share_help'); ?>
 			</div>
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="prev_entry"><?php echo Translate::t ('previous_article'); ?></label>
+			<label class="group-name" for="collapse_entry"><?php echo Minz_Translate::t ('collapse_article'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="prev_entry" name="shortcuts[prev_entry]" list="keys" value="<?php echo $s['prev_entry']; ?>" />
-				<?php echo Translate::t ('shift_for_first'); ?>
+				<input type="text" id="collapse_entry" name="shortcuts[collapse_entry]" list="keys" value="<?php echo $s['collapse_entry']; ?>" />
+			</div>
+		</div>
+
+		<legend><?php echo Minz_Translate::t ('shortcuts_other_action');?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="load_more_shortcut"><?php echo Minz_Translate::t ('load_more'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="load_more_shortcut" name="shortcuts[load_more]" list="keys" value="<?php echo $s['load_more']; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="focus_search_shortcut"><?php echo Minz_Translate::t ('focus_search'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="focus_search_shortcut" name="shortcuts[focus_search]" list="keys" value="<?php echo $s['focus_search']; ?>" />
+			</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 Translate::t ('save'); ?></button>
-				<button type="reset" class="btn"><?php echo Translate::t ('cancel'); ?></button>
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
 			</div>
 		</div>
 	</form>

+ 211 - 0
app/views/configure/users.phtml

@@ -0,0 +1,211 @@
+<?php $this->partial('aside_configure'); ?>
+
+<div class="post">
+	<a href="<?php echo _url('index', 'index'); ?>"><?php echo Minz_Translate::t('back_to_rss_feeds'); ?></a>
+
+	<form method="post" action="<?php echo _url('users', 'auth'); ?>">
+		<legend><?php echo Minz_Translate::t('login_configuration'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="current_user"><?php echo Minz_Translate::t('current_user'); ?></label>
+			<div class="group-controls">
+				<input id="current_user" type="text" disabled="disabled" value="<?php echo Minz_Session::param('currentUser', '_'); ?>" />
+				<label class="checkbox" for="is_admin">
+					<input type="checkbox" id="is_admin" disabled="disabled" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? 'checked="checked" ' : ''; ?>/>
+					<?php echo Minz_Translate::t('is_admin'); ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
+			<div class="group-controls">
+				<div class="stick">
+					<input type="password" id="passwordPlain" name="passwordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
+					<a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				</div>
+				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
+			</div>
+		</div>
+
+		<?php if (Minz_Configuration::apiEnabled()) { ?>
+		<div class="form-group">
+			<label class="group-name" for="apiPasswordPlain"><?php echo Minz_Translate::t('password_api'); ?></label>
+			<div class="group-controls">
+				<div class="stick">
+					<input type="password" id="apiPasswordPlain" name="apiPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
+					<a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				</div>
+			</div>
+		</div>
+		<?php } ?>
+
+		<div class="form-group">
+			<label class="group-name" for="mail_login"><?php echo Minz_Translate::t('persona_connection_email'); ?></label>
+			<?php $mail = $this->conf->mail_login; ?>
+			<div class="group-controls">
+				<input type="email" id="mail_login" name="mail_login" class="extend" autocomplete="off" value="<?php echo $mail; ?>" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? '' : 'disabled="disabled"'; ?> placeholder="alice@example.net" />
+				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
+			</div>
+		</div>
+
+	<?php if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { ?>
+
+		<legend><?php echo Minz_Translate::t('auth_type'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="auth_type"><?php echo Minz_Translate::t('auth_type'); ?></label>
+			<div class="group-controls">
+				<select id="auth_type" name="auth_type" required="required">
+					<?php if (!in_array(Minz_Configuration::authType(), array('form', 'persona', 'http_auth', 'none'))) { ?>
+						<option selected="selected"></option>
+					<?php } ?>
+					<option value="form"<?php echo Minz_Configuration::authType() === 'form' ? ' selected="selected"' : '', cryptAvailable() ? '' : ' disabled="disabled"'; ?>><?php echo Minz_Translate::t('auth_form'); ?></option>
+					<option value="persona"<?php echo Minz_Configuration::authType() === 'persona' ? ' selected="selected"' : '', $this->conf->mail_login == '' ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('auth_persona'); ?></option>
+					<option value="http_auth"<?php echo Minz_Configuration::authType() === 'http_auth' ? ' selected="selected"' : '', httpAuthUser() == '' ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('http_auth'); ?> (REMOTE_USER = '<?php echo httpAuthUser(); ?>')</option>
+					<option value="none"<?php echo Minz_Configuration::authType() === 'none' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t('auth_none'); ?></option>
+				</select>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="anon_access">
+					<input type="checkbox" name="anon_access" id="anon_access" value="1"<?php echo Minz_Configuration::allowAnonymous() ? ' checked="checked"' : '',
+						Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
+					<?php echo Minz_Translate::t('allow_anonymous', Minz_Configuration::defaultUser()); ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="anon_refresh">
+					<input type="checkbox" name="anon_refresh" id="anon_refresh" value="1"<?php echo Minz_Configuration::allowAnonymousRefresh() ? ' checked="checked"' : '',
+						Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
+					<?php echo Minz_Translate::t('allow_anonymous_refresh'); ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="unsafe_autologin">
+					<input type="checkbox" name="unsafe_autologin" id="unsafe_autologin" value="1"<?php echo Minz_Configuration::unsafeAutologinEnabled() ? ' checked="checked"' : '',
+						Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
+					<?php echo Minz_Translate::t('unsafe_autologin'); ?>
+					<kbd>p/i/?a=formLogin&amp;u=Alice&amp;p=1234</kbd>
+				</label>
+			</div>
+		</div>
+
+		<?php if (Minz_Configuration::canLogIn()) { ?>
+		<div class="form-group">
+			<label class="group-name" for="token"><?php echo Minz_Translate::t('auth_token'); ?></label>
+			<?php $token = $this->conf->token; ?>
+			<div class="group-controls">
+				<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo Minz_Translate::t('blank_to_disable'); ?>"<?php
+					echo Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
+				<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('explain_token', Minz_Url::display(null, 'html', true), $token); ?>
+			</div>
+		</div>
+		<?php } ?>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="api_enabled">
+					<input type="checkbox" name="api_enabled" id="api_enabled" value="1"<?php echo Minz_Configuration::apiEnabled() ? ' checked="checked"' : '',
+						Minz_Configuration::needsLogin() ? '' : ' disabled="disabled"'; ?> />
+					<?php echo Minz_Translate::t('api_enabled'); ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
+			</div>
+		</div>
+	</form>
+
+	<form method="post" action="<?php echo _url('users', 'delete'); ?>">
+		<legend><?php echo Minz_Translate::t('users'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="users_list"><?php echo Minz_Translate::t('users_list'); ?></label>
+			<div class="group-controls">
+				<select id="users_list" name="username"><?php
+					foreach (listUsers() as $user) {
+						echo '<option>', $user, '</option>';
+					}
+				?></select>
+			</div>
+		</div>
+	
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-attention confirm"><?php echo Minz_Translate::t('delete'); ?></button>
+			</div>
+		</div>
+	</form>
+
+	<form method="post" action="<?php echo _url('users', 'create'); ?>">
+		<legend><?php echo Minz_Translate::t('create_user'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="new_user_language"><?php echo Minz_Translate::t ('language'); ?></label>
+			<div class="group-controls">
+				<select name="new_user_language" id="new_user_language">
+				<?php $languages = $this->conf->availableLanguages (); ?>
+				<?php foreach ($languages as $short => $lib) { ?>
+				<option value="<?php echo $short; ?>"<?php echo $this->conf->language === $short ? ' selected="selected"' : ''; ?>><?php echo $lib; ?></option>
+				<?php } ?>
+				</select>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="new_user_name"><?php echo Minz_Translate::t('username'); ?></label>
+			<div class="group-controls">
+				<input id="new_user_name" name="new_user_name" type="text" size="16" required="required" maxlength="16" autocomplete="off" pattern="[0-9a-zA-Z]{1,16}" placeholder="demo" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="new_user_passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
+			<div class="group-controls">
+				<div class="stick">
+					<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="off" pattern=".{7,}" />
+					<a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				</div>
+				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="new_user_email"><?php echo Minz_Translate::t('persona_connection_email'); ?></label>
+			<?php $mail = $this->conf->mail_login; ?>
+			<div class="group-controls">
+				<input type="email" id="new_user_email" name="new_user_email" class="extend" autocomplete="off" placeholder="alice@example.net" />
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('create'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
+			</div>
+		</div>
+
+	</form>
+
+	<?php } ?>
+</div>

+ 9 - 8
app/views/entry/bookmark.phtml

@@ -1,15 +1,16 @@
 <?php
+header('Content-Type: application/json; charset=UTF-8');
 
-if (Request::param ('is_favorite')) {
-	Request::_param ('is_favorite', 0);
+if (Minz_Request::param ('is_favorite', true)) {
+	Minz_Request::_param ('is_favorite', 0);
 } else {
-	Request::_param ('is_favorite', 1);
+	Minz_Request::_param ('is_favorite', 1);
 }
 
-$url = Url::display (array (
-	'c' => Request::controllerName (),
-	'a' => Request::actionName (),
-	'params' => Request::params (),
+$url = Minz_Url::display (array (
+	'c' => Minz_Request::controllerName (),
+	'a' => Minz_Request::actionName (),
+	'params' => Minz_Request::params (),
 ));
 
-echo json_encode (array ('url' => str_ireplace ('&amp;', '&', $url)));
+echo json_encode (array ('url' => str_ireplace ('&amp;', '&', $url), 'icon' => FreshRSS_Themes::icon(Minz_Request::param ('is_favorite') ? 'non-starred' : 'starred')));

+ 9 - 8
app/views/entry/read.phtml

@@ -1,15 +1,16 @@
 <?php
+header('Content-Type: application/json; charset=UTF-8');
 
-if (Request::param ('is_read')) {
-	Request::_param ('is_read', 0);
+if (Minz_Request::param ('is_read', true)) {
+	Minz_Request::_param ('is_read', 0);
 } else {
-	Request::_param ('is_read', 1);
+	Minz_Request::_param ('is_read', 1);
 }
 
-$url = Url::display (array (
-	'c' => Request::controllerName (),
-	'a' => Request::actionName (),
-	'params' => Request::params (),
+$url = Minz_Url::display (array (
+	'c' => Minz_Request::controllerName (),
+	'a' => Minz_Request::actionName (),
+	'params' => Minz_Request::params (),
 ));
 
-echo json_encode (array ('url' => str_ireplace ('&amp;', '&', $url)));
+echo json_encode (array ('url' => str_ireplace ('&amp;', '&', $url), 'icon' => FreshRSS_Themes::icon(Minz_Request::param ('is_read') ? 'unread' : 'read')));

+ 2 - 3
app/views/error/index.phtml

@@ -1,10 +1,9 @@
 <div class="post">
 	<div class="alert alert-error">
 		<h1 class="alert-head"><?php echo $this->code; ?></h1>
-
 		<p>
-			<?php echo Translate::t ('page_not_found'); ?><br />
-			<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Translate::t ('back_to_rss_feeds'); ?></a>
+			<?php echo $this->errorMessage; ?><br />
+			<a href="<?php echo _url('index', 'index'); ?>"><?php echo Minz_Translate::t('back_to_rss_feeds'); ?></a>
 		</p>
 	</div>
 </div>

+ 1 - 0
app/views/feed/actualize.phtml

@@ -0,0 +1 @@
+OK

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

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

+ 0 - 5
app/views/helpers/confirm_action_script.phtml

@@ -1,5 +0,0 @@
-<script type="text/javascript">
-    $('.confirm').click(function () {
-        return confirm("<?php echo Translate::t('confirm_action'); ?>");
-    });
-</script>

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

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

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

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

+ 61 - 0
app/views/helpers/javascript_vars.phtml

@@ -0,0 +1,61 @@
+<?php
+
+echo '"use strict";', "\n";
+
+$mark = $this->conf->mark_when;
+echo 'var ',
+	'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',
+	',auto_mark_scroll=', $mark['scroll'] ? 'true' : 'false',
+	',auto_load_more=', $this->conf->auto_load_more ? 'true' : 'false',
+	',does_lazyload=', $this->conf->lazyload ? 'true' : 'false',
+	',sticky_post=', $this->conf->sticky_post ? 'true' : 'false';
+
+$s = $this->conf->shortcuts;
+echo ',shortcuts={',
+	'mark_read:"', $s['mark_read'], '",',
+	'mark_favorite:"', $s['mark_favorite'], '",',
+	'go_website:"', $s['go_website'], '",',
+	'prev_entry:"', $s['prev_entry'], '",',
+	'next_entry:"', $s['next_entry'], '",',
+	'first_entry:"', $s['first_entry'], '",',
+	'last_entry:"', $s['last_entry'], '",',
+	'collapse_entry:"', $s['collapse_entry'], '",',
+	'load_more:"', $s['load_more'], '",',
+	'auto_share:"', $s['auto_share'], '",',
+	'focus_search:"', $s['focus_search'], '",',
+	'user_filter:"', $s['user_filter'], '",',
+	'help:"', $s['help'], '"',
+"},\n";
+
+if (Minz_Request::param ('output') === 'global') {
+	echo "iconClose='", FreshRSS_Themes::icon('close'), "',\n";
+}
+
+$authType = Minz_Configuration::authType();
+if ($authType === 'persona') {
+	// If user is disconnected, current_user_mail MUST be null
+	$mail = Minz_Session::param ('mail', false);
+	if ($mail) {
+		echo 'current_user_mail="' . $mail . '",';
+	} else {
+		echo 'current_user_mail=null,';
+	}
+}
+
+echo 'authType="', $authType, '",',
+	'url_freshrss="', _url ('index', 'index'), '",',
+	'url_login="', _url ('index', 'login'), '",',
+	'url_logout="', _url ('index', 'logout'), '",';
+
+echo 'str_confirmation_default="', Minz_Translate::t('confirm_action'), '"', ",\n";
+echo 'str_notif_title_articles="', Minz_Translate::t('notif_title_new_articles'), '"', ",\n";
+echo 'str_notif_body_articles="', Minz_Translate::t('notif_body_new_articles'), '"', ",\n";
+echo 'html5_notif_timeout=', $this->conf->html5_notif_timeout,",\n";
+
+
+$autoActualise = Minz_Session::param('actualize_feeds', false);
+echo 'auto_actualize_feeds=', $autoActualise ? 'true' : 'false', ";\n";

+ 8 - 8
app/views/helpers/logs_pagination.phtml

@@ -1,7 +1,7 @@
 <?php
-	$c = Request::controllerName ();
-	$a = Request::actionName ();
-	$params = Request::params ();
+	$c = Minz_Request::controllerName ();
+	$a = Minz_Request::actionName ();
+	$params = Minz_Request::params ();
 ?>
 
 <?php if ($this->nbPage > 1) { ?>
@@ -9,14 +9,14 @@
 	<?php $params[$getteur] = 1; ?>
 	<li class="item pager-first">
 		<?php if ($this->currentPage > 1) { ?>
-		<a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">« <?php echo Translate::t('first'); ?></a>
+		<a href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">« <?php echo Minz_Translate::t('first'); ?></a>
 		<?php } ?>
 	</li>
 
 	<?php $params[$getteur] = $this->currentPage - 1; ?>
 	<li class="item pager-previous">
 		<?php if ($this->currentPage > 1) { ?>
-		<a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">‹ <?php echo Translate::t('previous'); ?></a>
+		<a href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">‹ <?php echo Minz_Translate::t('previous'); ?></a>
 		<?php } ?>
 	</li>
 
@@ -24,7 +24,7 @@
 		<?php if($i > 0 && $i <= $this->nbPage) { ?>
 			<?php if ($i != $this->currentPage) { ?>
 			<?php $params[$getteur] = $i; ?>
-			<li class="item pager-item"><a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo $i; ?></a></li>
+			<li class="item pager-item"><a href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo $i; ?></a></li>
 			<?php } else { ?>
 			<li class="item pager-current"><?php echo $i; ?></li>
 			<?php } ?>
@@ -34,13 +34,13 @@
 	<?php $params[$getteur] = $this->currentPage + 1; ?>
 	<li class="item pager-next">
 		<?php if ($this->currentPage < $this->nbPage) { ?>
-		<a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Translate::t('next'); ?> ›</a>
+		<a href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Minz_Translate::t('next'); ?> ›</a>
 		<?php } ?>
 	</li>
 	<?php $params[$getteur] = $this->nbPage; ?>
 	<li class="item pager-last">
 		<?php if ($this->currentPage < $this->nbPage) { ?>
-		<a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Translate::t('last'); ?> »</a>
+		<a href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Minz_Translate::t('last'); ?> »</a>
 		<?php } ?>
 	</li>
 </ul>

+ 28 - 11
app/views/helpers/pagination.phtml

@@ -1,20 +1,37 @@
 <?php
-	$c = Request::controllerName ();
-	$a = Request::actionName ();
-	$params = Request::params ();
+	$c = Minz_Request::controllerName();
+	$a = Minz_Request::actionName();
+	$params = Minz_Request::params();
+	$markReadUrl = Minz_Session::param('markReadUrl');
+	Minz_Session::_param('markReadUrl', false);
 ?>
 
+<form id="mark-read-pagination" method="post" style="display: none"></form>
+
 <ul class="pagination">
 	<li class="item pager-next">
-	<?php if ($this->next != '') { ?>
-	<?php $params[$getteur] = $this->next; ?>
-	<a id="load_more" href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Translate::t ('load_more'); ?></a>
+	<?php if (!empty($this->nextId)) { ?>
+		<?php
+			$params['next'] = $this->nextId;
+			$params['ajax'] = 1;
+		?>
+		<a id="load_more" href="<?php echo Minz_Url::display(array('c' => $c, 'a' => $a, 'params' => $params)); ?>">
+			<?php echo _t('load_more'); ?>
+		</a>
+	<?php } elseif ($markReadUrl) { ?>
+		<button id="bigMarkAsRead"
+		        class="as-link <?php echo $this->conf->reading_confirm ? 'confirm' : ''; ?>"
+		        form="mark-read-pagination"
+		        formaction="<?php echo $markReadUrl; ?>"
+		        type="submit">
+			<?php echo _t('nothing_to_load'); ?><br />
+			<span class="bigTick">✓</span><br />
+			<?php echo _t('mark_all_read'); ?>
+		</button>
 	<?php } else { ?>
-	<div class="bigMarkAsRead">
-		<p><?php echo Translate::t ('nothing_to_load'); ?></p>
-		<p class="bigTick">✔</p>
-		<p><?php echo Translate::t ('mark_all_read'); ?></p>
-	</div>
+		<a id="bigMarkAsRead" href=".">
+			<?php echo _t('nothing_to_load'); ?><br />
+		</a>
 	<?php } ?>
 	</li>
 </ul>

+ 28 - 17
app/views/helpers/view/global_view.phtml

@@ -1,30 +1,34 @@
 <?php $this->partial ('nav_menu'); ?>
 
-<div id="stream" class="global">
+<?php if (!empty($this->entries)) { ?>
+<div id="stream" class="global categories">
 <?php
+	$arUrl = array('c' => 'index', 'a' => 'index', 'params' => array());
+	if ($this->conf->view_mode !== 'normal') {
+		$arUrl['params']['output'] = 'normal';
+	}
+	$p = Minz_Request::param('state', '');
+	if (($p != '') && ($this->conf->default_view !== $p)) {
+		$arUrl['params']['state'] = $p;
+	}
+
 	foreach ($this->cat_aside as $cat) {
 		$feeds = $cat->feeds ();
-		$catNotRead = $cat->nbNotRead ();
 		if (!empty ($feeds)) {
 ?>
-	<div class="category">
-		<div class="cat_header">
-			<a href="<?php echo _url ('index', 'index', 'get', 'c_' . $cat->id (), 'output', 'normal'); ?>">
-			<?php echo $cat->name(); ?><?php echo $catNotRead > 0 ? ' (' . $catNotRead . ')' : ''; ?>
+	<div class="box-category">
+		<div class="category">
+			<a data-unread="<?php echo formatNumber($cat->nbNotRead()); ?>" class="btn" href="<?php $arUrl['params']['get'] = 'c_' . $cat->id (); echo Minz_Url::display($arUrl); ?>">
+			<?php echo $cat->name(); ?>
 			</a>
 		</div>
-
 		<ul class="feeds">
 			<?php foreach ($feeds as $feed) { ?>
 			<?php $not_read = $feed->nbNotRead (); ?>
-			<li class="item">
-				<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" />
-
-				<a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id (), 'output', 'normal'); ?>">
-				<?php echo $not_read > 0 ? '<b>' : ''; ?>
+			<li id="f_<?php echo $feed->id (); ?>" class="item<?php echo $feed->inError () ? ' error' : ''; ?><?php echo $feed->nbEntries () == 0 ? ' empty' : ''; ?>">
+				<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" />
+				<a class="feed" data-unread="<?php echo formatNumber($feed->nbNotRead()); ?>" data-priority="<?php echo $feed->priority (); ?>" href="<?php $arUrl['params']['get'] = 'f_' . $feed->id(); echo Minz_Url::display($arUrl); ?>">
 				<?php echo $feed->name(); ?>
-				<?php echo $not_read > 0 ? ' (' . $not_read . ')' : ''; ?>
-				<?php echo $not_read > 0 ? '</b>' : ''; ?>
 				</a>
 			</li>
 			<?php } ?>
@@ -37,6 +41,13 @@
 </div>
 
 <div id="overlay"></div>
-<div id="panel">
-	<a class="close" href="#"><i class="icon i_close"></i></a>
-</div>
+<div id="panel"<?php echo $this->conf->display_posts ? '' : ' class="hide_posts"'; ?>>
+	<a class="close" href="#"><?php echo FreshRSS_Themes::icon('close'); ?></a>
+</div>
+
+<?php } else { ?>
+<div id="stream" class="prompt alert alert-warn global">
+	<h2><?php echo _t('no_feed_to_display'); ?></h2>
+	<a href="<?php echo _url('configure', 'feed'); ?>"><?php echo _t('think_to_add'); ?></a><br /><br />
+</div>
+<?php } ?>

+ 156 - 138
app/views/helpers/view/normal_view.phtml

@@ -3,170 +3,188 @@
 $this->partial ('aside_flux');
 $this->partial ('nav_menu');
 
-if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) {
-	$items = $this->entryPaginator->items ();
-?>
-
-<div id="stream" class="normal">
-	<?php
-		$display_today = true;
-		$display_yesterday = true;
-		$display_others = true;
-	?>
-	<?php foreach ($items as $item) { ?>
-
-	<?php if ($display_today && $item->isDay (Days::TODAY)) { ?>
-	<div class="day">
-		<?php echo Translate::t ('today'); ?>
-		<span class="date"> - <?php echo timestamptodate (time (), false); ?></span>
-		<span class="name"><?php echo $this->currentName; ?></span>
-	</div>
-	<?php $display_today = false; } ?>
-	<?php if ($display_yesterday && $item->isDay (Days::YESTERDAY)) { ?>
-	<div class="day">
-		<?php echo Translate::t ('yesterday'); ?>
-		<span class="date"> - <?php echo timestamptodate (time () - 86400, false); ?></span>
-		<span class="name"><?php echo $this->currentName; ?></span>
-	</div>
-	<?php $display_yesterday = false; } ?>
-	<?php if ($display_others && $item->isDay (Days::BEFORE_YESTERDAY)) { ?>
-	<div class="day">
-		<?php echo Translate::t ('before_yesterday'); ?>
-		<span class="name"><?php echo $this->currentName; ?></span>
-	</div>
-	<?php $display_others = false; } ?>
+if (!empty($this->entries)) {
+	$display_today = true;
+	$display_yesterday = true;
+	$display_others = true;
+	if ($this->loginOk) {
+		$sharing = $this->conf->sharing;
+	} else {
+		$sharing = array();
+	}
+	$hidePosts = !$this->conf->display_posts;
+	$lazyload = $this->conf->lazyload;
+	$topline_read = $this->conf->topline_read;
+	$topline_favorite = $this->conf->topline_favorite;
+	$topline_date = $this->conf->topline_date;
+	$topline_link = $this->conf->topline_link;
+	$bottomline_read = $this->conf->bottomline_read;
+	$bottomline_favorite = $this->conf->bottomline_favorite;
+	$bottomline_sharing = $this->conf->bottomline_sharing && (count($sharing));
+	$bottomline_tags = $this->conf->bottomline_tags;
+	$bottomline_date = $this->conf->bottomline_date;
+	$bottomline_link = $this->conf->bottomline_link;
 
-	<div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>">
-		<ul class="horizontal-list flux_header">
-			<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
-			<li class="item manage">
-				<?php if (!$item->isRead ()) { ?>
-				<a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 1); ?>">&nbsp;</a>
-				<?php } else { ?>
-				<a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 0); ?>">&nbsp;</a>
-				<?php } ?>
+	$content_width = $this->conf->content_width;
+?>
 
-				<?php if (!$item->isFavorite ()) { ?>
-				<a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 1); ?>">&nbsp;</a>
-				<?php } else { ?>
-				<a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 0); ?>">&nbsp;</a>
-				<?php } ?>
-			</li>
-			<?php } ?>
-			<?php
-				$feed = HelperCategory::findFeed($this->cat_aside, $item->feed ());	//We most likely already have the feed object in cache
-				if (empty($feed)) $feed = $item->feed (true);
-			?>
-			<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>
+<div id="stream" class="normal<?php echo $hidePosts ? ' hide_posts' : ''; ?>"><?php
+	?><div id="new-article">
+		<a href="<?php echo Minz_Url::display ($this->url); ?>"><?php echo Minz_Translate::t ('new_article'); ?></a>
+	</div><?php
+	foreach ($this->entries as $item) {
+		if ($display_today && $item->isDay (FreshRSS_Days::TODAY, $this->today)) {
+			?><div class="day" id="day_today"><?php
+				echo Minz_Translate::t ('today');
+				?><span class="date"> — <?php echo timestamptodate (time (), false); ?></span><?php
+				?><span class="name"><?php echo $this->currentName; ?></span><?php
+			?></div><?php
+			$display_today = false;
+		}
+		if ($display_yesterday && $item->isDay (FreshRSS_Days::YESTERDAY, $this->today)) {
+			?><div class="day" id="day_yesterday"><?php
+				echo Minz_Translate::t ('yesterday');
+				?><span class="date"> — <?php echo timestamptodate (time () - 86400, false); ?></span><?php
+				?><span class="name"><?php echo $this->currentName; ?></span><?php
+			?></div><?php
+			$display_yesterday = false;
+		}
+		if ($display_others && $item->isDay (FreshRSS_Days::BEFORE_YESTERDAY, $this->today)) {
+			?><div class="day" id="day_before_yesterday"><?php
+				echo Minz_Translate::t ('before_yesterday');
+				?><span class="name"><?php echo $this->currentName; ?></span><?php
+			?></div><?php
+			$display_others = false;
+		}
+	?><div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>">
+		<ul class="horizontal-list flux_header"><?php
+			if ($this->loginOk) {
+				if ($topline_read) {
+					?><li class="item manage"><?php
+						$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id ()));
+						if ($item->isRead()) {
+							$arUrl['params']['is_read'] = 0;
+						}
+						?><a class="read" href="<?php echo Minz_Url::display($arUrl); ?>"><?php
+							echo FreshRSS_Themes::icon($item->isRead () ? 'read' : 'unread'); ?></a><?php
+					?></li><?php
+				}
+				if ($topline_favorite) {
+					?><li class="item manage"><?php
+						$arUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id ()));
+						if ($item->isFavorite()) {
+							$arUrl['params']['is_favorite'] = 0;
+						}
+						?><a class="bookmark" href="<?php echo Minz_Url::display($arUrl); ?>"><?php
+							echo FreshRSS_Themes::icon($item->isFavorite () ? 'starred' : 'non-starred'); ?></a><?php
+					?></li><?php
+				}
+			}
+			$feed = FreshRSS_CategoryDAO::findFeed($this->cat_aside, $item->feed ());	//We most likely already have the feed object in cache
+			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>
-			<li class="item date"><?php echo $item->date (); ?></li>
-			<li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>">&nbsp;</a></li>
+			<?php if ($topline_date) { ?><li class="item date"><?php echo $item->date (); ?> </li><?php } ?>
+			<?php if ($topline_link) { ?><li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a></li><?php } ?>
 		</ul>
 
 		<div class="flux_content">
-			<div class="content">
-				<h1 class="title"><?php echo $item->title (); ?></h1>
-				<?php $author = $item->author (); ?>
-				<?php echo $author != '' ? '<div class="author">' . Translate::t ('by_author', $author) . '</div>' : ''; ?>
+			<div class="content <?php echo $content_width; ?>">
+				<h1 class="title"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo $item->title (); ?></a></h1>
 				<?php
-					if($this->conf->lazyload() == 'yes') {
-						echo lazyimg($item->content ());
-					} else {
-						echo $item->content();
-					}
+					$author = $item->author();
+					echo $author != '' ? '<div class="author">' . Minz_Translate::t('by_author', $author) . '</div>' : '',
+						$lazyload && $hidePosts ? lazyimg($item->content()) : $item->content();
 				?>
 			</div>
-
-			<ul class="horizontal-list bottom">
-				<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
-				<li class="item manage">
-					<?php if (!$item->isRead ()) { ?>
-					<a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 1); ?>">&nbsp;</a>
-					<?php } else { ?>
-					<a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 0); ?>">&nbsp;</a>
-					<?php } ?>
-
-					<?php if (!$item->isFavorite ()) { ?>
-					<a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 1); ?>">&nbsp;</a>
-					<?php } else { ?>
-					<a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 0); ?>">&nbsp;</a>
-					<?php } ?>
-				</li>
-				<?php } ?>
-				<li class="item">
-					<?php $link = urlencode ($item->link ()); ?>
-					<?php $title = urlencode ($item->title () . ' - ' . $feed->name ()); ?>
-					<div class="dropdown">
+			<ul class="horizontal-list bottom"><?php
+				if ($this->loginOk) {
+					if ($bottomline_read) {
+						?><li class="item manage"><?php
+							$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id ()));
+							if ($item->isRead()) {
+								$arUrl['params']['is_read'] = 0;
+							}
+							?><a class="read" href="<?php echo Minz_Url::display($arUrl); ?>"><?php
+								echo FreshRSS_Themes::icon($item->isRead () ? 'read' : 'unread'); ?></a><?php
+						?></li><?php
+					}
+					if ($bottomline_favorite) {
+						?><li class="item manage"><?php
+							$arUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id ()));
+							if ($item->isFavorite()) {
+								$arUrl['params']['is_favorite'] = 0;
+							}
+							?><a class="bookmark" href="<?php echo Minz_Url::display($arUrl); ?>"><?php
+								echo FreshRSS_Themes::icon($item->isFavorite () ? 'starred' : 'non-starred'); ?></a><?php
+						?></li><?php
+					}
+				} ?>
+				<li class="item"><?php
+						if ($bottomline_sharing) {
+							$link = urlencode ($item->link ());
+							$title = urlencode ($item->title () . ' · ' . $feed->name ());
+					?><div class="dropdown">
 						<div id="dropdown-share-<?php echo $item->id ();?>" class="dropdown-target"></div>
-						<i class="icon i_share"></i> <a class="dropdown-toggle" href="#dropdown-share-<?php echo $item->id ();?>"><?php echo Translate::t ('share'); ?></a>
+						<a class="dropdown-toggle" href="#dropdown-share-<?php echo $item->id ();?>">
+							<?php echo FreshRSS_Themes::icon('share'); ?>
+							<?php echo Minz_Translate::t ('share'); ?>
+						</a>
 
 						<ul class="dropdown-menu">
-							<li class="dropdown-close"><a href="#close">&nbsp;</a></li>
-
-							<?php
-							$shaarli = $this->conf->urlShaarli ();
-							if ((!login_is_conf ($this->conf) || is_logged ()) && $shaarli) {
-							?>
-							<li class="item">
-								<a target="_blank" href="<?php echo $shaarli . '?post=' . $link . '&amp;title=' . $title . '&amp;source=bookmarklet'; ?>">
-									Shaarli
-								</a>
-							</li>
-							<?php } ?>
-							<li class="item">
-								<a href="mailto:?subject=<?php echo urldecode($title); ?>&amp;body=<?php echo $link; ?>">
-									<?php echo Translate::t ('by_email'); ?>
-								</a>
-							</li>
-							<li class="item">
-								<a target="_blank" href="https://twitter.com/share?url=<?php echo $link; ?>&amp;text=<?php echo $title; ?>">
-									Twitter
-								</a>
-							</li>
-							<li class="item">
-								<a target="_blank" href="https://www.facebook.com/sharer.php?u=<?php echo $link; ?>&amp;t=<?php echo $title; ?>">
-									Facebook
-								</a>
-							</li>
-							<li class="item">
-								<a target="_blank" href="https://plus.google.com/share?url=<?php echo $link; ?>">
-									Google+
-								</a>
-							</li>
+							<li class="dropdown-close"><a href="#close">❌</a></li>
+							<?php foreach ($sharing as $share) :?>
+								<li class="item share">
+									<a target="_blank" href="<?php echo FreshRSS_Share::generateUrl($this->conf->shares, $share, $item->link(), $item->title() . ' . ' . $feed->name())?>">
+										<?php echo Minz_Translate::t ($share['name']);?>
+									</a>
+								</li>
+							<?php endforeach;?>
 						</ul>
 					</div>
-				</li>
-				<?php $tags = $item->tags(); ?>
-				<?php if(!empty($tags)) { ?>
-				<li class="item">
+					<?php } ?>
+				</li><?php
+				$tags = $bottomline_tags ? $item->tags() : null;
+				if (!empty($tags)) {
+				?><li class="item">
 					<div class="dropdown">
 						<div id="dropdown-tags-<?php echo $item->id ();?>" class="dropdown-target"></div>
-						<i class="icon i_tag"></i> <a class="dropdown-toggle" href="#dropdown-tags-<?php echo $item->id ();?>"><?php echo Translate::t ('related_tags'); ?></a>
-
+						<a class="dropdown-toggle" href="#dropdown-tags-<?php echo $item->id ();?>"><?php
+							echo FreshRSS_Themes::icon('tag'), Minz_Translate::t ('related_tags');
+						?></a>
 						<ul class="dropdown-menu">
-							<li class="dropdown-close"><a href="#close">&nbsp;</a></li>
-
-							<?php foreach($tags as $tag) { ?>
-							<li class="item"><a href="<?php echo _url ('index', 'index', 'search', urlencode ('#' . $tag)); ?>"><?php echo $tag; ?></a></li>
-							<?php } ?>
+							<li class="dropdown-close"><a href="#close">❌</a></li><?php
+							foreach($tags as $tag) {
+								?><li class="item"><a href="<?php echo _url ('index', 'index', 'search', urlencode ('#' . $tag)); ?>"><?php echo $tag; ?></a></li><?php
+							} ?>
 						</ul>
 					</div>
-				</li>
-				<?php } ?>
-				<li class="item date"><?php echo $item->date (); ?></li>
-				<li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>">&nbsp;</a></li>
+				</li><?php
+				}
+				if ($bottomline_date) {
+					?><li class="item date"><?php echo $item->date (); ?></li><?php
+				}
+				if ($bottomline_link) {
+					?><li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a></li><?php
+				} ?>
 			</ul>
 		</div>
 	</div>
 	<?php } ?>
-	
-	<?php $this->entryPaginator->render ('pagination.phtml', 'next'); ?>
+
+	<?php $this->renderHelper('pagination'); ?>
 </div>
 
 <?php $this->partial ('nav_entries'); ?>
 
 <?php } else { ?>
-<div id="stream" class="alert alert-warn normal">
-	<span class="alert-head"><?php echo Translate::t ('no_feed_to_display'); ?></span>
+<div id="stream" class="prompt alert alert-warn normal">
+	<h2><?php echo _t('no_feed_to_display'); ?></h2>
+	<a href="<?php echo _url('configure', 'feed'); ?>"><?php echo _t('think_to_add'); ?></a><br /><br />
 </div>
-<?php } ?>
+<?php } ?>

+ 19 - 23
app/views/helpers/view/reader_view.phtml

@@ -1,48 +1,44 @@
 <?php
 $this->partial ('nav_menu');
 
-if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) {
-	$items = $this->entryPaginator->items ();
+if (!empty($this->entries)) {
+	$lazyload = $this->conf->lazyload;
+	$content_width = $this->conf->content_width;
 ?>
 
 <div id="stream" class="reader">
-	<?php foreach ($items as $item) { ?>
+	<?php foreach ($this->entries as $item) { ?>
 
 	<div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>">
 		<div class="flux_content">
-			<div class="content">
+			<div class="content <?php echo $content_width; ?>">
 				<?php
-					$feed = HelperCategory::findFeed($this->cat_aside, $item->feed ());	//We most likely already have the feed object in cache
+					$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);
 				?>
 				<a href="<?php echo $item->link (); ?>">
-					<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" /> <span><?php echo $feed->name (); ?></span>
+					<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" /> <span><?php echo $feed->name(); ?></span>
 				</a>
 				<h1 class="title"><?php echo $item->title (); ?></h1>
 
-				<div class="author">
-					<?php $author = $item->author (); ?>
-					<?php echo $author != '' ? Translate::t ('by_author', $author) . ' - ' : ''; ?>
-					<?php echo $item->date (); ?>
-				</div>
+				<div class="author"><?php
+					$author = $item->author();
+					echo $author != '' ? Minz_Translate::t('by_author', $author) . ' — ' : '',
+						$item->date();
+				?></div>
 
-				<?php
-					if($this->conf->lazyload() == 'yes') {
-						echo lazyimg($item->content ());
-					} else {
-						echo $item->content();
-					}
-				?>
+				<?php echo $item->content(); ?>
 			</div>
 		</div>
 	</div>
 	<?php } ?>
-	
-	<?php $this->entryPaginator->render ('pagination.phtml', 'next'); ?>
+
+	<?php $this->renderHelper('pagination'); ?>
 </div>
 
 <?php } else { ?>
-<div id="stream" class="alert alert-warn reader">
-	<span class="alert-head"><?php echo Translate::t ('no_feed_to_display'); ?></span>
+<div id="stream" class="prompt alert alert-warn reader">
+	<h2><?php echo _t('no_feed_to_display'); ?></h2>
+	<a href="<?php echo _url('configure', 'feed'); ?>"><?php echo _t('think_to_add'); ?></a><br /><br />
 </div>
-<?php } ?>
+<?php } ?>

+ 7 - 8
app/views/helpers/view/rss_view.phtml

@@ -1,18 +1,17 @@
 <?php echo '<?xml version="1.0" encoding="UTF-8" ?>'; ?>
 <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
 	<channel>
-		<title><?php echo View::title(); ?></title>
-		<link><?php echo Url::display(); ?></link>
-		<description><?php echo Translate::t ('rss_feeds_of', View::title()); ?></description>
+		<title><?php echo $this->rss_title; ?></title>
+		<link><?php echo Minz_Url::display(null, 'html', true); ?></link>
+		<description><?php echo Minz_Translate::t ('rss_feeds_of', $this->rss_title); ?></description>
 		<pubDate><?php echo date('D, d M Y H:i:s O'); ?></pubDate>
 		<lastBuildDate><?php echo gmdate('D, d M Y H:i:s'); ?> GMT</lastBuildDate>
-		<atom:link href="<?php echo _url ('index', 'index', 'output', 'rss'); ?>" rel="self" type="application/rss+xml" />
+		<atom:link href="<?php echo Minz_Url::display ($this->url, 'html', true); ?>" rel="self" type="application/rss+xml" />
 <?php
-$items = $this->entryPaginator->items ();
-foreach ($items as $item) {
+foreach ($this->entries as $item) {
 ?>
 		<item>
-			<title><?php echo htmlspecialchars(html_entity_decode($item->title (), ENT_NOQUOTES, 'UTF-8'), ENT_NOQUOTES, 'UTF-8'); ?></title>
+			<title><?php echo $item->title (); ?></title>
 			<link><?php echo $item->link (); ?></link>
 			<?php $author = $item->author (); ?>
 			<?php if ($author != '') { ?>
@@ -24,7 +23,7 @@ foreach ($items as $item) {
 			<pubDate><?php echo date('D, d M Y H:i:s O', $item->date (true)); ?></pubDate>
 			<guid isPermaLink="false"><?php echo $item->id (); ?></guid>
 		</item>
-<?php } ?>		
+<?php } ?>
 
 	</channel>
 </rss>

+ 0 - 0
app/views/importExport/export.phtml


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

@@ -0,0 +1,61 @@
+<?php $this->partial('aside_feed'); ?>
+
+<div class="post ">
+	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a>
+
+	<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 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>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo _t('import'); ?></button>
+			</div>
+		</div>
+	</form>
+
+	<?php if (count($this->feeds) > 0) { ?>
+	<form method="post" action="<?php echo _url('importExport', 'export'); ?>">
+		<legend><?php echo _t('export'); ?></legend>
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="export_opml">
+					<input type="checkbox" name="export_opml" id="export_opml" value="1" checked="checked" />
+					<?php echo _t('export_opml'); ?>
+				</label>
+
+				<label class="checkbox" for="export_starred">
+					<input type="checkbox" name="export_starred" id="export_starred" value="1" <?php echo extension_loaded('zip') ? 'checked="checked"' : ''; ?> />
+					<?php echo _t('export_starred'); ?>
+				</label>
+
+				<?php
+					$select_args = '';
+					if (extension_loaded('zip')) {
+						$select_args = ' size="' . min(10, count($this->feeds)) .'" multiple="multiple"';
+					}
+				?>
+				<select name="export_feeds[]"<?php echo $select_args; ?>>
+					<?php echo extension_loaded('zip') ? '' : '<option></option>'; ?>
+					<?php foreach ($this->feeds as $feed) { ?>
+					<option value="<?php echo $feed->id(); ?>"><?php echo $feed->name(); ?></option>
+					<?php } ?>
+				</select>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo _t('export'); ?></button>
+			</div>
+		</div>
+	</form>
+	<?php } ?>
+</div>

+ 16 - 13
app/views/index/about.phtml

@@ -1,24 +1,27 @@
 <div class="post content">
-	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Translate::t ('back_to_rss_feeds'); ?></a>
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
 
-	<h1><?php echo Translate::t ('about_freshrss'); ?></h1>
+	<h1><?php echo Minz_Translate::t ('about_freshrss'); ?></h1>
 
 	<dl class="infos">
-		<dt><?php echo Translate::t ('project_website'); ?></dt>
-		<dd><a href="http://marienfressinaud.github.io/FreshRSS/">http://marienfressinaud.github.io/FreshRSS/</a></dd>
+		<dt><?php echo Minz_Translate::t ('project_website'); ?></dt>
+		<dd><a href="<?php echo FRESHRSS_WEBSITE; ?>"><?php echo FRESHRSS_WEBSITE; ?></a></dd>
 
-		<dt><?php echo Translate::t ('lead_developer'); ?></dt>
-		<dd><a href="mailto:contact@marienfressinaud.fr">Marien Fressinaud</a> - <a href="http://marienfressinaud.fr"><?php echo Translate::t ('website'); ?></a></dd>
+		<dt><?php echo Minz_Translate::t ('lead_developer'); ?></dt>
+		<dd><a href="mailto:contact@marienfressinaud.fr">Marien Fressinaud</a> — <a href="http://marienfressinaud.fr"><?php echo Minz_Translate::t ('website'); ?></a></dd>
 
-		<dt><?php echo Translate::t ('bugs_reports'); ?></dt>
-		<dd><?php echo Translate::t ('github_or_email'); ?></dd>
+		<dt><?php echo Minz_Translate::t ('bugs_reports'); ?></dt>
+		<dd><?php echo Minz_Translate::t ('github_or_email'); ?></dd>
 
-		<dt><?php echo Translate::t ('license'); ?></dt>
-		<dd><?php echo Translate::t ('agpl3'); ?></dd>
+		<dt><?php echo Minz_Translate::t ('license'); ?></dt>
+		<dd><?php echo Minz_Translate::t ('agpl3'); ?></dd>
+
+		<dt><?php echo Minz_Translate::t ('version'); ?></dt>
+		<dd><?php echo FRESHRSS_VERSION; ?></dd>
 	</dl>
 
-	<p><?php echo Translate::t ('freshrss_description'); ?></p>
+	<p><?php echo Minz_Translate::t ('freshrss_description'); ?></p>
 
-	<h1><?php echo Translate::t ('credits'); ?></h1>
-	<p><?php echo Translate::t ('credits_content'); ?></p>
+	<h1><?php echo Minz_Translate::t ('credits'); ?></h1>
+	<p><?php echo Minz_Translate::t ('credits_content'); ?></p>
 </div>

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