Kaynağa Gözat

Sortie de la version 0.7

Marien Fressinaud 12 yıl önce
ebeveyn
işleme
165eb57459
100 değiştirilmiş dosya ile 6074 ekleme ve 4367 silme
  1. 144 45
      CHANGELOG
  2. 74 28
      README.md
  3. 0 29
      actualize_script.php
  4. 3 0
      app/.htaccess
  5. 0 93
      app/App_FrontController.php
  6. 356 0
      app/Controllers/configureController.php
  7. 158 0
      app/Controllers/entryController.php
  8. 4 4
      app/Controllers/errorController.php
  9. 425 0
      app/Controllers/feedController.php
  10. 357 0
      app/Controllers/indexController.php
  11. 46 0
      app/Controllers/javascriptController.php
  12. 178 0
      app/Controllers/usersController.php
  13. 6 0
      app/Exceptions/BadUrlException.php
  14. 7 0
      app/Exceptions/EntriesGetterException.php
  15. 1 2
      app/Exceptions/FeedException.php
  16. 6 0
      app/Exceptions/OpmlException.php
  17. 150 0
      app/FreshRSS.php
  18. 85 0
      app/Models/Category.php
  19. 251 0
      app/Models/CategoryDAO.php
  20. 247 0
      app/Models/Configuration.php
  21. 1 1
      app/Models/Days.php
  22. 186 0
      app/Models/Entry.php
  23. 472 0
      app/Models/EntryDAO.php
  24. 274 0
      app/Models/Feed.php
  25. 345 0
      app/Models/FeedDAO.php
  26. 26 0
      app/Models/Log.php
  27. 25 0
      app/Models/LogDAO.php
  28. 205 0
      app/Models/StatsDAO.php
  29. 106 0
      app/Models/Themes.php
  30. 36 0
      app/Models/UserDAO.php
  31. 59 0
      app/actualize_script.php
  32. 0 1
      app/configuration/.gitignore
  33. 0 375
      app/controllers/configureController.php
  34. 0 117
      app/controllers/entryController.php
  35. 0 363
      app/controllers/feedController.php
  36. 0 257
      app/controllers/indexController.php
  37. 0 13
      app/controllers/javascriptController.php
  38. 97 88
      app/i18n/en.php
  39. 94 85
      app/i18n/fr.php
  40. 67 0
      app/i18n/install.en.php
  41. 66 0
      app/i18n/install.fr.php
  42. 13 0
      app/index.html
  43. 17 8
      app/layout/aside_configure.phtml
  44. 17 17
      app/layout/aside_feed.phtml
  45. 57 50
      app/layout/aside_flux.phtml
  46. 67 39
      app/layout/header.phtml
  47. 16 11
      app/layout/layout.phtml
  48. 3 3
      app/layout/nav_entries.phtml
  49. 115 63
      app/layout/nav_menu.phtml
  50. 0 332
      app/models/Category.php
  51. 0 156
      app/models/EntriesGetter.php
  52. 0 590
      app/models/Entry.php
  53. 0 19
      app/models/Exception/FeedException.php
  54. 0 564
      app/models/Feed.php
  55. 0 47
      app/models/Log_Model.php
  56. 0 445
      app/models/RSSConfiguration.php
  57. 0 33
      app/models/RSSPaginator.php
  58. 0 47
      app/models/RSSThemes.php
  59. 59 0
      app/sql.php
  60. 58 0
      app/views/configure/archiving.phtml
  61. 11 11
      app/views/configure/categorize.phtml
  62. 86 112
      app/views/configure/display.phtml
  63. 77 47
      app/views/configure/feed.phtml
  64. 14 11
      app/views/configure/importExport.phtml
  65. 64 0
      app/views/configure/sharing.phtml
  66. 35 14
      app/views/configure/shortcut.phtml
  67. 162 0
      app/views/configure/users.phtml
  68. 9 8
      app/views/entry/bookmark.phtml
  69. 9 8
      app/views/entry/read.phtml
  70. 2 2
      app/views/error/index.phtml
  71. 1 1
      app/views/feed/actualize.phtml
  72. 43 38
      app/views/helpers/javascript_vars.phtml
  73. 8 8
      app/views/helpers/logs_pagination.phtml
  74. 17 11
      app/views/helpers/pagination.phtml
  75. 16 9
      app/views/helpers/view/global_view.phtml
  76. 180 92
      app/views/helpers/view/normal_view.phtml
  77. 11 10
      app/views/helpers/view/reader_view.phtml
  78. 5 6
      app/views/helpers/view/rss_view.phtml
  79. 16 13
      app/views/index/about.phtml
  80. 34 0
      app/views/index/formLogin.phtml
  81. 20 19
      app/views/index/index.phtml
  82. 10 6
      app/views/index/logs.phtml
  83. 125 0
      app/views/index/stats.phtml
  84. 19 15
      app/views/javascript/actualize.phtml
  85. 8 0
      app/views/javascript/nbUnreadsPerFeed.phtml
  86. 2 0
      app/views/javascript/nonce.phtml
  87. 0 1
      cache/.gitignore
  88. 17 0
      constants.php
  89. 8 0
      data/.gitignore
  90. 3 0
      data/.htaccess
  91. 1 0
      data/cache/.gitignore
  92. 13 0
      data/cache/index.html
  93. 2 0
      data/favicons/.gitignore
  94. 13 0
      data/favicons/index.html
  95. 13 0
      data/index.html
  96. 1 0
      data/log/.gitignore
  97. 13 0
      data/log/index.html
  98. 1 0
      data/persona/.gitignore
  99. 13 0
      data/persona/index.html
  100. 13 0
      index.html

+ 144 - 45
CHANGELOG

@@ -1,97 +1,195 @@
-# Changelog
-## 2013-11-21 changes with FreshRSS 0.6.1
+# Journal des modifications
+
+## 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
+* Affiche un message d’erreur plus explicite si fichier de configuration inaccessible
+
 
-## 2013-11-17 changes with FreshRSS 0.6
+## 2013-11-17 FreshRSS 0.6
 
 * Nettoyage du code JavaScript + optimisations
-* Utilisation d'adresses relatives
+* Utilisation dadresses relatives
 * Amélioration des performances coté client
-* Mise à jour automatique du nombre d'articles non lus
+* Mise à jour automatique du nombre darticles non lus
 * Corrections traductions
 * Mise en cache de FreshRSS
-* Amélioration des retours utilisateur lorsque la configuration n'est pas bonne
+* Amélioration des retours utilisateur lorsque la configuration nest 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)
+* Suppression de champs lors de linstallation (base_url et sel)
 * Correction bugs divers
 
-## 2013-10-15 changes with FreshRSS 0.5.1
+
+## 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 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
+## 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
@@ -106,6 +204,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

+ 74 - 28
README.md

@@ -1,45 +1,91 @@
 # 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.
+FreshRSS est un agrégateur de flux RSS à auto-héberger à limage de [Leed](http://projet.idleman.fr/leed/) ou de [Kriss Feed](http://tontof.net/kriss/feed/).
 
-* Site officiel : http://marienfressinaud.github.io/FreshRSS/
-* Démo : http://marienfressinaud.fr/projets/freshrss/
+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.6.1
-* Date de publication 2013-11-21
-* License AGPL3
+* Version actuelle : 0.7
+* Date de publication 2014-01-29
+* 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)
 
 # Disclaimer
-Cette application a été développée pour s'adapter à des besoins personnels et non professionels.
+Cette application a été développée pour sadapter à 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.
+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 Apache2 ou Nginx (non testé sur les autres)
-* PHP 5.2+ (PHP 5.3.3+ recommandé)
- * Requis : [libxml](http://php.net/xml), [cURL](http://php.net/curl), [PDO_MySQL](http://php.net/pdo-mysql)
- * Recommandés : [Zlib](http://php.net/zlib), [mbstring](http://php.net/mbstring), [iconv](http://php.net/iconv)
-* MySQL 5.0.3+ (SQLite à venir)
-* Un navigateur Web récent tel Firefox, Chrome, Opera, Safari, Internet Explorer 9+
- * Fonctionne aussi sur mobile
+* Serveur modeste, par exemple sous Linux ou Windows
+	* Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées)
+* Serveur Web Apache2 ou Nginx (non testé sur les autres)
+* PHP 5.2+ (PHP 5.3.7+ recommandé)
+	* Requis : [PDO_MySQL](http://php.net/pdo-mysql), [cURL](http://php.net/curl), [LibXML](http://php.net/xml), [PCRE](http://php.net/pcre), [ctype](http://php.net/ctype)
+	* Recommandés : [JSON](http://php.net/json), [zlib](http://php.net/zlib), [mbstring](http://php.net/mbstring), [iconv](http://php.net/iconv)
+* MySQL 5.0.3+ (ou SQLite 3.7.4+ à venir)
+* Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
+	* Fonctionne aussi sur mobile
 
-![Capture d'écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
+![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. 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. 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 :
+* 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/actualize_script.php >/dev/null 2>&1
+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/)
+
+## Uniquement pour certaines options
+* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
+* [phpQuery](http://code.google.com/p/phpquery/)
+* [Lazy Load](http://www.appelsiini.net/projects/lazyload)
+
+## 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)

+ 0 - 29
actualize_script.php

@@ -1,29 +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 ();
-touch(PUBLIC_PATH . '/data/touch.txt');

+ 3 - 0
app/.htaccess

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

+ 0 - 93
app/App_FrontController.php

@@ -1,93 +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');
-	}
-
-	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_Model.php');
-	}
-
-	private function loadParamsView () {
-		try {
-			$this->conf = Session::param ('conf', new RSSConfiguration ());
-		} catch(MinzException $e) {
-			// Permission denied or conf file does not exist
-			// it's critical!
-			print $e->getMessage();
-			exit();
-		}
-
-		View::_param ('conf', $this->conf);
-
-		$entryDAO = new EntryDAO ();
-		View::_param ('nb_not_read', $entryDAO->countNotRead ());
-
-		Session::_param ('language', $this->conf->language ());
-
-		$output = Request::param ('output');
-		if(!$output) {
-			$output = $this->conf->viewMode();
-			Request::_param ('output', $output);
-		}
-	}
-
-	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 . '?' . @filemtime(PUBLIC_PATH . '/themes/' . $theme['path'] . '/' . $file)));
-			}
-		}
-		View::appendStyle (Url::display ('/themes/printer/style.css?' . @filemtime(PUBLIC_PATH . '/themes/printer/style.css')), 'print');
-
-		if (login_is_conf ($this->conf)) {
-			View::appendScript ('https://login.persona.org/include.js');
-		}
-		$includeLazyLoad = $this->conf->lazyload () === 'yes' && ($this->conf->displayPosts () === 'yes' || Request::param ('output') === 'reader');
-		View::appendScript (Url::display ('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')), false, !$includeLazyLoad, !$includeLazyLoad);
-		if ($includeLazyLoad) {
-			View::appendScript (Url::display ('/scripts/jquery.lazyload.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.lazyload.min.js')));
-		}
-		View::appendScript (Url::display ('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
-	}
-
-	private function loadNotifications () {
-		$notif = Session::param ('notification');
-		if ($notif) {
-			View::_param ('notification', $notif);
-			Session::_param ('notification');
-		}
-	}
-}

+ 356 - 0
app/Controllers/configureController.php

@@ -0,0 +1,356 @@
+<?php
+
+class FreshRSS_configure_Controller extends Minz_ActionController {
+	public function firstAction () {
+		if (!$this->view->loginOk) {
+			Minz_Error::error (
+				403,
+				array ('error' => array (Minz_Translate::t ('access_denied')))
+			);
+		}
+
+		$catDAO = new FreshRSS_CategoryDAO ();
+		$catDAO->checkDefault ();
+	}
+
+	public function categorizeAction () {
+		$feedDAO = new FreshRSS_FeedDAO ();
+		$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 (),
+						'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 FreshRSS_Category ($newCat);
+				$values = array (
+					'id' => $cat->id (),
+					'name' => $cat->name (),
+					'color' => $cat->color ()
+				);
+
+				if ($catDAO->searchByName ($newCat) == false) {
+					$catDAO->addCategory ($values);
+				}
+			}
+			invalidateHttpCache();
+
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('categories_updated')
+			);
+			Minz_Session::_param ('notification', $notif);
+
+			Minz_Request::forward (array ('c' => 'configure', 'a' => 'categorize'), true);
+		}
+
+		$this->view->categories = $catDAO->listCategories (false);
+		$this->view->defaultCategory = $catDAO->getDefault ();
+		$this->view->feeds = $feedDAO->listFeeds ();
+		$this->view->flux = false;
+
+		Minz_View::prependTitle (Minz_Translate::t ('categories_management') . ' · ');
+	}
+
+	public function feedAction () {
+		$catDAO = new FreshRSS_CategoryDAO ();
+		$this->view->categories = $catDAO->listCategories (false);
+
+		$feedDAO = new FreshRSS_FeedDAO ();
+		$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 (Minz_Translate::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)),
+					);
+
+					if ($feedDAO->updateFeed ($id, $values)) {
+						$this->view->flux->_category ($cat);
+
+						$notif = array (
+							'type' => 'good',
+							'content' => Minz_Translate::t ('feed_updated')
+						);
+					} else {
+						$notif = array (
+							'type' => 'bad',
+							'content' => Minz_Translate::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 (Minz_Translate::t ('rss_feed_management') . ' — ' . $this->view->flux->name () . ' · ');
+			}
+		} else {
+			Minz_View::prependTitle (Minz_Translate::t ('rss_feed_management') . ' · ');
+		}
+	}
+
+	public function displayAction () {
+		if (Minz_Request::isPost()) {
+			$this->view->conf->_language(Minz_Request::param('language', 'en'));
+			$this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10));
+			$this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal'));
+			$this->view->conf->_default_view (Minz_Request::param('default_view', 'a'));
+			$this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false));
+			$this->view->conf->_display_posts(Minz_Request::param('display_posts', false));
+			$this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false));
+			$this->view->conf->_lazyload (Minz_Request::param('lazyload', false));
+			$this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC'));
+			$this->view->conf->_mark_when (array(
+				'article' => Minz_Request::param('mark_open_article', false),
+				'site' => Minz_Request::param('mark_open_site', false),
+				'scroll' => Minz_Request::param('mark_scroll', false),
+				'reception' => Minz_Request::param('mark_upon_reception', false),
+			));
+			$themeId = Minz_Request::param('theme', '');
+			if ($themeId == '') {
+				$themeId = FreshRSS_Themes::defaultTheme;
+			}
+			$this->view->conf->_theme($themeId);
+			$this->view->conf->_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->save();
+
+			Minz_Session::_param ('language', $this->view->conf->language);
+			Minz_Translate::reset ();
+			invalidateHttpCache();
+
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('configuration_updated')
+			);
+			Minz_Session::_param ('notification', $notif);
+
+			Minz_Request::forward (array ('c' => 'configure', 'a' => 'display'), true);
+		}
+
+		$this->view->themes = FreshRSS_Themes::get();
+
+		Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' · ');
+	}
+
+	public function sharingAction () {
+		if (Minz_Request::isPost ()) {
+			$this->view->conf->_sharing (array(
+				'shaarli' => Minz_Request::param ('shaarli', false),
+				'wallabag' => Minz_Request::param ('wallabag', false),
+				'diaspora' => Minz_Request::param ('diaspora', false),
+				'twitter' => Minz_Request::param ('twitter', false),
+				'g+' => Minz_Request::param ('g+', false),
+				'facebook' => Minz_Request::param ('facebook', false),
+				'email' => Minz_Request::param ('email', false),
+				'print' => Minz_Request::param ('print', false),
+			));
+			$this->view->conf->save();
+			invalidateHttpCache();
+
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('configuration_updated')
+			);
+			Minz_Session::_param ('notification', $notif);
+
+			Minz_Request::forward (array ('c' => 'configure', 'a' => 'sharing'), true);
+		}
+
+		Minz_View::prependTitle (Minz_Translate::t ('sharing') . ' · ');
+	}
+
+	public function importExportAction () {
+		require_once(LIB_PATH . '/lib_opml.php');
+		$catDAO = new FreshRSS_CategoryDAO ();
+		$this->view->categories = $catDAO->listCategories ();
+
+		$this->view->req = Minz_Request::param ('q');
+
+		if ($this->view->req == 'export') {
+			Minz_View::_title ('freshrss_feeds.opml');
+
+			$this->view->_useLayout (false);
+			header('Content-Type: application/xml; charset=utf-8');
+			header('Content-disposition: attachment; filename=freshrss_feeds.opml');
+
+			$feedDAO = new FreshRSS_FeedDAO ();
+			$catDAO = new FreshRSS_CategoryDAO ();
+
+			$list = array ();
+			foreach ($catDAO->listCategories () as $key => $cat) {
+				$list[$key]['name'] = $cat->name ();
+				$list[$key]['feeds'] = $feedDAO->listByCategory ($cat->id ());
+			}
+
+			$this->view->categories = $list;
+		} elseif ($this->view->req == 'import' && Minz_Request::isPost ()) {
+			if ($_FILES['file']['error'] == 0) {
+				invalidateHttpCache();
+				// on parse le fichier OPML pour récupérer les catégories et les flux associés
+				try {
+					list ($categories, $feeds) = opml_import (
+						file_get_contents ($_FILES['file']['tmp_name'])
+					);
+
+					// On redirige vers le controller feed qui va se charger d'insérer les flux en BDD
+					// les flux sont mis au préalable dans des variables de Request
+					Minz_Request::_param ('q', 'null');
+					Minz_Request::_param ('categories', $categories);
+					Minz_Request::_param ('feeds', $feeds);
+					Minz_Request::forward (array ('c' => 'feed', 'a' => 'massiveImport'));
+				} catch (FreshRSS_Opml_Exception $e) {
+					Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
+
+					$notif = array (
+						'type' => 'bad',
+						'content' => Minz_Translate::t ('bad_opml_file')
+					);
+					Minz_Session::_param ('notification', $notif);
+
+					Minz_Request::forward (array (
+						'c' => 'configure',
+						'a' => 'importExport'
+					), true);
+				}
+			}
+		}
+
+		$feedDAO = new FreshRSS_FeedDAO ();
+		$this->view->feeds = $feedDAO->listFeeds ();
+
+		// au niveau de la vue, permet de ne pas voir un flux sélectionné dans la liste
+		$this->view->flux = false;
+
+		Minz_View::prependTitle (Minz_Translate::t ('import_export_opml') . ' · ');
+	}
+
+	public function shortcutAction () {
+		$list_keys = array ('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter',
+		                    'escape', 'f', 'g', 'h', 'i', 'insert', 'j', 'k', 'l', 'left',
+		                    '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;
+
+		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();
+
+			$notif = array (
+				'type' => 'good',
+				'content' => Minz_Translate::t ('shortcuts_updated')
+			);
+			Minz_Session::_param ('notification', $notif);
+
+			Minz_Request::forward (array ('c' => 'configure', 'a' => 'shortcut'), true);
+		}
+
+		Minz_View::prependTitle (Minz_Translate::t ('shortcuts') . ' · ');
+	}
+
+	public function usersAction() {
+		Minz_View::prependTitle(Minz_Translate::t ('users') . ' · ');
+	}
+
+	public function archivingAction () {
+		if (Minz_Request::isPost()) {
+			$old = Minz_Request::param('old_entries', 3);
+			$keepHistoryDefault = Minz_Request::param('keep_history_default', 0);
+
+			$this->view->conf->_old_entries($old);
+			$this->view->conf->_keep_history_default($keepHistoryDefault);
+			$this->view->conf->save();
+			invalidateHttpCache();
+
+			$notif = array(
+				'type' => 'good',
+				'content' => Minz_Translate::t('configuration_updated')
+			);
+			Minz_Session::_param('notification', $notif);
+
+			Minz_Request::forward(array('c' => 'configure', 'a' => 'archiving'), true);
+		}
+
+		Minz_View::prependTitle(Minz_Translate::t('archiving_configuration') . ' · ');
+
+		$entryDAO = new FreshRSS_EntryDAO();
+		$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);
+		}
+	}
+}

+ 158 - 0
app/Controllers/entryController.php

@@ -0,0 +1,158 @@
+<?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 = new FreshRSS_EntryDAO ();
+		if ($id == false) {
+			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 = new FreshRSS_EntryDAO ();
+			$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 = new FreshRSS_EntryDAO();
+			$entryDAO->optimizeTable();
+
+			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 = new FreshRSS_FeedDAO();
+		$feeds = $feedDAO->listFeedsOrderUpdate();
+		$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());
+				}
+			}
+		}
+
+		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);
+	}
+}

+ 4 - 4
app/controllers/errorController.php → app/Controllers/errorController.php

@@ -1,8 +1,8 @@
 <?php
 
-class ErrorController extends ActionController {
+class FreshRSS_error_Controller extends Minz_ActionController {
 	public function indexAction () {
-		switch (Request::param ('code')) {
+		switch (Minz_Request::param ('code')) {
 		case 403:
 			$this->view->code = 'Error 403 - Forbidden';
 			break;
@@ -19,8 +19,8 @@ class ErrorController extends ActionController {
 			$this->view->code = 'Error 404 - Not found';
 		}
 		
-		$this->view->logs = Request::param ('logs');
+		$this->view->logs = Minz_Request::param ('logs');
 		
-		View::prependTitle ($this->view->code . ' - ');
+		Minz_View::prependTitle ($this->view->code . ' · ');
 	}
 }

+ 425 - 0
app/Controllers/feedController.php

@@ -0,0 +1,425 @@
+<?php
+
+class FreshRSS_feed_Controller extends Minz_ActionController {
+	public function firstAction () {
+		if (!$this->view->loginOk) {
+			$token = $this->view->conf->token;	//TODO: check the token logic again, and if it is still needed
+			$token_param = Minz_Request::param ('token', '');
+			$token_is_ok = ($token != '' && $token == $token_param);
+			$action = Minz_Request::actionName ();
+			if (!($token_is_ok && $action === 'actualize')) {
+				Minz_Error::error (
+					403,
+					array ('error' => array (Minz_Translate::t ('access_denied')))
+				);
+			}
+		}
+
+		$this->catDAO = new FreshRSS_CategoryDAO ();
+		$this->catDAO->checkDefault ();
+	}
+
+	public function addAction () {
+		@set_time_limit(300);
+
+		if (Minz_Request::isPost ()) {
+			$url = Minz_Request::param ('url_rss');
+			$cat = Minz_Request::param ('category', false);
+			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);
+
+				$feedDAO = new FreshRSS_FeedDAO ();
+				$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 = new FreshRSS_EntryDAO ();
+						$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);
+
+						$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);
+						}
+						$feedDAO->updateLastUpdate ($feed->id ());
+						$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_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_Session::_param ('notification', $notif);
+			}
+			if ($transactionStarted) {
+				$feedDAO->rollBack ();
+			}
+
+			Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true);
+		}
+	}
+
+	public function truncateAction () {
+		if (Minz_Request::isPost ()) {
+			$id = Minz_Request::param ('id');
+			$feedDAO = new FreshRSS_FeedDAO ();
+			$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 = new FreshRSS_FeedDAO ();
+		$entryDAO = new FreshRSS_EntryDAO ();
+
+		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 ();
+		}
+
+		// 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) {
+			try {
+				$url = $feed->url();
+				$feed->load(false);
+				$entries = array_reverse($feed->entries());	//We want chronological order and SimplePie uses reverse order
+
+				//For this feed, check last n entry GUIDs already in database
+				$existingGuids = array_fill_keys ($entryDAO->listLastGuidsByFeed ($feed->id (), count($entries) + 10), 1);
+				$useDeclaredDate = empty($existingGuids);
+
+				$feedHistory = $feed->keepHistory();
+				if ($feedHistory == -2) {	//default
+					$feedHistory = $this->view->conf->keep_history_default;
+				}
+
+				// On ne vérifie pas strictement que l'article n'est pas déjà en BDD
+				// La BDD refusera l'ajout car (id_feed, guid) doit être unique
+				$feedDAO->beginTransaction ();
+				foreach ($entries as $entry) {
+					$eDate = $entry->date (true);
+					if ((!isset ($existingGuids[$entry->guid ()])) &&
+						(($feedHistory != 0) || ($eDate  >= $date_min))) {
+						$values = $entry->toArray ();
+						//Use declared date at first import, otherwise use discovery date
+						$values['id'] = ($useDeclaredDate || $eDate < $date_min) ?
+							min(time(), $eDate) . uSecString() :
+							uTimeString();
+						$values['is_read'] = $is_read;
+						$entryDAO->addEntry ($values);
+					}
+				}
+
+				if (($feedHistory >= 0) && (rand(0, 30) === 1)) {
+					$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 ());
+				$feedDAO->commit ();
+				$flux_update++;
+				if ($feed->url() !== $url) {	//URL has changed (auto-discovery)
+					$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
+				}
+				$feed->faviconPrepare();
+			} catch (FreshRSS_Feed_Exception $e) {
+				Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
+				$feedDAO->updateLastUpdate ($feed->id (), 1);
+			}
+
+			// 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
+			$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' => 'bad',
+				'content' => Minz_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 (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 massiveImportAction () {
+		@set_time_limit(300);
+
+		$entryDAO = new FreshRSS_EntryDAO ();
+		$feedDAO = new FreshRSS_FeedDAO ();
+
+		$categories = Minz_Request::param ('categories', array (), true);
+		$feeds = Minz_Request::param ('feeds', array (), true);
+
+		// on ajoute les catégories en masse dans une fonction à part
+		$this->addCategories ($categories);
+
+		// on calcule la date des articles les plus anciens qu'on accepte
+		$nb_month_old = $this->view->conf->old_entries;
+		$date_min = time () - (3600 * 24 * 30 * $nb_month_old);
+
+		// la variable $error permet de savoir si une erreur est survenue
+		// Le but est de ne pas arrêter l'import même en cas d'erreur
+		// L'utilisateur sera mis au courant s'il y a eu des erreurs, mais
+		// ne connaîtra pas les détails. Ceux-ci seront toutefois logguées
+		$error = false;
+		$i = 0;
+		foreach ($feeds as $feed) {
+			try {
+				$values = array (
+					'id' => $feed->id (),
+					'url' => $feed->url (),
+					'category' => $feed->category (),
+					'name' => $feed->name (),
+					'website' => $feed->website (),
+					'description' => $feed->description (),
+					'lastUpdate' => 0,
+					'httpAuth' => $feed->httpAuth ()
+				);
+
+				// ajout du flux que s'il n'est pas déjà en BDD
+				if (!$feedDAO->searchByUrl ($values['url'])) {
+					$id = $feedDAO->addFeed ($values);
+					if ($id) {
+						$feed->_id ($id);
+						$feed->faviconPrepare();
+					} else {
+						$error = true;
+					}
+				}
+			} catch (FreshRSS_Feed_Exception $e) {
+				$error = true;
+				Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
+			}
+		}
+
+		if ($error) {
+			$res = Minz_Translate::t ('feeds_imported_with_errors');
+		} else {
+			$res = Minz_Translate::t ('feeds_imported');
+		}
+
+		$notif = array (
+			'type' => 'good',
+			'content' => $res
+		);
+		Minz_Session::_param ('notification', $notif);
+		Minz_Session::_param ('actualize_feeds', true);
+
+		// et on redirige vers la page d'accueil
+		Minz_Request::forward (array (
+			'c' => 'index',
+			'a' => 'index'
+		), true);
+	}
+
+	public function deleteAction () {
+		if (Minz_Request::isPost ()) {
+			$type = Minz_Request::param ('type', 'feed');
+			$id = Minz_Request::param ('id');
+
+			$feedDAO = new FreshRSS_FeedDAO ();
+			if ($type == 'category') {
+				if ($feedDAO->deleteFeedByCategory ($id)) {
+					$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)) {
+					$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);
+
+			if ($type == 'category') {
+				Minz_Request::forward (array ('c' => 'configure', 'a' => 'categorize'), true);
+			} else {
+				Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed'), true);
+			}
+		}
+	}
+
+	private function addCategories ($categories) {
+		foreach ($categories as $cat) {
+			if (!$this->catDAO->searchByName ($cat->name ())) {
+				$values = array (
+					'id' => $cat->id (),
+					'name' => $cat->name (),
+					'color' => $cat->color ()
+				);
+				$catDAO->addCategory ($values);
+			}
+		}
+	}
+}

+ 357 - 0
app/Controllers/indexController.php

@@ -0,0 +1,357 @@
+<?php
+
+class FreshRSS_index_Controller extends Minz_ActionController {
+	private $nb_not_read_cat = 0;
+
+	public function indexAction () {
+		$output = Minz_Request::param ('output');
+		$token = '';
+
+		// check if user is logged in
+		if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous())
+		{
+			$token = $this->view->conf->token;
+			$token_param = Minz_Request::param ('token', '');
+			$token_is_ok = ($token != '' && $token === $token_param);
+			if (!($output === 'rss' && $token_is_ok)) {
+				return;
+			}
+			$params['token'] = $token;
+		}
+
+		// construction of RSS url of this feed
+		$params = Minz_Request::params ();
+		$params['output'] = 'rss';
+		if (isset ($params['search'])) {
+			$params['search'] = urlencode ($params['search']);
+		}
+		$this->view->rss_url = array (
+			'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 = new FreshRSS_EntryDAO();
+
+		$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();
+		if ($this->view->nb_not_read > 0) {
+			Minz_View::appendTitle (' (' . formatNumber($this->view->nb_not_read) . ')');
+		}
+		Minz_View::prependTitle (
+			$this->view->currentName .
+			($this->nb_not_read_cat > 0 ? ' (' . formatNumber($this->nb_not_read_cat) . ')' : '') .
+			' · '
+		);
+
+		// On récupère les différents éléments de filtrage
+		$this->view->state = $state = Minz_Request::param ('state', $this->view->conf->default_view);
+		$filter = Minz_Request::param ('search', '');
+		if (!empty($filter)) {
+			$state = 'all';	//Search always in read and unread articles
+		}
+		$this->view->order = $order = Minz_Request::param ('order', $this->view->conf->sort_order);
+		$nb = Minz_Request::param ('nb', $this->view->conf->posts_per_page);
+		$first = Minz_Request::param ('next', '');
+
+		if ($state === 'not_read') {	//Any unread article in this category at all?
+			switch ($getType) {
+				case 'a':
+					$hasUnread = $this->view->nb_not_read > 0;
+					break;
+				case 's':
+					$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) {
+				$this->view->state = $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, $state, $order, $nb + 1, $first, $filter, $date_min, $keepHistoryDefault);
+
+			// Si on a récupéré aucun article "non lus"
+			// on essaye de récupérer tous les articles
+			if ($state === 'not_read' && empty($entries)) {
+				Minz_Log::record ('Conflicting information about nbNotRead!', Minz_Log::DEBUG);
+				$this->view->state = 'all';
+				$entries = $entryDAO->listWhere($getType, $getId, 'all', $order, $nb, $first, $filter, $date_min, $keepHistoryDefault);
+			}
+
+			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 = new FreshRSS_FeedDAO();
+					$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 statsAction () {
+		if (!$this->view->loginOk) {
+			Minz_Error::error (
+				403,
+				array ('error' => array (Minz_Translate::t ('access_denied')))
+			);
+		}
+
+		Minz_View::prependTitle (Minz_Translate::t ('stats') . ' · ');
+
+		$statsDAO = new FreshRSS_StatsDAO ();
+		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();
+	}
+
+	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');
+	}
+
+	public function formLoginAction () {
+		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);
+					} 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);
+		}
+		invalidateHttpCache();
+	}
+
+	public function formLogoutAction () {
+		$this->view->_useLayout(false);
+		invalidateHttpCache();
+		Minz_Session::_param('currentUser');
+		Minz_Session::_param('mail');
+		Minz_Session::_param('passwordHash');
+		Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+	}
+}

+ 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 = new FreshRSS_FeedDAO ();
+		$this->view->feeds = $feedDAO->listFeeds ();
+	}
+
+	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 = '';
+	}
+}

+ 178 - 0
app/Controllers/usersController.php

@@ -0,0 +1,178 @@
+<?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', false);
+			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);
+
+			if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+				$this->view->conf->_mail_login(Minz_Request::param('mail_login', false));
+			}
+			$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');
+				$auth_type = Minz_Request::param('auth_type', 'none');
+				if ($anon != Minz_Configuration::allowAnonymous() ||
+					$auth_type != Minz_Configuration::authType()) {
+					Minz_Configuration::_authType($auth_type);
+					Minz_Configuration::_allowAnonymous($anon);
+					$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', '_'))) {
+			require_once(APP_PATH . '/sql.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', false);
+				$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', '_'))) {
+			require_once(APP_PATH . '/sql.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);
 	}

+ 6 - 0
app/Exceptions/OpmlException.php

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

+ 150 - 0
app/FreshRSS.php

@@ -0,0 +1,150 @@
+<?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();
+		$this->loadStylesAndScripts($loginOk);	//TODO: Do not load that when not needed, e.g. some Ajax requests
+		$this->loadNotifications();
+	}
+
+	private function accessControl($currentUser) {
+		if ($currentUser == '') {
+			switch (Minz_Configuration::authType()) {
+				case 'form':
+					$currentUser = Minz_Configuration::defaultUser();
+					Minz_Session::_param('passwordHash');
+					$loginOk = false;
+					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;
+			}
+			if ((!$loginOk) && (PHP_SAPI === 'cli') && (Minz_Request::actionName() === 'actualize')) {	//Command line
+				Minz_Configuration::_authType('none');
+				$loginOk = true;
+			}
+		}
+		Minz_View::_param ('loginOk', $loginOk);
+		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) {
+				Minz_View::appendStyle (Minz_Url::display ('/themes/' . $theme['id'] . '/' . $file . '?' . @filemtime(PUBLIC_PATH . '/themes/' . $theme['id'] . '/' . $file)));
+			}
+		}
+
+		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;
+		}
+		$includeLazyLoad = $this->conf->lazyload && ($this->conf->display_posts || Minz_Request::param ('output') === 'reader');
+		Minz_View::appendScript (Minz_Url::display ('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')), false, !$includeLazyLoad, !$includeLazyLoad);
+		if ($includeLazyLoad) {
+			Minz_View::appendScript (Minz_Url::display ('/scripts/jquery.lazyload.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.lazyload.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');
+		}
+	}
+}

+ 85 - 0
app/Models/Category.php

@@ -0,0 +1,85 @@
+<?php
+
+class FreshRSS_Category extends Minz_Model {
+	private $id = 0;
+	private $name;
+	private $color;
+	private $nbFeed = -1;
+	private $nbNotRead = -1;
+	private $feeds = null;
+
+	public function __construct ($name = '', $color = '#0062BE', $feeds = null) {
+		$this->_name ($name);
+		$this->_color ($color);
+		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 color () {
+		return $this->color;
+	}
+	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 = new FreshRSS_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;
+	}
+}

+ 251 - 0
app/Models/CategoryDAO.php

@@ -0,0 +1,251 @@
+<?php
+
+class FreshRSS_CategoryDAO extends Minz_ModelPdo {
+	public function addCategory ($valuesTmp) {
+		$sql = 'INSERT INTO `' . $this->prefix . 'category` (name, color) VALUES(?, ?)';
+		$stm = $this->bd->prepare ($sql);
+
+		$values = array (
+			substr($valuesTmp['name'], 0, 255),
+			substr($valuesTmp['color'], 0, 7),
+		);
+
+		if ($stm && $stm->execute ($values)) {
+			return $this->bd->lastInsertId();
+		} 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 $stm->rowCount();
+		} 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 $stm->rowCount();
+		} 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 = self::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 = self::daoToCategory ($res);
+
+		if (isset ($cat[0])) {
+			return $cat[0];
+		} else {
+			return false;
+		}
+	}
+
+	public function listCategories ($prePopulateFeeds = true, $details = false) {
+		if ($prePopulateFeeds) {
+			$sql = 'SELECT c.id AS c_id, c.name AS c_name, '
+			     . ($details ? 'c.color AS c_color, ' : '')
+			     . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ')
+			     . 'FROM `' . $this->prefix . 'category` c '
+			     . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category = c.id '
+			     . '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 === false) {
+			$cat = new FreshRSS_Category (Minz_Translate::t ('default_category'));
+			$cat->_id (1);
+
+			$values = array (
+				'id' => $cat->id (),
+				'name' => $cat->name (),
+				'color' => $cat->color ()
+			);
+
+			$this->addCategory ($values);
+		}
+	}
+
+	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'],
+					isset($previousLine['c_color']) ? $previousLine['c_color'] : '',
+					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'],
+				isset($previousLine['c_color']) ? $previousLine['c_color'] : '',
+				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'],
+				$dao['color']
+			);
+			$cat->_id ($dao['id']);
+			$list[$key] = $cat;
+		}
+
+		return $list;
+	}
+}

+ 247 - 0
app/Models/Configuration.php

@@ -0,0 +1,247 @@
+<?php
+
+class FreshRSS_Configuration {
+	private $filename;
+
+	private $data = array(
+		'language' => 'en',
+		'old_entries' => 3,
+		'keep_history_default' => 0,
+		'mail_login' => '',
+		'token' => '',
+		'passwordHash' => '',	//CRYPT_BLOWFISH
+		'posts_per_page' => 20,
+		'view_mode' => 'normal',
+		'default_view' => 'not_read',
+		'auto_load_more' => true,
+		'display_posts' => false,
+		'onread_jump_next' => true,
+		'lazyload' => true,
+		'sort_order' => 'DESC',
+		'anon_access' => false,
+		'mark_when' => array(
+			'article' => true,
+			'site' => true,
+			'scroll' => false,
+			'reception' => false,
+		),
+		'theme' => 'Origine',
+		'shortcuts' => array(
+			'mark_read' => 'r',
+			'mark_favorite' => 'f',
+			'go_website' => 'space',
+			'next_entry' => 'j',
+			'prev_entry' => 'k',
+			'collapse_entry' => 'c',
+			'load_more' => 'm',
+			'auto_share' => 's',
+		),
+		'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(
+			'shaarli' => '',
+			'wallabag' => '',
+			'diaspora' => '',
+			'twitter' => true,
+			'g+' => true,
+			'facebook' => true,
+			'email' => true,
+			'print' => true,
+		),
+	);
+
+	private $available_languages = array(
+		'en' => 'English',
+		'fr' => 'Français',
+	);
+
+	public function __construct ($user) {
+		$this->filename = DATA_PATH . '/' . $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;
+	}
+
+	public function save() {
+		@rename($this->filename, $this->filename . '.bak.php');
+		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 sharing($key = false) {
+		if ($key === false) {
+			return $this->data['sharing'];
+		}
+		if (isset($this->data['sharing'][$key])) {
+			return $this->data['sharing'][$key];
+		}
+		return false;
+	}
+
+	public function availableLanguages() {
+		return $this->available_languages;
+	}
+
+	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) {
+		$this->data['default_view'] = $value === 'all' ? 'all' : 'not_read';
+	}
+	public function _display_posts ($value) {
+		$this->data['display_posts'] = ((bool)$value) && $value !== 'no';
+	}
+	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 _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 _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 _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) {
+		$are_url = array ('shaarli', 'wallabag', 'diaspora');
+		foreach ($values as $key => $value) {
+			if (in_array($key, $are_url)) {
+				$is_url = (
+					filter_var ($value, FILTER_VALIDATE_URL) ||
+					(version_compare(PHP_VERSION, '5.3.3', '<') &&
+						(strpos($value, '-') > 0) &&
+						($value === filter_var($value, FILTER_SANITIZE_URL)))
+				);  //PHP bug #51192
+
+				if (!$is_url) {
+					$value = '';
+				}
+			} elseif (!is_bool($value)) {
+				$value = true;
+			}
+
+			$this->data['sharing'][$key] = $value;
+		}
+	}
+	public function _theme($value) {
+		$this->data['theme'] = $value;
+	}
+	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;

+ 186 - 0
app/Models/Entry.php

@@ -0,0 +1,186 @@
+<?php
+
+class FreshRSS_Entry extends Minz_Model {
+
+	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 = new FreshRSS_FeedDAO ();
+			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 = new FreshRSS_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(
+						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),
+		);
+	}
+}

+ 472 - 0
app/Models/EntryDAO.php

@@ -0,0 +1,472 @@
+<?php
+
+class FreshRSS_EntryDAO extends Minz_ModelPdo {
+	public function addEntry ($valuesTmp) {
+		$sql = 'INSERT INTO `' . $this->prefix . 'entry`(id, guid, title, author, content_bin, link, date, is_read, is_favorite, id_feed, tags) '
+		     . 'VALUES(?, ?, ?, ?, COMPRESS(?), ?, ?, ?, ?, ?, ?)';
+		$stm = $this->bd->prepare ($sql);
+
+		$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->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]
+				. ' 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 markFavorite ($id, $is_favorite = true) {
+		$sql = 'UPDATE `' . $this->prefix . 'entry` e '
+		     . 'SET e.is_favorite = ? '
+		     . 'WHERE e.id=?';
+		$values = array ($is_favorite ? 1 : 0, $id);
+		$stm = $this->bd->prepare ($sql);
+		if ($stm && $stm->execute ($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+	public function markRead ($id, $is_read = true) {
+		$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
+		     . 'SET e.is_read = ?,'
+		     . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
+		     . 'WHERE e.id=?';
+		$values = array ($is_read ? 1 : 0, $id);
+		$stm = $this->bd->prepare ($sql);
+		if ($stm && $stm->execute ($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+	public function markReadEntries ($idMax = 0, $favorites = false) {
+		if ($idMax === 0) {
+			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
+			     . 'SET e.is_read = 1, f.cache_nbUnreads=0 '
+			     . 'WHERE e.is_read = 0 AND ';
+			if ($favorites) {
+				$sql .= 'e.is_favorite = 1';
+			} else {
+				$sql .= 'f.priority > 0';
+			}
+			$stm = $this->bd->prepare ($sql);
+			if ($stm && $stm->execute ()) {
+				return $stm->rowCount();
+			} else {
+				$info = $stm->errorInfo();
+				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+				return false;
+			}
+		} else {
+			$this->bd->beginTransaction ();
+
+			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
+			     . 'SET e.is_read = 1 '
+			     . 'WHERE e.is_read = 0 AND e.id <= ? AND ';
+			if ($favorites) {
+				$sql .= 'e.is_favorite = 1';
+			} else {
+				$sql .= 'f.priority > 0';
+			}
+			$values = array ($idMax);
+			$stm = $this->bd->prepare ($sql);
+			if (!($stm && $stm->execute ($values))) {
+				$info = $stm->errorInfo();
+				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+				$this->bd->rollBack ();
+				return false;
+			}
+			$affected = $stm->rowCount();
+
+			if ($affected > 0) {
+				$sql = 'UPDATE `' . $this->prefix . 'feed` f '
+			     . 'LEFT OUTER JOIN ('
+			     .	'SELECT e.id_feed, '
+			     .	'COUNT(*) AS nbUnreads '
+			     .	'FROM `' . $this->prefix . 'entry` e '
+			     .	'WHERE e.is_read = 0 '
+			     .	'GROUP BY e.id_feed'
+			     . ') x ON x.id_feed=f.id '
+			     . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0)';
+				$stm = $this->bd->prepare ($sql);
+				if (!($stm && $stm->execute ())) {
+					$info = $stm->errorInfo();
+					Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+					$this->bd->rollBack ();
+					return false;
+				}
+			}
+
+			$this->bd->commit ();
+			return $affected;
+		}
+	}
+	public function markReadCat ($id, $idMax = 0) {
+		if ($idMax === 0) {
+			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
+			     . 'SET e.is_read = 1, f.cache_nbUnreads=0 '
+			     . 'WHERE f.category = ? AND e.is_read = 0';
+			$values = array ($id);
+			$stm = $this->bd->prepare ($sql);
+			if ($stm && $stm->execute ($values)) {
+				return $stm->rowCount();
+			} else {
+				$info = $stm->errorInfo();
+				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+				return false;
+			}
+		} else {
+			$this->bd->beginTransaction ();
+
+			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
+			     . '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->errorInfo();
+				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+				$this->bd->rollBack ();
+				return false;
+			}
+			$affected = $stm->rowCount();
+
+			if ($affected > 0) {
+				$sql = 'UPDATE `' . $this->prefix . 'feed` f '
+			     . 'LEFT OUTER JOIN ('
+			     .	'SELECT e.id_feed, '
+			     .	'COUNT(*) AS nbUnreads '
+			     .	'FROM `' . $this->prefix . 'entry` e '
+			     .	'WHERE e.is_read = 0 '
+			     .	'GROUP BY e.id_feed'
+			     . ') x ON x.id_feed=f.id '
+			     . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) '
+			     . 'WHERE f.category = ?';
+				$values = array ($id);
+				$stm = $this->bd->prepare ($sql);
+				if (!($stm && $stm->execute ($values))) {
+					$info = $stm->errorInfo();
+					Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+					$this->bd->rollBack ();
+					return false;
+				}
+			}
+
+			$this->bd->commit ();
+			return $affected;
+		}
+	}
+	public function markReadFeed ($id, $idMax = 0) {
+		if ($idMax === 0) {
+			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
+			     . 'SET e.is_read = 1, f.cache_nbUnreads=0 '
+			     . 'WHERE f.id=? AND e.is_read = 0';
+			$values = array ($id);
+			$stm = $this->bd->prepare ($sql);
+			if ($stm && $stm->execute ($values)) {
+				return $stm->rowCount();
+			} else {
+				$info = $stm->errorInfo();
+				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+				return false;
+			}
+		} else {
+			$this->bd->beginTransaction ();
+
+			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
+			     . 'SET e.is_read = 1 '
+			     . 'WHERE f.id=? AND e.is_read = 0 AND e.id <= ?';
+			$values = array ($id, $idMax);
+			$stm = $this->bd->prepare ($sql);
+			if (!($stm && $stm->execute ($values))) {
+				$info = $stm->errorInfo();
+				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+				$this->bd->rollBack ();
+				return false;
+			}
+			$affected = $stm->rowCount();
+
+			if ($affected > 0) {
+				$sql = 'UPDATE `' . $this->prefix . 'feed` f '
+				     . 'SET f.cache_nbUnreads=f.cache_nbUnreads-' . $affected
+				     . ' WHERE f.id=?';
+				$values = array ($id);
+				$stm = $this->bd->prepare ($sql);
+				if (!($stm && $stm->execute ($values))) {
+					$info = $stm->errorInfo();
+					Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+					$this->bd->rollBack ();
+					return false;
+				}
+			}
+
+			$this->bd->commit ();
+			return $affected;
+		}
+	}
+
+	public function searchByGuid ($feed_id, $id) {
+		// un guid est unique pour un flux donné
+		$sql = 'SELECT id, guid, title, author, UNCOMPRESS(content_bin) AS 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] : false;
+	}
+
+	public function searchById ($id) {
+		$sql = 'SELECT id, guid, title, author, UNCOMPRESS(content_bin) AS 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] : false;
+	}
+
+	public function listWhere($type = 'a', $id = '', $state = 'all', $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $keepHistoryDefault = 0) {
+		$where = '';
+		$joinFeed = false;
+		$values = array();
+		switch ($type) {
+			case 'a':
+				$where .= 'f.priority > 0 ';
+				$joinFeed = true;
+				break;
+			case 's':
+				$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;
+			default:
+				throw new FreshRSS_EntriesGetter_Exception ('Bad type in Entry->listByType: [' . $type . ']!');
+		}
+		switch ($state) {
+			case 'all':
+				break;
+			case 'not_read':
+				$where .= 'AND e1.is_read = 0 ';
+				break;
+			case 'read':
+				$where .= 'AND e1.is_read = 1 ';
+				break;
+			case 'favorite':
+				$where .= 'AND e1.is_favorite = 1 ';
+				break;
+			default:
+				throw new FreshRSS_EntriesGetter_Exception ('Bad state in Entry->listByType: [' . $state . ']!');
+		}
+		switch ($order) {
+			case 'DESC':
+			case 'ASC':
+				break;
+			default:
+				throw new FreshRSS_EntriesGetter_Exception ('Bad order in Entry->listByType: [' . $order . ']!');
+		}
+		if ($firstId !== '') {
+			$where .= 'AND e1.id ' . ($order === 'DESC' ? '<=' : '>=') . $firstId . ' ';
+		}
+		if (($date_min > 0) && ($type !== 's')) {
+			$where .= 'AND (e1.id >= ' . $date_min . '000000 OR e1.is_read = 0 OR e1.is_favorite = 1 OR (f.keep_history <> 0';
+			if (intval($keepHistoryDefault) === 0) {
+				$where .= ' AND f.keep_history <> -2';	//default
+			}
+			$where .= ')) ';
+			$joinFeed = true;
+		}
+		$search = '';
+		if ($filter !== '') {
+			$filter = trim($filter);
+			$filter = addcslashes($filter, '\\%_');
+			if (stripos($filter, 'intitle:') === 0) {
+				$filter = substr($filter, strlen('intitle:'));
+				$intitle = true;
+			} else {
+				$intitle = false;
+			}
+			if (stripos($filter, 'inurl:') === 0) {
+				$filter = substr($filter, strlen('inurl:'));
+				$inurl = true;
+			} else {
+				$inurl = false;
+			}
+			if (stripos($filter, 'author:') === 0) {
+				$filter = substr($filter, strlen('author:'));
+				$author = true;
+			} else {
+				$author = false;
+			}
+			$terms = array_unique(explode(' ', $filter));
+			sort($terms);	//Put #tags first
+			foreach ($terms as $word) {
+				$word = trim($word);
+				if (strlen($word) > 0) {
+					if ($intitle) {
+						$search .= 'AND e1.title LIKE ? ';
+						$values[] = '%' . $word .'%';
+					} elseif ($inurl) {
+						$search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? ';
+						$values[] = '%' . $word .'%';
+					} elseif ($author) {
+						$search .= 'AND e1.author LIKE ? ';
+						$values[] = '%' . $word .'%';
+					} else {
+						if ($word[0] === '#' && isset($word[1])) {
+							$search .= 'AND e1.tags LIKE ? ';
+							$values[] = '%' . $word .'%';
+						} else {
+							$search .= 'AND CONCAT(e1.title, UNCOMPRESS(e1.content_bin)) LIKE ? ';
+							$values[] = '%' . $word .'%';
+						}
+					}
+				}
+			}
+		}
+
+		$sql = 'SELECT e.id, e.guid, e.title, e.author, UNCOMPRESS(e.content_bin) AS content, e.link, e.date, e.is_read, e.is_favorite, e.id_feed, e.tags '
+		     . 'FROM `' . $this->prefix . 'entry` e '
+		     . 'INNER JOIN (SELECT e1.id FROM `' . $this->prefix . 'entry` e1 '
+			     . ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed = f.id ' : '')
+			     . 'WHERE ' . $where
+			     . $search
+			     . 'ORDER BY e1.id ' . $order
+			     . ($limit > 0 ? ' LIMIT ' . $limit : '')	//TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
+		     . ') 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 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 COUNT(id) FROM `' . $this->prefix . 'entry` WHERE is_favorite=1'
+		     . ' UNION SELECT COUNT(id) FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 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 optimizeTable() {
+		$sql = 'OPTIMIZE TABLE `' . $this->prefix . 'entry`';
+		$stm = $this->bd->prepare ($sql);
+		$stm->execute ();
+	}
+
+	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;
+	}
+}

+ 274 - 0
app/Models/Feed.php

@@ -0,0 +1,274 @@
+<?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 $hash = null;
+
+	public function __construct ($url, $validate=true) {
+		if ($validate) {
+			$this->_url ($url);
+		} else {
+			$this->url = $url;
+		}
+	}
+
+	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 nbEntries () {
+		if ($this->nbEntries < 0) {
+			$feedDAO = new FreshRSS_FeedDAO ();
+			$this->nbEntries = $feedDAO->countEntries ($this->id ());
+		}
+
+		return $this->nbEntries;
+	}
+	public function nbNotRead () {
+		if ($this->nbNotRead < 0) {
+			$feedDAO = new FreshRSS_FeedDAO ();
+			$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 (empty($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) {
+		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 _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);
+				$feed->init ();
+
+				if ($feed->error ()) {
+					throw new FreshRSS_Feed_Exception ($feed->error . ' [' . $url . ']');
+				}
+
+				// si on a utilisé l'auto-discover, notre url va avoir changé
+				$subscribe_url = $feed->subscribe_url ();
+				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 ($loadDetails) {
+					$title = htmlspecialchars(html_only_entity_decode($feed->get_title()), ENT_COMPAT, 'UTF-8');
+					$this->_name ($title === null ? $this->url : $title);
+
+					$this->_website(html_only_entity_decode($feed->get_link()));
+					$this->_description(html_only_entity_decode($feed->get_description()));
+				}
+
+				// et on charge les articles du flux
+				$this->loadEntries ($feed);
+			}
+		}
+	}
+	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 (array_key_exists($elink, $elinks)) continue;
+				$elinks[$elink] = '1';
+				$mime = strtolower($enclosure->get_type());
+				if (strpos($mime, 'image/') === 0) {
+					$content .= '<br /><img src="' . $elink . '" alt="" />';
+				}
+			}
+
+			$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;
+		}
+
+		$this->entries = $entries;
+	}
+}

+ 345 - 0
app/Models/FeedDAO.php

@@ -0,0 +1,345 @@
+<?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) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -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->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 $stm->rowCount();
+		} else {
+			$info = $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function updateLastUpdate ($id, $inError = 0) {
+		$sql = 'UPDATE `' . $this->prefix . 'feed` f '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
+		     . 'SET f.cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=f.id),'
+		     . 'f.cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=f.id AND e2.is_read=0),'
+		     . 'lastUpdate=?, error=? '
+		     . 'WHERE f.id=?';
+
+		$stm = $this->bd->prepare ($sql);
+
+		$values = array (
+			time (),
+			$inError,
+			$id,
+		);
+
+		if ($stm && $stm->execute ($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function 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->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function deleteFeed ($id) {
+		/*//For MYISAM (MySQL 5.5-) without FOREIGN KEY
+		$sql = 'DELETE FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
+		$stm = $this->bd->prepare ($sql);
+		$values = array ($id);
+		if (!($stm && $stm->execute ($values))) {
+			$info = $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}*/
+
+		$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->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+	public function deleteFeedByCategory ($id) {
+		/*//For MYISAM (MySQL 5.5-) without FOREIGN KEY
+		$sql = 'DELETE FROM `' . $this->prefix . 'entry` e '
+		     . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
+		     . 'WHERE f.category=?';
+		$stm = $this->bd->prepare ($sql);
+		$values = array ($id);
+		if (!($stm && $stm->execute ($values))) {
+			$info = $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}*/
+
+		$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->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 = self::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 (self::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 self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
+	}
+
+	public function listFeedsOrderUpdate () {
+		$sql = 'SELECT id, name, url, pathEntries, httpAuth, keep_history FROM `' . $this->prefix . 'feed` ORDER BY lastUpdate';
+		$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);
+
+		$values = array ($feed_id);
+
+		if ($stm && $stm->execute ($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+
+	public function truncate ($id) {
+		$sql = 'DELETE e.* FROM `' . $this->prefix . 'entry` e WHERE e.id_feed=?';
+		$stm = $this->bd->prepare($sql);
+		$values = array($id);
+		$this->bd->beginTransaction ();
+		if (!($stm && $stm->execute ($values))) {
+				$info = $stm->errorInfo();
+				Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+				$this->bd->rollBack ();
+				return false;
+			}
+		$affected = $stm->rowCount();
+
+		$sql = 'UPDATE `' . $this->prefix . 'feed` f '
+			 . 'SET f.cache_nbEntries=0, f.cache_nbUnreads=0 WHERE f.id=?';
+		$values = array ($id);
+		$stm = $this->bd->prepare ($sql);
+		if (!($stm && $stm->execute ($values))) {
+			$info = $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			$this->bd->rollBack ();
+			return false;
+		}
+
+		$this->bd->commit ();
+		return $affected;
+	}
+
+	public function cleanOldEntries ($id, $date_min, $keep = 15) {	//Remember to call updateLastUpdate($id) just after
+		$sql = 'DELETE e.* FROM `' . $this->prefix . 'entry` e '
+		     . 'WHERE e.id_feed = :id_feed AND e.id <= :id_max AND e.is_favorite = 0 AND e.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 because of: 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_INT);
+		$stm->bindParam(':keep', $keep, PDO::PARAM_INT);
+
+		if ($stm && $stm->execute ()) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $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->_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;
+	}
+}

+ 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', '');
+	}
+}

+ 205 - 0
app/Models/StatsDAO.php

@@ -0,0 +1,205 @@
+<?php
+
+class FreshRSS_StatsDAO extends Minz_ModelPdo {
+
+	/**
+	 * 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 = array();
+
+		// Generates a list of 30 last day to be sure we always have 30 days.
+		// If we do not do that kind of thing, we'll end up with holes in the
+		// days if the user do not have a lot of feeds.
+		$sql = <<<SQL
+SELECT - (tens.val + units.val + 1) AS day
+FROM (
+    SELECT 0 AS val
+    UNION ALL SELECT 1
+    UNION ALL SELECT 2
+    UNION ALL SELECT 3
+    UNION ALL SELECT 4
+    UNION ALL SELECT 5
+    UNION ALL SELECT 6
+    UNION ALL SELECT 7
+    UNION ALL SELECT 8
+    UNION ALL SELECT 9
+) AS units
+CROSS JOIN (
+    SELECT 0 AS val
+    UNION ALL SELECT 10
+    UNION ALL SELECT 20
+) AS tens
+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']] = 0;
+		}
+
+		// Get stats per day for the last 30 days and applies the result on 
+		// the array created with the last query.
+		$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 -30 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);
+	}
+
+	/**
+	 * 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 id
+ORDER BY count DESC
+LIMIT 10
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		return $stm->fetchAll(PDO::FETCH_ASSOC);
+	}
+
+	private function convertToSerie($data) {
+		$serie = array();
+
+		foreach ($data as $key => $value) {
+			$serie[] = array($key, $value);
+		}
+
+		return json_encode($serie);
+	}
+
+	private function convertToPieSerie($data) {
+		$serie = array();
+
+		foreach ($data as $value) {
+			$value['data'] = array(array(0, (int) $value['data']));
+			$serie[] = $value;
+		}
+
+		return json_encode($serie);
+	}
+
+}

+ 106 - 0
app/Models/Themes.php

@@ -0,0 +1,106 @@
+<?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 && 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' => '★',
+			'category' => '☷',
+			'category-white' => '☷',
+			'close' => '❌',
+			'configure' => '⚙',
+			'down' => '▽',
+			'favorite' => '★',
+			'help' => 'ⓘ',
+			'link' => '↗',
+			'login' => '🔒',
+			'logout' => '🔓',
+			'next' => '⏩',
+			'non-starred' => '☆',
+			'prev' => '⏪',
+			'read' => '☑',
+			'unread' => '☐',
+			'refresh' => '🔃',	//↻
+			'search' => '🔍',
+			'share' => '♺',
+			'starred' => '★',
+			'tag' => '⚐',
+			'up' => '△',
+		);
+		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] . '" />';
+	}
+}

+ 36 - 0
app/Models/UserDAO.php

@@ -0,0 +1,36 @@
+<?php
+
+class FreshRSS_UserDAO extends Minz_ModelPdo {
+	public function createUser($username) {
+		require_once(APP_PATH . '/sql.php');
+		$db = Minz_Configuration::dataBase();
+
+		$sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_');
+		$stm = $this->bd->prepare($sql, array(PDO::ATTR_EMULATE_PREPARES => true));
+		$values = array(
+			'catName' => Minz_Translate::t('default_category'),
+		);
+		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 deleteUser($username) {
+		require_once(APP_PATH . '/sql.php');
+		$db = Minz_Configuration::dataBase();
+
+		$sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_');
+		$stm = $this->bd->prepare($sql);
+		if ($stm && $stm->execute()) {
+			return true;
+		} else {
+			$info = $stm->errorInfo();
+			Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
+			return false;
+		}
+	}
+}

+ 59 - 0
app/actualize_script.php

@@ -0,0 +1,59 @@
+<?php
+require(dirname(__FILE__) . '/../constants.php');
+
+//<Mutex>
+$lock = DATA_PATH . '/actualize.lock.txt';
+if (file_exists($lock) && ((time() - @filemtime($lock)) > 3600)) {
+	@unlink($lock);
+}
+if (($handle = @fopen($lock, 'x')) === false) {
+	syslog(LOG_NOTICE, 'FreshRSS actualize already running?');
+	fwrite(STDERR, 'FreshRSS actualize already running?' . "\n");
+	return;
+}
+register_shutdown_function('unlink', $lock);
+//Could use http://php.net/function.pcntl-signal.php to catch interruptions
+@fclose($handle);
+//</Mutex>
+
+require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
+
+session_cache_limiter('');
+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);
+	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();
+	$freshRSS->_useOb(false);
+
+	Minz_Session::init('FreshRSS');
+	Minz_Session::_param('currentUser', $myUser);
+
+	$freshRSS->init();
+	$freshRSS->run();
+
+	invalidateHttpCache();
+	Minz_Session::unset_session(true);
+	Minz_ModelPdo::clean();
+}
+syslog(LOG_INFO, 'FreshRSS actualize done.');
+ob_end_flush();
+fwrite(STDOUT, 'Done.' . "\n");

+ 0 - 1
app/configuration/.gitignore

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

+ 0 - 375
app/controllers/configureController.php

@@ -1,375 +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 ();
-		$this->view->feeds = $feedDAO->listFeeds ();
-		$this->view->flux = false;
-
-		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', 'no');
-			$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');
-			$topline_read = Request::param ('topline_read', 'no');
-			$topline_favorite = Request::param ('topline_favorite', 'no');
-			$topline_date = Request::param ('topline_date', 'no');
-			$topline_link = Request::param ('topline_link', 'no');
-			$bottomline_read = Request::param ('bottomline_read', 'no');
-			$bottomline_favorite = Request::param ('bottomline_favorite', 'no');
-			$bottomline_sharing = Request::param ('bottomline_sharing', 'no');
-			$bottomline_tags = Request::param ('bottomline_tags', 'no');
-			$bottomline_date = Request::param ('bottomline_date', 'no');
-			$bottomline_link = Request::param ('bottomline_link', 'no');
-
-			$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);
-			$this->view->conf->_topline_read ($topline_read);
-			$this->view->conf->_topline_favorite ($topline_favorite);
-			$this->view->conf->_topline_date ($topline_date);
-			$this->view->conf->_topline_link ($topline_link);
-			$this->view->conf->_bottomline_read ($bottomline_read);
-			$this->view->conf->_bottomline_favorite ($bottomline_favorite);
-			$this->view->conf->_bottomline_sharing ($bottomline_sharing);
-			$this->view->conf->_bottomline_tags ($bottomline_tags);
-			$this->view->conf->_bottomline_date ($bottomline_date);
-			$this->view->conf->_bottomline_link ($bottomline_link);
-
-			$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 (),
-				'topline_read' => $this->view->conf->toplineRead () ? 'yes' : 'no',
-				'topline_favorite' => $this->view->conf->toplineFavorite () ? 'yes' : 'no',
-				'topline_date' => $this->view->conf->toplineDate () ? 'yes' : 'no',
-				'topline_link' => $this->view->conf->toplineLink () ? 'yes' : 'no',
-				'bottomline_read' => $this->view->conf->bottomlineRead () ? 'yes' : 'no',
-				'bottomline_favorite' => $this->view->conf->bottomlineFavorite () ? 'yes' : 'no',
-				'bottomline_sharing' => $this->view->conf->bottomlineSharing () ? 'yes' : 'no',
-				'bottomline_tags' => $this->view->conf->bottomlineTags () ? 'yes' : 'no',
-				'bottomline_date' => $this->view->conf->bottomlineDate () ? 'yes' : 'no',
-				'bottomline_link' => $this->view->conf->bottomlineLink () ? 'yes' : 'no',
-			);
-
-			$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: application/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 - 117
app/controllers/entryController.php

@@ -1,117 +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();
-
-		touch(PUBLIC_PATH . '/data/touch.txt');
-
-		$notif = array (
-			'type' => 'good',
-			'content' => Translate::t ('optimization_complete')
-		);
-		Session::_param ('notification', $notif);
-
-		Request::forward(array(
-			'c' => 'configure',
-			'a' => 'display'
-		), true);
-	}
-}

+ 0 - 363
app/controllers/feedController.php

@@ -1,363 +0,0 @@
-<?php
-
-class feedController extends ActionController {
-	public function firstAction () {
-		$token = $this->view->conf->token();
-		$token_param = Request::param ('token', '');
-		$token_is_ok = ($token != '' && $token == $token_param);
-		$action = Request::actionName ();
-
-		if (login_is_conf ($this->view->conf) &&
-				!is_logged () &&
-				!($token_is_ok && $action == 'actualize')) {
-			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 ();
-
-				//For this feed, check last n entry IDs already in database
-				$existingIds = array_fill_keys ($entryDAO->listLastIdsByFeed ($feed->id (), count($entries) + 2), 1);
-
-				// 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 ((!isset ($existingIds[$entry->id ()])) &&
-						($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);
-		Session::_param ('actualize_feeds', true);
-
-		// 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 - 257
app/controllers/indexController.php

@@ -1,257 +0,0 @@
-<?php
-
-class indexController extends ActionController {
-	private $get = false;
-	private $nb_not_read = 0;
-	private $nb_not_read_cat = 0;
-
-	public function indexAction () {
-		$output = Request::param ('output');
-
-		$token = $this->view->conf->token();
-		$token_param = Request::param ('token', '');
-		$token_is_ok = ($token != '' && $token == $token_param);
-
-		// check if user is log in
-		if(login_is_conf ($this->view->conf) &&
-				!is_logged() &&
-				$this->view->conf->anonAccess() == 'no' &&
-				!($output == 'rss' && $token_is_ok)) {
-			return;
-		}
-
-		// construction of RSS url of this feed
-		$params = Request::params ();
-		$params['output'] = 'rss';
-		if (isset ($params['search'])) {
-			$params['search'] = urlencode ($params['search']);
-		}
-		if (login_is_conf($this->view->conf) &&
-				$this->view->conf->anonAccess() == 'no' &&
-				$token != '') {
-			$params['token'] = $token;
-		}
-		$this->view->rss_url = array (
-			'c' => 'index',
-			'a' => 'index',
-			'params' => $params
-		);
-
-		$this->view->rss_title = View::title();
-
-		if ($output == 'rss') {
-			// no layout for RSS output
-			$this->view->_useLayout (false);
-			header('Content-Type: application/rss+xml; charset=utf-8');
-		} else {
-			View::appendScript (Url::display ('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
-
-			if ($output == 'global') {
-				View::appendScript (Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/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->countUnreadReadFavorites ();
-		$this->view->nb_total = $entryDAO->count ();
-		$this->view->currentName = '';
-
-		$this->view->get_c = '';
-		$this->view->get_f = '';
-
-		$type = $this->getType ();
-		$error = $this->checkAndProcessType ($type);
-
-		// mise à jour des titres
-		$this->view->rss_title = $this->view->currentName . ' - ' . $this->view->rss_title;
-		View::prependTitle (
-			$this->view->currentName .
-			($this->nb_not_read_cat > 0 ? ' (' . $this->nb_not_read_cat . ')' : '')
-		);
-
-		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
-	 * + Met à jour la variable $this->nb_not_read_cat
-	 */
-	private function checkAndProcessType ($type) {
-		if ($type['type'] == 'all') {
-			$this->view->currentName = Translate::t ('your_rss_feeds');
-			$this->view->get_c = $type['type'];
-			return false;
-		} elseif ($type['type'] == 'favoris') {
-			$this->view->currentName = Translate::t ('your_favorites');
-			$this->view->get_c = $type['type'];
-			return false;
-		} elseif ($type['type'] == 'public') {
-			$this->view->currentName = Translate::t ('public');
-			$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 ();
-				$this->nb_not_read_cat = $cat->nbNotRead ();
-				$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 ();
-				$this->nb_not_read_cat = $feed->nbNotRead ();
-				$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 (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);
-		if ($res['status'] == 'okay' && $res['email'] == $this->view->conf->mailLogin ()) {
-			Session::_param ('mail', $res['email']);
-			touch(PUBLIC_PATH . '/data/touch.txt');
-		} 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');
-		touch(PUBLIC_PATH . '/data/touch.txt');
-	}
-}

+ 0 - 13
app/controllers/javascriptController.php

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

+ 97 - 88
app/i18n/en.php

@@ -5,13 +5,17 @@ return array (
 	'login'				=> 'Login',
 	'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',
 	'about'				=> 'About',
+	'stats'				=> 'Statistics',
 
 	'your_rss_feeds'		=> 'Your RSS feeds',
 	'add_rss_feed'			=> 'Add a RSS feed',
@@ -19,7 +23,8 @@ return array (
 	'import_export_opml'		=> 'Import / export (OPML)',
 
 	'subscription_management'	=> 'Subscriptions management',
-	'all_feeds'			=> 'Main stream (%d)',
+	'main_stream'			=> 'Main stream',
+	'all_feeds'			=> 'All feeds',
 	'favorite_feeds'		=> 'Favourites (%d)',
 	'not_read'			=> '%d unread',
 	'not_reads'			=> '%d unread',
@@ -43,6 +48,8 @@ return array (
 	'rss_view'			=> 'RSS feed',
 	'show_all_articles'		=> 'Show all articles',
 	'show_not_reads'		=> 'Show only unread',
+	'show_read'			=> 'Show only read',
+	'show_favorite'			=> 'Show favorites',
 	'older_first'			=> 'Oldest first',
 	'newer_first'			=> 'Newer first',
 
@@ -59,7 +66,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',
@@ -67,7 +74,7 @@ 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',
@@ -83,10 +90,12 @@ return array (
 	'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',
 
@@ -119,6 +128,8 @@ return array (
 	'shift_for_first'		=> '+ <code>shift</code> to skip to the first article of page',
 	'next_page'			=> 'Skip to the next page',
 	'previous_page'			=> 'Skip to the previous page',
+	'collapse_article'		=> 'Collapse current article',
+	'auto_share'			=> 'Share current article',
 
 	'file_to_import'		=> 'File to import',
 	'import'			=> 'Import',
@@ -127,11 +138,16 @@ return array (
 
 	'informations'			=> 'Information',
 	'feed_in_error'			=> 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.',
+	'feed_description'		=> 'Description',
 	'website_url'			=> 'Website URL',
 	'feed_url'			=> 'Feed URL',
+	'articles'			=> 'articles',
 	'number_articles'		=> 'Number of articles',
-	'keep_history'			=> 'Keep history?',
+	'by_feed'			=> 'by feed',
+	'by_default'			=> 'By default',
+	'keep_history'			=> 'Minimum number of articles to keep',
 	'categorize'			=> 'Store in a category',
+	'truncate'			=> 'Delete all articles',
 	'advanced'			=> 'Advanced',
 	'show_in_all_flux'		=> 'Show in main stream',
 	'yes'				=> 'Yes',
@@ -145,40 +161,73 @@ return array (
 	'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'			=> '<a href="./?c=configure&amp;a=feed">You may add some feeds</a>.',
+
+	'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>',
+	'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)',
+	'auth_token'			=> 'Authentication token',
+	'explain_token'			=> 'Allows to access RSS output of the default user without authentication.<br /><kbd>%s?output=rss&token=%s</kbd>',
+	'login_configuration'		=> 'Login',
+	'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',
+	'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',
 	'articles_per_page'		=> 'Number of articles per page',
 	'default_view'			=> 'Default view',
 	'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',
-	'reading_icons'		=> 'Reading icons',
-	'top_line'		=> 'Top line',
-	'bottom_line'		=> 'Bottom line',
+	'after_onread'			=> 'After “mark all as read”,',
+	'jump_next'			=> 'jump to next unread sibling (feed or category)',
+	'reading_icons'			=> 'Reading icons',
+	'top_line'			=> 'Top line',
+	'bottom_line'			=> 'Bottom line',
 	'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',
+	'auto_read_when'		=> 'Mark article as read…',
+	'article_selected'		=> 'when article is selected',
+	'article_open_on_website'	=> 'when article is opened on its original website',
+	'scroll'			=> 'during page scrolls',
+	'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',
+	'more_information'		=> 'More information',
+	'activate_sharing'		=> 'Activate sharing',
+	'shaarli'			=> 'Shaarli',
+	'wallabag'			=> 'wallabag',
+	'diaspora'			=> 'Diaspora*',
+	'twitter'			=> 'Twitter',
+	'g+'				=> 'Google+',
+	'facebook'			=> 'Facebook',
+	'email'				=> 'Email',
+	'print'				=> 'Print',
 
 	'article'			=> 'Article',
 	'title'				=> 'Title',
@@ -187,7 +236,7 @@ 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',
 
@@ -196,9 +245,10 @@ return array (
 	'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',
@@ -211,12 +261,14 @@ return array (
 	'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.',
+	'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 forbidden! (%s)',
+	'login_required'		=> 'Login required:',
 
 	'confirm_action'		=> 'Are you sure you want to perform this action? It cannot be cancelled!',
 
@@ -247,61 +299,18 @@ 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',
 );

+ 94 - 85
app/i18n/fr.php

@@ -5,13 +5,17 @@ return array (
 	'login'				=> 'Connexion',
 	'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',
 	'about'				=> 'À propos',
+	'stats'				=> 'Statistiques',
 
 	'your_rss_feeds'		=> 'Vos flux RSS',
 	'add_rss_feed'			=> 'Ajouter un flux RSS',
@@ -19,7 +23,8 @@ return array (
 	'import_export_opml'		=> 'Importer / exporter (OPML)',
 
 	'subscription_management'	=> 'Gestion des abonnements',
-	'all_feeds'			=> 'Flux principal (%d)',
+	'main_stream'			=> 'Flux principal',
+	'all_feeds'			=> 'Tous les flux',
 	'favorite_feeds'		=> 'Favoris (%d)',
 	'not_read'			=> '%d non lu',
 	'not_reads'			=> '%d non lus',
@@ -43,6 +48,8 @@ return array (
 	'rss_view'			=> 'Flux RSS',
 	'show_all_articles'		=> 'Afficher tous les articles',
 	'show_not_reads'		=> 'Afficher les non lus',
+	'show_read'			=> 'Afficher les lus',
+	'show_favorite'			=> 'Afficher les favoris',
 	'older_first'			=> 'Plus anciens en premier',
 	'newer_first'			=> 'Plus récents en premier',
 
@@ -59,7 +66,7 @@ return array (
 	'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',
+	'error_occurred_update'	=> 'Rien n’a été modifié',
 
 	'default_category'		=> 'Sans catégorie',
 	'categories_updated'		=> 'Les catégories ont été mises à jour',
@@ -67,7 +74,7 @@ return array (
 	'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',
+	'sharing_management'		=> 'Gestion des options de partage',
 	'bad_opml_file'			=> 'Votre fichier OPML n’est pas valide',
 	'shortcuts_updated'		=> 'Les raccourcis ont été mis à jour',
 	'shortcuts_management'		=> 'Gestion des raccourcis',
@@ -83,10 +90,12 @@ return array (
 	'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',
+	'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',
 
@@ -119,6 +128,8 @@ return array (
 	'shift_for_first'		=> '+ <code>shift</code> pour passer au premier article de la page',
 	'next_page'			=> 'Passer à la page suivante',
 	'previous_page'			=> 'Passer à la page précédente',
+	'collapse_article'		=> 'Refermer l’article courant',
+	'auto_share'			=> 'Partager l’article courant',
 
 	'file_to_import'		=> 'Fichier à importer',
 	'import'			=> 'Importer',
@@ -127,11 +138,16 @@ return array (
 
 	'informations'			=> 'Informations',
 	'feed_in_error'			=> 'Ce flux a rencontré un problème. Veuillez vérifier qu’il est toujours accessible puis actualisez-le.',
+	'feed_description'		=> 'Description',
 	'website_url'			=> 'URL du site',
 	'feed_url'			=> 'URL du flux',
+	'articles'			=> 'articles',
 	'number_articles'		=> 'Nombre d’articles',
-	'keep_history'			=> 'Garder l’historique ?',
+	'by_feed'			=> 'par flux',
+	'by_default'			=> 'Par défaut',
+	'keep_history'			=> 'Nombre minimum d’articles à conserver',
 	'categorize'			=> 'Ranger dans une catégorie',
+	'truncate'			=> 'Supprimer tous les articles',
 	'advanced'			=> 'Avancé',
 	'show_in_all_flux'		=> 'Afficher dans le flux principal',
 	'yes'				=> 'Oui',
@@ -145,40 +161,73 @@ return array (
 	'not_yet_implemented'		=> 'Pas encore implémenté',
 	'access_protected_feeds'	=> 'La connexion permet d’accéder aux flux protégés par une authentification HTTP',
 	'no_selected_feed'		=> 'Aucun flux sélectionné.',
-	'think_to_add'			=> 'Pensez à en ajouter !',
+	'think_to_add'			=> '<a href="./?c=configure&amp;a=feed">Vous pouvez ajouter des flux</a>.',
+
+	'current_user'			=> 'Utilisateur actuel',
+	'password_form'			=> 'Mot de passe<br /><small>(pour connexion par formulaire)</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)',
+	'auth_token'			=> 'Jeton d’identification',
+	'explain_token'			=> 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.<br /><kbd>%s?output=rss&token=%s</kbd>',
+	'login_configuration'		=> 'Identification',
+	'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',
+	'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 courriel 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',
 	'articles_per_page'		=> 'Nombre d’articles par page',
 	'default_view'			=> 'Vue par défaut',
 	'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',
-	'reading_icons'		=> 'Icônes de lecture',
-	'top_line'		=> 'Ligne du haut',
-	'bottom_line'		=> 'Ligne du bas',
+	'after_onread'			=> 'Après “marquer tout comme lu”,',
+	'jump_next'			=> 'sauter au prochain voisin non lu (flux ou catégorie)',
+	'reading_icons'			=> 'Icônes de lecture',
+	'top_line'			=> 'Ligne du haut',
+	'bottom_line'			=> 'Ligne du bas',
 	'img_with_lazyload'		=> 'Utiliser le mode “chargement différé” pour 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',
+	'auto_read_when'		=> 'Marquer un article comme lu…',
+	'article_selected'		=> 'lorsque l’article est sélectionné',
+	'article_open_on_website'	=> 'lorsque l’article est ouvert sur le site d’origine',
 	'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',
+	'more_information'		=> 'Plus d’informations',
+	'activate_sharing'		=> 'Activer le partage',
+	'shaarli'			=> 'Shaarli',
+	'wallabag'			=> 'wallabag',
+	'diaspora'			=> 'Diaspora*',
+	'twitter'			=> 'Twitter',
+	'g+'				=> 'Google+',
+	'facebook'			=> 'Facebook',
+	'email'				=> 'Courriel',
+	'print'				=> 'Imprimer',
 
 	'article'			=> 'Article',
 	'title'				=> 'Titre',
@@ -196,9 +245,10 @@ return array (
 	'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',
@@ -211,12 +261,14 @@ return array (
 	'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',
+	'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'		=> 'Accès interdit ! (%s)',
+	'login_required'		=> 'Accès protégé par mot de passe :',
 
 	'confirm_action'		=> 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !',
 
@@ -247,61 +299,18 @@ 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 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',
-	'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',
 );

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

@@ -0,0 +1,67 @@
+<?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)',
+	'pdomysql_is_ok'		=> 'You have PDO and its driver for MySQL',
+	'pdomysql_is_nok'		=> 'You lack PDO or its driver for MySQL (php5-mysql package)',
+	'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',
+	'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 the <kbd>./p/i/install.php</kbd> file and any database backup created during the update process.<br />You may choose to skip this step and delete <kbd>./p/i/install.php</kbd> manually.',
+	'finish_installation'		=> 'Complete installation',
+	'install_not_deleted'		=> 'Something went wrong; you must delete the file <em>%s</em> manually.',
+);

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

@@ -0,0 +1,66 @@
+<?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)',
+	'pdomysql_is_ok'		=> 'Vous disposez de PDO et de son driver pour MySQL (paquet php5-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'			=> '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',
+	'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 le fichier <kbd>./p/i/install.php</kbd>, ainsi que d’éventuelles copies de base de données créées durant le processus de mise à jour.<br />Vous pouvez choisir de sauter cette étape et de supprimer <kbd>./p/i/install.php</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>

+ 17 - 8
app/layout/aside_configure.phtml

@@ -1,10 +1,19 @@
-<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 Minz_Translate::t ('configuration'); ?></li>
+	<li class="item<?php echo Minz_Request::actionName () == 'display' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a>
 	</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 Minz_Translate::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 Minz_Translate::t ('sharing'); ?></a>
+	</li>
+	<li class="item<?php echo Minz_Request::actionName () == 'shortcut' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('configure', 'shortcut'); ?>"><?php echo Minz_Translate::t ('shortcuts'); ?></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 Minz_Translate::t ('users'); ?></a>
+	</li>
+</ul>

+ 17 - 17
app/layout/aside_feed.phtml

@@ -1,22 +1,22 @@
 <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 } ?>
@@ -25,25 +25,25 @@
 
 					<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 class="item<?php echo Minz_Request::actionName () == 'importExport' ? ' active' : ''; ?>">
+		<a href="<?php echo _url ('configure', 'importExport'); ?>"><?php echo Minz_Translate::t ('import_export_opml'); ?></a>
 	</li>
 
-	<li class="item<?php echo Request::actionName () == 'categorize' ? ' active' : ''; ?>">
-		<a href="<?php echo _url ('configure', 'categorize'); ?>"><?php echo Translate::t ('categories_management'); ?></a>
+	<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>
@@ -54,11 +54,11 @@
 	<li class="item<?php echo ($this->flux && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>">
 		<a href="<?php echo _url ('configure', 'feed', 'id', $feed->id ()); ?>">
 			<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" />
-			<?php echo htmlspecialchars($feed->name (), ENT_NOQUOTES, 'UTF-8'); ?>
+			<?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>

+ 57 - 50
app/layout/aside_flux.phtml

@@ -1,81 +1,88 @@
 <div class="aside aside_flux" id="aside_flux">
-	<a class="toggle_aside" href="#close"><i class="icon i_close"></i></a>
+	<a class="toggle_aside" href="#close"><?php echo FreshRSS_Themes::icon('close'); ?></a>
 
 	<ul class="categories">
-		<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
+		<?php if ($this->loginOk) { ?>
 		<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 ('configure', 'categorize'); ?>"><i class="icon i_category"></i></a>
+				<a class="btn btn-important" href="<?php echo _url ('configure', 'feed'); ?>"><?php echo Minz_Translate::t ('subscription_management'); ?></a>
+				<a class="btn btn-important" href="<?php echo _url ('configure', 'categorize'); ?>" title="<?php echo Minz_Translate::t ('categories_management'); ?>"><?php echo FreshRSS_Themes::icon('category-white'); ?></a>
 			</div>
 		</li>
-		<?php } elseif (login_is_conf ($this->conf)) { ?>
-		<li><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Translate::t ('about_freshrss'); ?></a></li>
+		<?php } elseif (Minz_Configuration::needsLogin()) { ?>
+		<li><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::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="category all">
-				<a data-unread="<?php echo $this->nb_not_read; ?>" 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); ?>
+				<a data-unread="<?php echo formatNumber($this->nb_not_read); ?>" class="btn<?php echo $this->get_c == 'a' ? ' active' : ''; ?>" href="<?php echo Minz_Url::display($arUrl); ?>">
+					<?php echo FreshRSS_Themes::icon('all'); ?>
+					<?php echo Minz_Translate::t ('main_stream'); ?>
 				</a>
 			</div>
 		</li>
 
 		<li>
 			<div class="category favorites">
-				<a data-unread="<?php echo $this->nb_favorites['unread']; ?>" 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['read'] + $this->nb_favorites['unread']); ?>
+				<a data-unread="<?php echo formatNumber($this->nb_favorites['unread']); ?>" class="btn<?php echo $this->get_c == 's' ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 's'; echo Minz_Url::display($arUrl); ?>">
+					<?php echo FreshRSS_Themes::icon('bookmark'); ?>
+					<?php echo Minz_Translate::t('favorite_feeds', formatNumber($this->nb_favorites['all'])); ?>
 				</a>
 			</div>
 		</li>
 
-		<?php foreach ($this->cat_aside as $cat) { ?>
-		<?php $feeds = $cat->feeds (); ?>
-		<?php if (!empty ($feeds)) { ?>
-		<li>
-			<?php $c_active = false; if ($this->get_c == $cat->id ()) { $c_active = true; } ?>
-			<div class="category stick<?php echo $c_active ? ' active' : ''; ?>">
-				<a data-unread="<?php echo $cat->nbNotRead (); ?>" class="btn<?php echo $c_active ? ' active' : ''; ?>" href="<?php echo _url ('index', 'index', 'get', 'c_' . $cat->id ()); ?>">
-					<?php echo htmlspecialchars($cat->name (), ENT_NOQUOTES, 'UTF-8'); ?>
-				</a>
-				<a class="btn dropdown-toggle" href="#"><i class="icon <?php echo $c_active ? 'i_up' : 'i_down'; ?>"></i></a>
-			</div>
-
-			<ul class="feeds<?php echo $c_active ? ' 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' : ''; ?>">
-					<div class="dropdown">
-						<div class="dropdown-target"></div>
-						<a class="dropdown-toggle" data-fweb="<?php echo $feed->website (); ?>"><i class="icon i_configure"></i></a>
-<?php /* feed_config_template */ ?>
-					</div>
-					<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" />
-					<a class="feed" data-unread="<?php echo $feed->nbNotRead (); ?>" data-priority="<?php echo $feed->priority (); ?>" href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed_id); ?>"><?php echo htmlspecialchars($feed->name(), ENT_NOQUOTES, 'UTF-8'); ?></a>
-				</li>
-				<?php } ?>
-			</ul>
-		</li>
-		<?php } } ?>
+		<?php
+		foreach ($this->cat_aside as $cat) {
+			$feeds = $cat->feeds ();
+			if (!empty ($feeds)) {
+				?><li><?php
+				$c_active = false;
+				if ($this->get_c == $cat->id ()) {
+					$c_active = true;
+				}
+				?><div class="category stick<?php echo $c_active ? ' active' : ''; ?>"><?php
+					?><a data-unread="<?php echo formatNumber($cat->nbNotRead()); ?>" class="btn<?php echo $c_active ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 'c_' . $cat->id(); echo Minz_Url::display($arUrl); ?>"><?php echo $cat->name (); ?></a><?php
+					?><a class="btn dropdown-toggle" href="#"><?php echo FreshRSS_Themes::icon($c_active ? 'up' : 'down'); ?></a><?php
+				?></div><?php
+				?><ul class="feeds<?php echo $c_active ? ' 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' : ''; ?>"><?php
+						?><div class="dropdown"><?php
+							?><div class="dropdown-target"></div><?php
+							?><a class="dropdown-toggle" data-fweb="<?php echo $feed->website (); ?>"><?php echo FreshRSS_Themes::icon('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>
 
 <script id="feed_config_template" type="text/html">
 	<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_!!!!!!'); ?>"><?php echo Translate::t ('filter'); ?></a></li>
-		<li class="item"><a target="_blank" href="http://example.net/"><?php echo Translate::t ('see_website'); ?></a></li>
-		<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
+		<li class="dropdown-close"><a href="#close"></a></li>
+		<li class="item"><a href="<?php echo _url ('index', 'index', 'get', 'f_!!!!!!'); ?>"><?php echo Minz_Translate::t ('filter'); ?></a></li>
+		<li class="item"><a target="_blank" href="http://example.net/"><?php echo Minz_Translate::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 Translate::t ('administration'); ?></a></li>
-		<li class="item"><a href="<?php echo _url ('feed', 'actualize', 'id', '!!!!!!'); ?>"><?php echo Translate::t ('actualize'); ?></a></li>
-		<li class="item"><a href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', 'f_!!!!!!'); ?>"><?php echo Translate::t ('mark_read'); ?></a></li>
+		<li class="item"><a href="<?php echo _url ('configure', 'feed', 'id', '!!!!!!'); ?>"><?php echo Minz_Translate::t ('administration'); ?></a></li>
+		<li class="item"><a href="<?php echo _url ('feed', 'actualize', 'id', '!!!!!!'); ?>"><?php echo Minz_Translate::t ('actualize'); ?></a></li>
+		<li class="item"><a href="<?php echo _url ('entry', 'read', 'get', 'f_!!!!!!'); ?>"><?php echo Minz_Translate::t ('mark_read'); ?></a></li>
 		<?php } ?>
 	</ul>
 </script>

+ 67 - 39
app/layout/header.phtml

@@ -1,79 +1,107 @@
-<?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 FreshRSS_Themes::icon('logout'); ?> <a class="signout" href="<?php echo _url ('index', 'formLogout'); ?>"><?php echo Minz_Translate::t ('logout'); ?></a></li><?php
+			} else {
+				?><li class="item"><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="<?php echo _url ('index', 'formLogin'); ?>"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
+			}
+			break;
+		case 'persona':
+			if ($this->loginOk) {
+				?><li class="item"><?php echo FreshRSS_Themes::icon('logout'); ?> <a class="signout" href="#"><?php echo Minz_Translate::t ('logout'); ?></a></li><?php
+			} else {
+				?><li class="item"><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
+			}
+			break;
+	}
+	?></ul><?php
+}
+?>
 
 <div class="header">
 	<div class="item title">
 		<h1>
 			<a href="<?php echo _url ('index', 'index'); ?>">
-				<img class="logo" width="32" height="32" src="<?php echo Url::display ('/themes/icons/icon-32.png'); ?>" alt="[logo]" />
-				<?php echo Configuration::title (); ?>
+				<img class="logo" src="<?php echo Minz_Url::display ('/themes/icons/icon.svg'); ?>" 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') { ?>
+		<?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 Minz_Translate::t ('search'); ?>" />
 
-				<?php $get = Request::param ('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 $order = Minz_Request::param ('order', ''); ?>
 				<?php if($order != '') { ?>
 				<input type="hidden" name="order" value="<?php echo $order; ?>" />
 				<?php } ?>
 
-				<?php $state = Request::param ('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 FreshRSS_Themes::icon('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 FreshRSS_Themes::icon('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', 'shortcut'); ?>"><?php echo Translate::t ('shortcuts'); ?></a></li>
+				<li class="dropdown-close"><a href="#close">❌</a></li>
+				<li class="dropdown-header"><?php echo Minz_Translate::t ('configuration'); ?></li>
+				<li class="item"><a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a></li>
+				<li class="item"><a href="<?php echo _url ('configure', 'archiving'); ?>"><?php echo Minz_Translate::t ('archiving_configuration'); ?></a></li>
+				<li class="item"><a href="<?php echo _url ('configure', 'sharing'); ?>"><?php echo Minz_Translate::t ('sharing'); ?></a></li>
+				<li class="item"><a href="<?php echo _url ('configure', 'shortcut'); ?>"><?php echo Minz_Translate::t ('shortcuts'); ?></a></li>
 				<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="item"><a href="<?php echo _url ('configure', 'users'); ?>"><?php echo Minz_Translate::t ('users'); ?></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>
-				<?php } ?>
+				<li class="item"><a href="<?php echo _url ('index', 'stats'); ?>"><?php echo Minz_Translate::t ('stats'); ?></a></li>
+				<li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about'); ?></a></li>
+				<li class="item"><a href="<?php echo _url ('index', 'logs'); ?>"><?php echo Minz_Translate::t ('logs'); ?></a></li>
+				<?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 FreshRSS_Themes::icon('logout'), ' ', Minz_Translate::t ('logout'); ?></a></li><?php
+							break;
+						case 'persona':
+							?><li class="item"><a class="signout" href="#"><?php echo FreshRSS_Themes::icon('logout'), ' ', Minz_Translate::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 FreshRSS_Themes::icon('login'); ?><a class="signin" href="<?php echo _url ('index', 'formLogin'); ?>"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
+				break;
+			case 'persona':
+				echo FreshRSS_Themes::icon('login'); ?><a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
+				break;
+		}
+		?></div><?php
+	} ?>
 </div>

+ 16 - 11
app/layout/layout.phtml

@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="<?php echo $this->conf->language (); ?>" xml:lang="<?php echo $this->conf->language (); ?>">
+<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" />
@@ -10,20 +10,25 @@
 <?php $this->renderHelper ('javascript_vars'); ?>
 		//]]></script>
 <?php
-	$next = isset($this->entryPaginator) ? $this->entryPaginator->next() : '';
-	if (!empty($next)) {
-		$params = Request::params ();
-		$params['next'] = $next;
+	if (!empty($this->nextId)) {
+		$params = Minz_Request::params ();
+		$params['next'] = $this->nextId;
 ?>
-		<link id="prefetch" rel="next prefetch" href="<?php echo Url::display (array ('c' => Request::controllerName (), 'a' => Request::actionName (), 'params' => $params)); ?>" />
+		<link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display (array ('c' => Minz_Request::controllerName (), 'a' => Minz_Request::actionName (), 'params' => $params)); ?>" />
 <?php } ?>
-		<link rel="icon" href="<?php echo Url::display ('/favicon.ico'); ?>" />
+		<link rel="shortcut icon" type="image/x-icon" sizes="16x16 64x64" href="<?php echo Minz_Url::display('/favicon.ico'); ?>" />
+		<link rel="icon msapplication-TileImage apple-touch-icon" type="image/png" sizes="256x256" href="<?php echo Minz_Url::display('/themes/icons/favicon-256.png'); ?>" />
 <?php if (isset ($this->rss_url)) { ?>
-		<link rel="alternate" type="application/rss+xml" title="<?php echo htmlspecialchars($this->rss_title, ENT_COMPAT, 'UTF-8'); ?>" href="<?php echo Url::display ($this->rss_url); ?>" />
+		<link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display ($this->rss_url); ?>" />
 <?php } ?>
+		<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); ?>">
+		<meta name="msapplication-TileColor" content="#FFF" />
 		<meta name="robots" content="noindex,nofollow" />
 	</head>
-	<body>
+	<body class="<?php echo Minz_Request::param('output', 'normal'); ?>">
 <?php $this->partial ('header'); ?>
 
 <div id="global">
@@ -32,11 +37,11 @@
 
 <?php
 	if (isset ($this->notification)) {
-		touch(PUBLIC_PATH . '/data/touch.txt', time() + 1);
+		invalidateHttpCache();
 ?>
 <div class="notification <?php echo $this->notification['type']; ?>">
 	<?php echo $this->notification['content']; ?>
-	<a class="close" href=""><i class="icon i_close"></i></a>
+	<a class="close" href=""><?php echo FreshRSS_Themes::icon('close'); ?></a>
 </div>
 <?php } ?>
 	</body>

+ 3 - 3
app/layout/nav_entries.phtml

@@ -1,5 +1,5 @@
 <ul id="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>
+	<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>

+ 115 - 63
app/layout/nav_menu.phtml

@@ -1,30 +1,36 @@
+<?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 FreshRSS_Themes::icon('category'); ?></a>
+	<?php } ?>
 
-	<?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>
+	<?php if ($this->loginOk) { ?>
+	<a id="actualize" class="btn" href="<?php echo _url ('feed', 'actualize'); ?>"><?php echo FreshRSS_Themes::icon('refresh'); ?></a>
 
 	<?php
 		$get = false;
-		$string_mark = Translate::t ('mark_all_read');
+		$string_mark = Minz_Translate::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 = Minz_Translate::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 = Minz_Translate::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) {
+						if ($cat->id () == $this->get_c) {
 							$foundCurrent = true;
 							continue;
 						}
@@ -32,13 +38,13 @@
 						$anotherUnreadId = $cat->id ();
 						if ($foundCurrent) break;
 					}
-					$nextGet = strlen ($anotherUnreadId) > 1 ? 'c_' . $anotherUnreadId : 'all';
+					$nextGet = empty ($anotherUnreadId) ? 'a' : 'c_' . $anotherUnreadId;
 					break;
 				case 'f':
 					foreach ($this->cat_aside as $cat) {
-						if ($cat->id () === $this->get_c) {
+						if ($cat->id () == $this->get_c) {
 							foreach ($cat->feeds () as $feed) {
-								if ($feed->id () === $this->get_f) {
+								if ($feed->id () == $this->get_f) {
 									$foundCurrent = true;
 									continue;
 								}
@@ -49,37 +55,46 @@
 							break;
 						}
 					}
-					$nextGet = strlen ($anotherUnreadId) > 1 ? 'f_' . $anotherUnreadId : 'c_' . $this->get_c;
+					$nextGet = empty ($anotherUnreadId) ? 'c_' . $this->get_c : 'f_' . $anotherUnreadId;
 					break;
 			}
 		}
+		$p = isset($this->entries[0]) ? $this->entries[0] : null;
+		$idMax = $p === null ? '0' : $p->id();
+
+		$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('get' => $get, 'nextGet' => $nextGet, 'idMax' => $idMax));
+		$output = Minz_Request::param('output', '');
+		if (($output != '') && ($this->conf->view_mode !== $output)) {
+			$arUrl['params']['output'] = $output;
+		}
+		$markReadUrl = Minz_Url::display($arUrl);
+		Minz_Session::_param ('markReadUrl', $markReadUrl);
 	?>
 
 	<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>
+		<a class="read_all btn" href="<?php echo $markReadUrl; ?>"><?php echo Minz_Translate::t ('mark_read'); ?></a>
 		<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 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="item"><a href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', $get, 'nextGet', $nextGet); ?>"><?php echo $string_mark; ?></a></li> 
+				<li class="item"><a href="<?php echo $markReadUrl; ?>"><?php echo $string_mark; ?></a></li> 
 				<li class="separator"></li>
 <?php
-	$date = getdate ();
-	$today = mktime (0, 0, 0, $date['mon'], $date['mday'], $date['year']);
+	$today = $this->today;
 	$one_week = $today - 604800;
 ?>
-				<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"><a href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', $get, 'idMax', $today . '000000'); ?>"><?php echo Minz_Translate::t ('before_one_day'); ?></a></li>
+				<li class="item"><a href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', $get, 'idMax', $one_week . '000000'); ?>"><?php echo Minz_Translate::t ('before_one_week'); ?></a></li>
 			</ul>
 		</div>
 	</div>
 	<?php } ?>
 
 	<?php
-		$params = Request::params ();
+		$params = Minz_Request::params ();
 		if (isset ($params['search'])) {
 			$params['search'] = urlencode ($params['search']);
 		}
@@ -91,73 +106,88 @@
 	?>
 	<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>
+		<a class="dropdown-toggle btn" href="#dropdown-views"><?php echo Minz_Translate::t ('display'); ?> <?php echo FreshRSS_Themes::icon('down'); ?></a>
 		<ul class="dropdown-menu">
-			<li class="dropdown-close"><a href="#close">&nbsp;</a></li>
+			<li class="dropdown-close"><a href="#close"></a></li>
 
 			<?php
 				$url_output = $url;
-				$actual_view = Request::param('output', 'normal');
-			?>
-			<?php if($actual_view != 'normal') { ?>
+				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 class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
+					<?php echo Minz_Translate::t ('normal_view'); ?>
 				</a>
 			</li>
-			<?php } if($actual_view != 'reader') { ?>
+			<?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 class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
+					<?php echo Minz_Translate::t ('reader_view'); ?>
 				</a>
 			</li>
-			<?php } if($actual_view != 'global') { ?>
+			<?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 class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
+					<?php echo Minz_Translate::t ('global_view'); ?>
 				</a>
 			</li>
 			<?php } ?>
 
 			<li class="separator"></li>
 
-			<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'); ?>
+			<?php
+				$url_state = $url;
+				$url_state['params']['state'] = 'all';
+			?>
+			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'all') ? 'true' :'false'; ?>">
+				<a class="print_all" href="<?php echo Minz_Url::display ($url_state); ?>">
+					<?php echo Minz_Translate::t ('show_all_articles'); ?>
 				</a>
-				<?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'); ?>
+			</li>
+			<?php
+				$url_state['params']['state'] = 'not_read';
+			?>
+			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'not_read') ? 'true' :'false'; ?>">
+				<a class="print_non_read" href="<?php echo Minz_Url::display ($url_state); ?>">
+					<?php echo Minz_Translate::t ('show_not_reads'); ?>
 				</a>
-				<?php } ?>
 			</li>
+			<?php
+				$url_state['params']['state'] = 'read';
+			?>
+			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'read') ? 'true' :'false'; ?>">
+				<a class="print_read" href="<?php echo Minz_Url::display ($url_state); ?>">
+					<?php echo Minz_Translate::t ('show_read'); ?>
+				</a>
+			</li>
+			<?php
+				$url_state['params']['state'] = 'favorite';
+			?>
+			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'favorite') ? 'true' :'false'; ?>">
+				<a class="print_favorite" href="<?php echo Minz_Url::display ($url_state); ?>">
+					<?php echo Minz_Translate::t ('show_favorite'); ?>
+				</a>
+			</li>
+
+			<li class="separator"></li>
 
 			<li class="item">
 				<?php
 					$url_order = $url;
-					if ($this->order == 'low_to_high') {
-						$url_order['params']['order'] = 'high_to_low';
+					if ($this->order === 'DESC') {
+						$url_order['params']['order'] = 'ASC';
 				?>
-				<a href="<?php echo Url::display ($url_order); ?>">
-					<?php echo Translate::t ('older_first'); ?>
+				<a href="<?php echo Minz_Url::display ($url_order); ?>">
+					<?php echo Minz_Translate::t ('older_first'); ?>
 				</a>
 				<?php
 					} else {
-						$url_order['params']['order'] = 'low_to_high';
+						$url_order['params']['order'] = 'DESC';
 				?>
-				<a href="<?php echo Url::display ($url_order); ?>">
-					<?php echo Translate::t ('newer_first'); ?>
+				<a href="<?php echo Minz_Url::display ($url_order); ?>">
+					<?php echo Minz_Translate::t ('newer_first'); ?>
 				</a>
 				<?php } ?>
 			</li>
@@ -165,10 +195,32 @@
 			<li class="separator"></li>
 
 			<li class="item">
-				<a class="view_rss" target="_blank" href="<?php echo Url::display ($this->rss_url); ?>">
-					<?php echo Translate::t ('rss_view'); ?>
+				<a class="view_rss" target="_blank" href="<?php echo Minz_Url::display ($this->rss_url); ?>">
+					<?php echo Minz_Translate::t ('rss_view'); ?>
 				</a>
 			</li>
 		</ul>
 	</div>
+
+	<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 Minz_Translate::t ('search_short'); ?>" />
+
+			<?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>
 </div>

+ 0 - 332
app/models/Category.php

@@ -1,332 +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 (isset ($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) {
-		if ($prePopulateFeeds) {
-			$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.color AS c_color, '
-			     . 'COUNT(CASE WHEN e.is_read = 0 THEN 1 END) AS nbNotRead, '
-			     . 'COUNT(e.id) AS nbEntries, '
-			     . '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 '
-			     . '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();
-		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 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;
-		}
-
-		// add the last category
-		if ($previousLine != null) {
-			$cat = new Category (
-				$previousLine['c_name'],
-				$previousLine['c_color'],
-				HelperFeed::daoToFeed ($feedsDao)
-			);
-			$cat->_id ($previousLine['c_id']);
-			$list[] = $cat;
-		}
-
-		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 - 590
app/models/Entry.php

@@ -1,590 +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)) {
-			$limitCount = 20000;	//TODO: FIXME: Hack temporaire en attendant la recherche côté base-de-données
-		}
-		//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 listLastIdsByFeed($id, $n) {
-		$sql = 'SELECT id FROM ' . $this->prefix . 'entry WHERE id_feed=? ORDER BY date 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 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 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 - 564
app/models/Feed.php

@@ -1,564 +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, $validate=true) {
-		if ($validate) {
-			$this->_url ($url);
-		} else {
-			$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 (empty ($value)) {
-			throw new BadUrlException ($value);
-		}
-		if (!preg_match ('#^https?://#i', $value)) {
-			$value = 'http://' . $value;
-		}
-
-		if (filter_var ($value, FILTER_VALIDATE_URL)) {
-			$this->url = $value;
-		} elseif (version_compare(PHP_VERSION, '5.3.3', '<') && (strpos($value, '-') > 0) && ($value === filter_var($value, FILTER_SANITIZE_URL))) {	//PHP bug #51192
-			$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) {
-		$this->priority = is_numeric ($value) ? intval ($value) : 10;
-	}
-	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) {
-		$this->nbNotRead = is_numeric ($value) ? intval ($value) : -1;
-	}
-	public function _nbEntries ($value) {
-		$this->nbEntries = is_numeric ($value) ? intval ($value) : -1;
-	}
-
-	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->set_cache_duration(1500);
-				$feed->strip_htmltags (array (
-					'base', 'blink', 'body', 'doctype',
-					'font', 'form', 'frame', 'frameset', 'html',
-					'input', 'marquee', 'meta', 'noscript',
-					'param', 'script', 'style'
-				));
-				$feed->strip_attributes(array_merge($feed->strip_attributes, array(
-					'onload', 'onunload', 'onclick', 'ondblclick', 'onmousedown', 'onmouseup',
-					'onmouseover', 'onmousemove', 'onmouseout', 'onfocus', 'onblur',
-					'onkeypress', 'onkeydown', 'onkeyup', 'onselect', 'onchange')));
-				$feed->init ();
-
-				if ($feed->error ()) {
-					throw new FeedException ($feed->error . ' [' . $url . ']');
-				}
-
-				// 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 ();
-			$elinks = array();
-			foreach ($item->get_enclosures() as $enclosure) {
-				$elink = $enclosure->get_link();
-				if (array_key_exists($elink, $elinks)) continue;
-				$elinks[$elink] = '1';
-				$mime = strtolower($enclosure->get_type());
-				if (strpos($mime, 'image/') === 0) {
-					$content .= '<br /><img src="' . $elink . '" alt="" />';
-				}
-			}
-
-			$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 () {	//Is this used?
-		$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) {	//Is this used?
-		$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'], false);
-			$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['nbEntries'])) {
-				$list[$key]->_nbEntries ($dao['nbEntries']);
-			}
-			if (isset ($dao['id'])) {
-				$list[$key]->_id ($dao['id']);
-			}
-		}
-
-		return $list;
-	}
-}

+ 0 - 47
app/models/Log_Model.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 - 445
app/models/RSSConfiguration.php

@@ -1,445 +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;
-	private $topline_read;
-	private $topline_favorite;
-	private $topline_date;
-	private $topline_link;
-	private $bottomline_read;
-	private $bottomline_favorite;
-	private $bottomline_sharing;
-	private $bottomline_tags;
-	private $bottomline_date;
-	private $bottomline_link;
-
-	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);
-		$this->_topline_read ($confDAO->topline_read);
-		$this->_topline_favorite ($confDAO->topline_favorite);
-		$this->_topline_date ($confDAO->topline_date);
-		$this->_topline_link ($confDAO->topline_link);
-		$this->_bottomline_read ($confDAO->bottomline_read);
-		$this->_bottomline_favorite ($confDAO->bottomline_favorite);
-		$this->_bottomline_sharing ($confDAO->bottomline_sharing);
-		$this->_bottomline_tags ($confDAO->bottomline_tags);
-		$this->_bottomline_date ($confDAO->bottomline_date);
-		$this->_bottomline_link ($confDAO->bottomline_link);
-	}
-
-	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 toplineRead () {
-		return $this->topline_read;
-	}
-	public function toplineFavorite () {
-		return $this->topline_favorite;
-	}
-	public function toplineDate () {
-		return $this->topline_date;
-	}
-	public function toplineLink () {
-		return $this->topline_link;
-	}
-	public function bottomlineRead () {
-		return $this->bottomline_read;
-	}
-	public function bottomlineFavorite () {
-		return $this->bottomline_favorite;
-	}
-	public function bottomlineSharing () {
-		return $this->bottomline_sharing;
-	}
-	public function bottomlineTags () {
-		return $this->bottomline_tags;
-	}
-	public function bottomlineDate () {
-		return $this->bottomline_date;
-	}
-	public function bottomlineLink () {
-		return $this->bottomline_link;
-	}
-
-	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) {
-		if (filter_var ($value, FILTER_VALIDATE_URL)) {
-			$this->url_shaarli = $value;
-		} elseif (version_compare(PHP_VERSION, '5.3.3', '<') && (strpos($value, '-') > 0) && ($value === filter_var($value, FILTER_SANITIZE_URL))) {	//PHP bug #51192
-			$this->url_shaarli = $value;
-		} else {
-			$this->url_shaarli = '';
-		}
-	}
-	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';
-		}
-	}
-	public function _topline_read ($value) {
-		$this->topline_read = $value === 'yes';
-	}
-	public function _topline_favorite ($value) {
-		$this->topline_favorite = $value === 'yes';
-	}
-	public function _topline_date ($value) {
-		$this->topline_date = $value === 'yes';
-	}
-	public function _topline_link ($value) {
-		$this->topline_link = $value === 'yes';
-	}
-	public function _bottomline_read ($value) {
-		$this->bottomline_read = $value === 'yes';
-	}
-	public function _bottomline_favorite ($value) {
-		$this->bottomline_favorite = $value === 'yes';
-	}
-	public function _bottomline_sharing ($value) {
-		$this->bottomline_sharing = $value === 'yes';
-	}
-	public function _bottomline_tags ($value) {
-		$this->bottomline_tags = $value === 'yes';
-	}
-	public function _bottomline_date ($value) {
-		$this->bottomline_date = $value === 'yes';
-	}
-	public function _bottomline_link ($value) {
-		$this->bottomline_link = $value === 'yes';
-	}
-}
-
-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 $topline_read = 'yes';
-	public $topline_favorite = 'yes';
-	public $topline_date = 'yes';
-	public $topline_link = 'yes';
-	public $bottomline_read = 'yes';
-	public $bottomline_favorite = 'yes';
-	public $bottomline_sharing = 'yes';
-	public $bottomline_tags = 'yes';
-	public $bottomline_date = 'yes';
-	public $bottomline_link = 'yes';
-
-	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'];
-		}
-
-		if (isset ($this->array['topline_read'])) {
-			$this->topline_read = $this->array['topline_read'];
-		}
-		if (isset ($this->array['topline_favorite'])) {
-			$this->topline_favorite = $this->array['topline_favorite'];
-		}
-		if (isset ($this->array['topline_date'])) {
-			$this->topline_date = $this->array['topline_date'];
-		}
-		if (isset ($this->array['topline_link'])) {
-			$this->topline_link = $this->array['topline_link'];
-		}
-		if (isset ($this->array['bottomline_read'])) {
-			$this->bottomline_read = $this->array['bottomline_read'];
-		}
-		if (isset ($this->array['bottomline_favorite'])) {
-			$this->bottomline_favorite = $this->array['bottomline_favorite'];
-		}
-		if (isset ($this->array['bottomline_sharing'])) {
-			$this->bottomline_sharing = $this->array['bottomline_sharing'];
-		}
-		if (isset ($this->array['bottomline_tags'])) {
-			$this->bottomline_tags = $this->array['bottomline_tags'];
-		}
-		if (isset ($this->array['bottomline_date'])) {
-			$this->bottomline_date = $this->array['bottomline_date'];
-		}
-		if (isset ($this->array['bottomline_link'])) {
-			$this->bottomline_link = $this->array['bottomline_link'];
-		}
-	}
-
-	public function update ($values) {
-		foreach ($values as $key => $value) {
-			$this->array[$key] = $value;
-		}
-
-		$this->writeFile($this->array);
-	}
-}

+ 0 - 33
app/models/RSSPaginator.php

@@ -1,33 +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 next () {
-		return $this->next;
-	}
-
-	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;
-	}
-}

+ 59 - 0
app/sql.php

@@ -0,0 +1,59 @@
+<?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,
+	`color` char(7),
+	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
+	`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, :catName);
+');
+
+define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');

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

@@ -0,0 +1,58 @@
+<?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 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>

+ 11 - 11
app/views/configure/categorize.phtml

@@ -1,28 +1,28 @@
 <?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'); ?></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 (); ?>" />
 
 				<?php if ($cat->nbFeed () > 0) { ?>
-				<a class="confirm" href="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Translate::t ('ask_empty'); ?></a>
+				<a class="confirm" href="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Minz_Translate::t ('ask_empty'); ?></a>
 				<?php } ?>
-				(<?php echo Translate::t ('number_feeds', $cat->nbFeed ()); ?>)
+				(<?php echo Minz_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'); ?>
+				<?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 (); ?>" />
@@ -31,16 +31,16 @@
 		<?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>

+ 86 - 112
app/views/configure/display.phtml

@@ -1,99 +1,81 @@
 <?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 ('theme'); ?></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>
 
-		<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="form-group form-actions">
 			<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>
+				<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>
 
-		<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(null, 'html', true), $token); ?>
-			</div>
-		</div>
-	
-		<legend><?php echo Translate::t ('reading_configuration'); ?></legend>
+		<legend><?php echo Minz_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>
+			<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->postsPerPage (); ?>" />
+				<input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo $this->conf->posts_per_page; ?>" />
 			</div>
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="sort_order"><?php echo Translate::t ('sort_order'); ?></label>
+			<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="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>
+					<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 Translate::t ('default_view'); ?></label>
+			<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->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>
+					<option value="normal"<?php echo $this->conf->view_mode === 'normal' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('normal_view'); ?></option>
+					<option value="reader"<?php echo $this->conf->view_mode === 'reader' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('reader_view'); ?></option>
+					<option value="global"<?php echo $this->conf->view_mode === 'global' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('global_view'); ?></option>
 				</select>
 				<label class="radio" for="radio_all">
-					<input type="radio" name="default_view" id="radio_all" value="all"<?php echo $this->conf->defaultView () == 'all' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('show_all_articles'); ?>
+					<input type="radio" name="default_view" id="radio_all" value="all"<?php echo $this->conf->default_view === 'all' ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('show_all_articles'); ?>
 				</label>
 				<label class="radio" for="radio_not_read">
-					<input type="radio" name="default_view" id="radio_not_read" value="not_read"<?php echo $this->conf->defaultView () == 'not_read' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('show_not_reads'); ?>
+					<input type="radio" name="default_view" id="radio_not_read" value="not_read"<?php echo $this->conf->default_view === 'not_read' ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('show_not_reads'); ?>
 				</label>
 			</div>
 		</div>
@@ -101,9 +83,9 @@
 		<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>' : ''; ?>
+					<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>
@@ -111,9 +93,9 @@
 		<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>' : ''; ?>
+					<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>
@@ -121,100 +103,92 @@
 		<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>' : ''; ?>
+					<input type="checkbox" name="lazyload" id="lazyload" value="1"<?php echo $this->conf->lazyload ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('img_with_lazyload'); ?>
+					<noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
 				</label>
 			</div>
 		</div>
 
 		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('auto_read_when'); ?></label>
+			<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="yes"<?php echo $this->conf->markWhenArticle () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('article_selected'); ?>
+					<input type="checkbox" name="mark_open_article" id="check_open_article" value="1"<?php echo $this->conf->mark_when['article'] ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ('article_selected'); ?>
 				</label>
 				<label class="checkbox" for="check_open_site">
-					<input type="checkbox" name="mark_open_site" id="check_open_site" value="yes"<?php echo $this->conf->markWhenSite () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('article_open_on_website'); ?>
+					<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="yes"<?php echo $this->conf->markWhenScroll () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('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 Translate::t ('after_onread'); ?></label>
+			<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="yes"<?php echo $this->conf->onread_jump_next () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('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>
 
-		<legend><?php echo Translate::t ('reading_icons'); ?></legend>
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
+				<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
+			</div>
+		</div>
+
+		<legend><?php echo Minz_Translate::t ('reading_icons'); ?></legend>
 		<div class="form-group">
 			<table>
 				<thead>
 					<tr>
-						<th>&nbsp;</th>
-						<th><a class="read" title="<?php echo Translate::t ('mark_read'); ?>">&nbsp;</span></th>
-						<th><a class="bookmark" title="<?php echo Translate::t ('mark_favorite'); ?>">&nbsp;</span></th>
-						<th><?php echo Translate::t ('sharing'); ?></th>
-						<th><?php echo Translate::t ('related_tags'); ?></th>
-						<th><?php echo Translate::t ('publication_date'); ?></th>
-						<th class="item link"><a>&nbsp;</a></th>
+						<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 Translate::t ('top_line'); ?></th>
-						<td><input type="checkbox" name="topline_read" value="yes"<?php echo $this->conf->toplineRead () ? ' checked="checked"' : ''; ?> /></td>
-						<td><input type="checkbox" name="topline_favorite" value="yes"<?php echo $this->conf->toplineFavorite () ? ' checked="checked"' : ''; ?> /></td>
+						<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="yes"<?php echo $this->conf->toplineDate () ? ' checked="checked"' : ''; ?> /></td>
-						<td><input type="checkbox" name="topline_link" value="yes"<?php echo $this->conf->toplineLink () ? ' checked="checked"' : ''; ?> /></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 Translate::t ('bottom_line'); ?></th>
-						<td><input type="checkbox" name="bottomline_read" value="yes"<?php echo $this->conf->bottomlineRead () ? ' checked="checked"' : ''; ?> /></td>
-						<td><input type="checkbox" name="bottomline_favorite" value="yes"<?php echo $this->conf->bottomlineFavorite () ? ' checked="checked"' : ''; ?> /></td>
-						<td><input type="checkbox" name="bottomline_sharing" value="yes"<?php echo $this->conf->bottomlineSharing () ? ' checked="checked"' : ''; ?> /></td>
-						<td><input type="checkbox" name="bottomline_tags" value="yes"<?php echo $this->conf->bottomlineTags () ? ' checked="checked"' : ''; ?> /></td>
-						<td><input type="checkbox" name="bottomline_date" value="yes"<?php echo $this->conf->bottomlineDate () ? ' checked="checked"' : ''; ?> /></td>
-						<td><input type="checkbox" name="bottomline_link" value="yes"<?php echo $this->conf->bottomlineLink () ? ' checked="checked"' : ''; ?> /></td>
+						<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>
-		</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>
-		<div class="form-group">
-			<label class="group-name"></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'); ?>
-			</div>
+			</table><br />
 		</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>

+ 77 - 47
app/views/configure/feed.phtml

@@ -2,62 +2,46 @@
 
 <?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 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 } ?>
 
-	<form method="post" action="<?php echo _url ('configure', 'feed', 'id', $this->flux->id ()); ?>">
-		<legend><?php echo Translate::t ('informations'); ?></legend>
+	<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" for="name"><?php echo Translate::t ('title'); ?></label>
+			<label class="group-name" for="name"><?php echo Minz_Translate::t ('title'); ?></label>
 			<div class="group-controls">
-				<input type="text" name="name" id="name" value="<?php echo $this->flux->name () ; ?>" />
+				<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 ('website_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->website (); ?>
-					<a target="_blank" href="<?php echo $this->flux->website (); ?>"><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"><?php echo Translate::t ('feed_url'); ?></label>
+			<label class="group-name" for="website"><?php echo Minz_Translate::t ('website_url'); ?></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>
+				<input type="text" name="website" id="website" class="extend" value="<?php echo $this->flux->website (); ?>" />
+				<a target="_blank" href="<?php echo $this->flux->website (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
 			</div>
 		</div>
 		<div class="form-group">
-			<label class="group-name"></label>
+			<label class="group-name" for="url"><?php echo Minz_Translate::t ('feed_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>
+				<input type="text" name="url" id="url" class="extend" value="<?php echo $this->flux->url (); ?>" />
+				<a target="_blank" href="<?php echo $this->flux->url (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+				  <a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->flux->url (); ?>"><?php echo Minz_Translate::t ('feed_validator'); ?></a>
 			</div>
 		</div>
 		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('number_articles'); ?></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>
-		</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) { ?>
@@ -68,49 +52,95 @@
 				</select>
 			</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 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' => 'delete', 'params' => array ('id' => $this->flux->id ()))); ?>"><?php echo Minz_Translate::t ('delete'); ?></button>
+			</div>
+		</div>
+
+		<legend><?php echo Minz_Translate::t ('archiving_configuration'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name"></label>
+			<div class="group-controls">
+				<a class="btn" href="<?php echo _url ('feed', 'actualize', 'id', $this->flux->id ()); ?>">
+					<?php echo FreshRSS_Themes::icon('refresh'); ?> <?php echo Minz_Translate::t('actualize'); ?>
+				</a>
+			</div>
+		</div>
 		<div class="form-group">
-			<label class="group-name" for="path_entries"><?php echo Translate::t ('css_path_on_website'); ?></label>
+			<label class="group-name"><?php echo Minz_Translate::t ('number_articles'); ?></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'); ?>
+				<span class="control"><?php echo $this->flux->nbEntries (); ?></span>
+			</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 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']; ?>" autocomplete="off" />
-				<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" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="off" />
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
 			<div class="group-controls">
-				<input type="password" name="http_pass" id="http_pass" value="<?php echo $auth['password']; ?>" autocomplete="off" />
+				<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 } 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 } ?>

+ 14 - 11
app/views/configure/importExport.phtml

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

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

@@ -0,0 +1,64 @@
+<?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'); ?>">
+		<legend><?php echo Minz_Translate::t ('sharing'); ?></legend>
+		<div class="form-group">
+			<label class="group-name" for="shaarli">
+				<?php echo Minz_Translate::t ('your_shaarli'); ?>
+			</label>
+			<div class="group-controls">
+				<input type="url" id="shaarli" name="shaarli" class="extend" value="<?php echo $this->conf->sharing ('shaarli'); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" size="64" />
+
+				<?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="http://sebsauvage.net/wiki/doku.php?id=php:shaarli"><?php echo Minz_Translate::t ('more_information'); ?></a>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="wallabag">
+				<?php echo Minz_Translate::t ('your_wallabag'); ?>
+			</label>
+			<div class="group-controls">
+				<input type="url" id="wallabag" name="wallabag" class="extend" value="<?php echo $this->conf->sharing ('wallabag'); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" size="64" />
+
+				<?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="http://www.wallabag.org"><?php echo Minz_Translate::t ('more_information'); ?></a>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="diaspora">
+				<?php echo Minz_Translate::t ('your_diaspora_pod'); ?>
+			</label>
+			<div class="group-controls">
+				<input type="url" id="diaspora" name="diaspora" class="extend" value="<?php echo $this->conf->sharing ('diaspora'); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" size="64" />
+
+				<?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="https://diasporafoundation.org/"><?php echo Minz_Translate::t ('more_information'); ?></a>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name"><?php echo Minz_Translate::t ('activate_sharing'); ?></label>
+			<div class="group-controls">
+				<?php
+					$services = array ('twitter', 'g+', 'facebook', 'email', 'print');
+
+					foreach ($services as $service) {
+				?>
+				<label class="checkbox" for="<?php echo $service; ?>">
+					<input type="checkbox" name="<?php echo $service; ?>" id="<?php echo $service; ?>" value="1"<?php echo $this->conf->sharing($service) ? ' checked="checked"' : ''; ?> />
+					<?php echo Minz_Translate::t ($service); ?>
+				</label>
+				<?php } ?>
+			</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>

+ 35 - 14
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,76 @@
 		<?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_management'); ?></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>
 
 		<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="next_entry"><?php echo Minz_Translate::t ('next_article'); ?></label>
 			<div class="group-controls">
 				<input type="text" id="next_entry" name="shortcuts[next_entry]" list="keys" value="<?php echo $s['next_entry']; ?>" />
-				<?php echo Translate::t ('shift_for_last'); ?>
+				<?php echo Minz_Translate::t ('shift_for_last'); ?>
 			</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="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']; ?>" />
-				<?php echo Translate::t ('shift_for_first'); ?>
+				<?php echo Minz_Translate::t ('shift_for_first'); ?>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="collapse_entry"><?php echo Minz_Translate::t ('collapse_article'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="collapse_entry" name="shortcuts[collapse_entry]" list="keys" value="<?php echo $s['collapse_entry']; ?>" />
+			</div>
+		</div>
+
+		<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="auto_share_shortcut"><?php echo Minz_Translate::t ('auto_share'); ?></label>
+			<div class="group-controls">
+				<input type="text" id="auto_share_shortcut" name="shortcuts[auto_share]" list="keys" value="<?php echo $s['auto_share']; ?>" />
 			</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>

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

@@ -0,0 +1,162 @@
+<?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">
+				<input type="password" id="passwordPlain" name="passwordPlain" pattern=".{7,}" />
+				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="mail_login"><?php echo Minz_Translate::t('persona_connection_email'); ?></label>
+			<?php $mail = $this->conf->mail_login; ?>
+			<div class="group-controls">
+				<input type="email" id="mail_login" name="mail_login" class="extend" value="<?php echo $mail; ?>" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? '' : 'disabled="disabled"'; ?> placeholder="alice@example.net" />
+				<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"' : '', version_compare(PHP_VERSION, '5.3', '<') ? ' 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>
+
+		<?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 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" pattern="[0-9a-zA-Z]{1,16}" placeholder="demo" />
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="new_user_passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
+			<div class="group-controls">
+				<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" pattern=".{7,}" />
+				<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" 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 - 2
app/views/error/index.phtml

@@ -3,8 +3,8 @@
 		<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 Minz_Translate::t ('page_not_found'); ?><br />
+			<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
 		</p>
 	</div>
 </div>

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

@@ -1 +1 @@
-OK
+OK

+ 43 - 38
app/views/helpers/javascript_vars.phtml

@@ -1,39 +1,44 @@
 <?php
-	echo '"use strict";', "\n";
-	$mark = $this->conf->markWhen ();
-	echo 'var ',
-		'hide_posts=', ($this->conf->displayPosts () === 'yes' || Request::param ('output') === 'reader') ? 'false' : 'true',
-		',auto_mark_article=', $mark['article'] === 'yes' ? 'true' : 'false',
-		',auto_mark_site=', $mark['site'] === 'yes' ? 'true' : 'false',
-		',auto_mark_scroll=', $mark['scroll'] === 'yes' ? 'true' : 'false',
-		',auto_load_more=', $this->conf->autoLoadMore () === 'yes' ? 'true' : 'false',
-		',full_lazyload=', $this->conf->lazyload () === 'yes' && ($this->conf->displayPosts () === 'yes' || Request::param ('output') === 'reader') ? 'true' : 'false',
-		',does_lazyload=', $this->conf->lazyload() === 'yes' ? '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'], '"',
-		"},\n";
-
-	$mail = Session::param ('mail', 'null');
-	if ($mail != 'null') {
-		$mail = '"' . $mail . '"';
-	}
-	echo 'use_persona=', login_is_conf ($this->conf) ? 'true' : 'false',
-		',url_freshrss="', _url ('index', 'index'), '",',
-		'url_login="', _url ('index', 'login'), '",',
-		'url_logout="', _url ('index', 'logout'), '",',
-		'current_user_mail=', $mail, ",\n";
-
-	echo 'load_shortcuts=', Request::controllerName () === 'index' && Request::actionName () === 'index' ? 'true' : 'false', ",\n";
-
-	echo 'str_confirmation="', Translate::t('confirm_action'), '"', ",\n";
-
-	echo 'auto_actualize_feeds=', Session::param('actualize_feeds', false) ? 'true' : 'false', ";\n";
-	if (Session::param('actualize_feeds', false)) {
-		Session::_param('actualize_feeds');
-	}
+
+echo '"use strict";', "\n";
+
+$mark = $this->conf->mark_when;
+echo 'var ',
+	'hide_posts=', ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'false' : 'true',
+	',auto_mark_article=', $mark['article'] ? 'true' : 'false',
+	',auto_mark_site=', $mark['site'] ? 'true' : 'false',
+	',auto_mark_scroll=', $mark['scroll'] ? 'true' : 'false',
+	',auto_load_more=', $this->conf->auto_load_more ? 'true' : 'false',
+	',full_lazyload=', $this->conf->lazyload && ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'true' : 'false',
+	',does_lazyload=', $this->conf->lazyload ? 'true' : 'false';
+
+$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'], '",',
+	'collapse_entry:"', $s['collapse_entry'], '",',
+	'load_more:"', $s['load_more'], '",',
+	'auto_share:"', $s['auto_share'], '"',
+"},\n";
+
+if (Minz_Request::param ('output') === 'global') {
+	echo "iconClose='", FreshRSS_Themes::icon('close'), "',\n";
+}
+
+$authType = Minz_Configuration::authType();
+if ($authType === 'persona') {
+	echo 'current_user_mail="' . Minz_Session::param ('mail', '') . '",';
+}
+
+echo 'authType="', $authType, '",',
+	'url_freshrss="', _url ('index', 'index'), '",',
+	'url_login="', _url ('index', 'login'), '",',
+	'url_logout="', _url ('index', 'logout'), '",';
+
+echo 'str_confirmation="', Minz_Translate::t('confirm_action'), '"', ",\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>

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

@@ -1,20 +1,26 @@
 <?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);
 ?>
 
 <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; ?>
+	<a id="load_more" href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Minz_Translate::t ('load_more'); ?></a>
+	<?php } elseif ($markReadUrl) { ?>
+	<a id="bigMarkAsRead" href="<?php echo $markReadUrl; ?>">
+		<?php echo Minz_Translate::t ('nothing_to_load'); ?><br />
+		<span class="bigTick">✔</span><br />
+		<?php echo Minz_Translate::t ('mark_all_read'); ?>
+	</a>
 	<?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 Minz_Translate::t ('nothing_to_load'); ?><br />
+	</a>
 	<?php } ?>
 	</li>
 </ul>

+ 16 - 9
app/views/helpers/view/global_view.phtml

@@ -2,25 +2,32 @@
 
 <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 ();
 		if (!empty ($feeds)) {
 ?>
 	<div class="box-category">
 		<div class="category">
-			<a data-unread="<?php echo $cat->nbNotRead (); ?>" class="btn" href="<?php echo _url ('index', 'index', 'get', 'c_' . $cat->id (), 'output', 'normal'); ?>">
-			<?php echo htmlspecialchars($cat->name(), ENT_NOQUOTES, 'UTF-8'); ?>
+			<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 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 $feed->nbNotRead (); ?>" data-priority="<?php echo $feed->priority (); ?>" href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id (), 'output', 'normal'); ?>">
-				<?php echo htmlspecialchars($feed->name(), ENT_NOQUOTES, 'UTF-8'); ?>
+				<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>
 			</li>
 			<?php } ?>
@@ -33,6 +40,6 @@
 </div>
 
 <div id="overlay"></div>
-<div id="panel"<?php echo $this->conf->displayPosts () === 'no' ? ' class="hide_posts"' : ''; ?>>
-	<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>

+ 180 - 92
app/views/helpers/view/normal_view.phtml

@@ -3,53 +3,98 @@
 $this->partial ('aside_flux');
 $this->partial ('nav_menu');
 
-if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) {
-	$items = $this->entryPaginator->items ();
+if (!empty($this->entries)) {
+	$display_today = true;
+	$display_yesterday = true;
+	$display_others = true;
+	if ($this->loginOk) {
+		$shaarli = $this->conf->sharing ('shaarli');
+		$wallabag = $this->conf->sharing ('wallabag');
+		$diaspora = $this->conf->sharing ('diaspora');
+	} else {
+		$shaarli = '';
+		$wallabag = '';
+		$diaspora = '';
+	}
+	$twitter = $this->conf->sharing ('twitter');
+	$google_plus = $this->conf->sharing ('g+');
+	$facebook = $this->conf->sharing ('facebook');
+	$email = $this->conf->sharing ('email');
+	$print = $this->conf->sharing ('print');
+	$hidePosts = !$this->conf->display_posts;
+	$lazyload = $this->conf->lazyload;
+	$topline_read = $this->conf->topline_read;
+	$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 && (
+		$shaarli || $wallabag || $diaspora || $twitter ||
+		$google_plus || $facebook || $email || $print);
+	$bottomline_tags = $this->conf->bottomline_tags;
+	$bottomline_date = $this->conf->bottomline_date;
+	$bottomline_link = $this->conf->bottomline_link;
 ?>
 
-<div id="stream" class="normal<?php echo $this->conf->displayPosts () === 'no' ? ' hide_posts' : ''; ?>">
-	<?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" id="day_today">
-		<?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" id="day_yesterday">
-		<?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" id="day_before_yesterday">
-		<?php echo Translate::t ('before_yesterday'); ?>
-		<span class="name"><?php echo $this->currentName; ?></span>
-	</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 (!login_is_conf ($this->conf) || is_logged ()) { ?>
-				<?php if ($this->conf->toplineRead ()) { ?><li class="item manage"><a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', $item->isRead () ? 0 : 1); ?>">&nbsp;</a></li><?php } ?>
-				<?php if ($this->conf->toplineFavorite ()) { ?><li class="item manage"><a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', $item->isFavorite () ? 0 : 1); ?>">&nbsp;</a></li><?php } ?>
-			<?php
+<div id="stream" class="normal<?php echo $hidePosts ? ' hide_posts' : ''; ?>"><?php
+	?><div id="new-article">
+		<a href="<?php echo _url('index', 'index'); ?>"><?php echo Minz_Translate::t ('new_article'); ?></a>
+	</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 = 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 htmlspecialchars($feed->name(), ENT_NOQUOTES, 'UTF-8'); ?></span></a></li>
+			}
+			$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);
+			?><li class="item website"><a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>"><img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" /> <span><?php echo $feed->name(); ?></span></a></li>
 			<li class="item title"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo $item->title (); ?></a></li>
-			<?php if ($this->conf->toplineDate ()) { ?><li class="item date"><?php echo $item->date (); ?>&nbsp;</li><?php } ?>
-			<?php if ($this->conf->toplineLink ()) { ?><li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>">&nbsp;</a></li><?php } ?>
+			<?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">
@@ -57,96 +102,139 @@ if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) {
 				<h1 class="title"><?php echo $item->title (); ?></h1>
 				<?php
 					$author = $item->author ();
-					echo $author != '' ? '<div class="author">' . Translate::t ('by_author', $author) . '</div>' : '';
-					if($this->conf->lazyload() == 'yes') {
-						echo lazyimg($item->content ());
+					echo $author != '' ? '<div class="author">' . Minz_Translate::t ('by_author', $author) . '</div>' : '';
+					if ($lazyload) {
+						echo $hidePosts ? lazyIframe(lazyimg($item->content())) : lazyimg($item->content());
 					} else {
 						echo $item->content();
 					}
 				?>
 			</div>
-
-			<ul class="horizontal-list bottom">
-				<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
-					<?php if ($this->conf->bottomlineRead ()) { ?><li class="item manage"><a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', $item->isRead () ? 0 : 1); ?>">&nbsp;</a></li><?php } ?>
-					<?php if ($this->conf->bottomlineFavorite ()) { ?><li class="item manage"><a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', $item->isFavorite () ? 0 : 1); ?>">&nbsp;</a></li><?php } ?>
-				<?php } ?>
-				<li class="item">
-					<?php
-						if ($this->conf->bottomlineSharing ()) {
+			<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">
+							$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="dropdown-close"><a href="#close">❌</a></li>
+							<?php if ($shaarli) { ?>
 							<li class="item">
-								<a target="_blank" href="<?php echo $shaarli . '?post=' . $link . '&amp;title=' . $title . '&amp;source=bookmarklet'; ?>">
-									Shaarli
+								<a target="_blank" href="<?php echo $shaarli . '?post=' . $link . '&amp;title=' . $title . '&amp;source=FreshRSS'; ?>">
+									<?php echo Minz_Translate::t ('shaarli'); ?>
 								</a>
 							</li>
-							<?php } ?>
+							<?php } if ($wallabag) { ?>
 							<li class="item">
-								<a href="mailto:?subject=<?php echo urldecode($title); ?>&amp;body=<?php echo $link; ?>">
-									<?php echo Translate::t ('by_email'); ?>
+								<a target="_blank" href="<?php echo $wallabag . '?action=add&amp;url=' . base64_encode (urldecode($link)); ?>">
+									<?php echo Minz_Translate::t ('wallabag'); ?>
 								</a>
 							</li>
+							<?php } if ($diaspora) { ?>
+							<li class="item">
+								<a target="_blank" href="<?php echo $diaspora . '/bookmarklet?url=' . $link . '&amp;title=' . $title; ?>">
+									<?php echo Minz_Translate::t ('diaspora'); ?>
+								</a>
+							</li>
+							<?php } if ($twitter) { ?>
 							<li class="item">
 								<a target="_blank" href="https://twitter.com/share?url=<?php echo $link; ?>&amp;text=<?php echo $title; ?>">
-									Twitter
+									<?php echo Minz_Translate::t ('twitter'); ?>
+								</a>
+							</li>
+							<?php } if ($google_plus) { ?>
+							<li class="item">
+								<a target="_blank" href="https://plus.google.com/share?url=<?php echo $link; ?>">
+									<?php echo Minz_Translate::t ('g+'); ?>
 								</a>
 							</li>
+							<?php } if ($facebook) { ?>
 							<li class="item">
 								<a target="_blank" href="https://www.facebook.com/sharer.php?u=<?php echo $link; ?>&amp;t=<?php echo $title; ?>">
-									Facebook
+									<?php echo Minz_Translate::t ('facebook'); ?>
 								</a>
 							</li>
+							<?php } if ($email) { ?>
 							<li class="item">
-								<a target="_blank" href="https://plus.google.com/share?url=<?php echo $link; ?>">
-									Google+
+								<a href="mailto:?subject=<?php echo urldecode($title); ?>&amp;body=<?php echo $link; ?>">
+									<?php echo Minz_Translate::t ('by_email'); ?>
+								</a>
+							</li>
+							<?php } if ($print) { ?>
+							<li class="item">
+								<a href="#" class="print-article">
+									<?php echo Minz_Translate::t ('print'); ?>
 								</a>
 							</li>
+							<?php } ?>
 						</ul>
-					</div><?php } ?>
-				</li>
-				<?php
-					$tags = $this->conf->bottomlineTags () ? $item->tags() : null;
-					if (!empty($tags)) {
-				?>
-				<li class="item">
+					</div>
+					<?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 } ?>
-				<?php if ($this->conf->bottomlineDate ()) { ?><li class="item date"><?php echo $item->date (); ?>&nbsp;</li><?php } ?>
-				<?php if ($this->conf->bottomlineLink ()) { ?><li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>">&nbsp;</a></li><?php } ?>
+				</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>
+	<span class="alert-head"><?php echo Minz_Translate::t ('no_feed_to_display'); ?></span>
+	<?php echo Minz_Translate::t ('think_to_add'); ?>
 </div>
-<?php } ?>
+<?php } ?>

+ 11 - 10
app/views/helpers/view/reader_view.phtml

@@ -1,33 +1,33 @@
 <?php
 $this->partial ('nav_menu');
 
-if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) {
-	$items = $this->entryPaginator->items ();
+if (!empty($this->entries)) {
+	$lazyload = $this->conf->lazyload;
 ?>
 
 <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">
 				<?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 htmlspecialchars($feed->name(), ENT_NOQUOTES, 'UTF-8'); ?></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 $author != '' ? Minz_Translate::t ('by_author', $author) . ' — ' : ''; ?>
 					<?php echo $item->date (); ?>
 				</div>
 
 				<?php
-					if($this->conf->lazyload() == 'yes') {
+					if ($lazyload) {
 						echo lazyimg($item->content ());
 					} else {
 						echo $item->content();
@@ -38,11 +38,12 @@ if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) {
 	</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>
+	<span class="alert-head"><?php echo Minz_Translate::t ('no_feed_to_display'); ?></span>
+	<?php echo Minz_Translate::t ('think_to_add'); ?>
 </div>
-<?php } ?>
+<?php } ?>

+ 5 - 6
app/views/helpers/view/rss_view.phtml

@@ -2,17 +2,16 @@
 <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 $this->rss_title; ?></title>
-		<link><?php echo Url::display(null, 'html', true); ?></link>
-		<description><?php echo Translate::t ('rss_feeds_of', $this->rss_title); ?></description>
+		<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::display ($this->rss_url, 'html', true); ?>" rel="self" type="application/rss+xml" />
+		<atom:link href="<?php echo Minz_Url::display ($this->rss_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 != '') { ?>

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

+ 34 - 0
app/views/index/formLogin.phtml

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

+ 20 - 19
app/views/index/index.phtml

@@ -1,29 +1,30 @@
 <?php
 
-$output = Request::param ('output', 'normal');
-$token = $this->conf->token();
-$token_param = Request::param ('token', '');
-$token_is_ok = ($token != '' && $token == $token_param);
+$output = Minz_Request::param ('output', 'normal');
 
-if(!login_is_conf ($this->conf) ||
-   is_logged() ||
-   $this->conf->anonAccess() == 'yes' ||
-   ($output == 'rss' && $token_is_ok)) {
-	if($output == 'rss') {
+if ($this->loginOk || Minz_Configuration::allowAnonymous()) {
+	if ($output === 'normal') {
+		$this->renderHelper ('view/normal_view');
+	} elseif ($output === 'rss') {
 		$this->renderHelper ('view/rss_view');
-	} elseif($output == 'reader') {
+	} elseif ($output === 'reader') {
 		$this->renderHelper ('view/reader_view');
-	} elseif($output == 'global') {
+	} elseif ($output === 'global') {
 		$this->renderHelper ('view/global_view');
 	} else {
+		Minz_Request::_param ('output', 'normal');
+		$output = 'normal';
 		$this->renderHelper ('view/normal_view');
 	}
+} elseif ($output === 'rss') {
+	$token = $this->conf->token;
+	$token_param = Minz_Request::param ('token', '');
+	$token_is_ok = ($token != '' && $token == $token_param);
+	if ($token_is_ok) {
+		$this->renderHelper ('view/rss_view');
+	} else {
+		Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'), true);
+	}
 } else {
-?>
-<div class="post content">
-	<h1><?php echo Translate::t ('forbidden_access'); ?></h1>
-	<p><?php echo Translate::t ('forbidden_access_description'); ?></p>
-	<p><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Translate::t ('about_freshrss'); ?></a></p>
-</div>
-<?php
-}
+	Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'), true);
+}

+ 10 - 6
app/views/index/logs.phtml

@@ -1,21 +1,25 @@
 <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 ('logs'); ?></h1>
+	<h1><?php echo Minz_Translate::t ('logs'); ?></h1>
+	<form method="post" action="<?php echo _url ('index', 'logs'); ?>"><p>
+		<input type="hidden" name="clearLogs" />
+		<button type="submit" class="btn"><?php echo Minz_Translate::t ('clear_logs'); ?></button>
+	</p></form>
 
 	<?php $items = $this->logsPaginator->items (); ?>
 
 	<?php if (!empty ($items)) { ?>
 	<div class="logs">
 		<?php $this->logsPaginator->render ('logs_pagination.phtml', 'page'); ?>
-		
+
 		<?php foreach ($items as $log) { ?>
-		<div class="log <?php echo $log->level (); ?>"><span class="date"><?php echo date ('d/m/Y - H:i:s', strtotime ($log->date ())); ?></span><?php echo htmlspecialchars ($log->info (), ENT_NOQUOTES, 'UTF-8'); ?></div>
+		<div class="log <?php echo $log->level (); ?>"><span class="date"><?php echo @date ('Y-m-d H:i:s', @strtotime ($log->date ())); ?></span><?php echo htmlspecialchars ($log->info (), ENT_NOQUOTES, 'UTF-8'); ?></div>
 		<?php } ?>
-		
+
 		<?php $this->logsPaginator->render ('logs_pagination.phtml','page'); ?>
 	</div>
 	<?php } else { ?>
-	<p class="alert alert-warn"><?php echo Translate::t ('logs_empty'); ?></p>
+	<p class="alert alert-warn"><?php echo Minz_Translate::t ('logs_empty'); ?></p>
 	<?php } ?>
 </div>

+ 125 - 0
app/views/index/stats.phtml

@@ -0,0 +1,125 @@
+<div class="post content">
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
+	
+	<h1><?php echo Minz_Translate::t ('stats'); ?></h1>
+	
+	<div class="stat">
+		<h2><?php echo Minz_Translate::t ('stats_entry_repartition'); ?></h2>
+		<table>
+			<thead>
+				<tr>
+					<th> </th>
+					<th><?php echo Minz_Translate::t ('main_stream'); ?></th>
+					<th><?php echo Minz_Translate::t ('all_feeds'); ?></th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr>
+					<th><?php echo Minz_Translate::t ('status_total'); ?></th>
+					<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['total']); ?></td>
+					<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['total']); ?></td>
+				</tr>
+				<tr>
+					<th><?php echo Minz_Translate::t ('status_read'); ?></th>
+					<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['read']); ?></td>
+					<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['read']); ?></td>
+				</tr>
+				<tr>
+					<th><?php echo Minz_Translate::t ('status_unread'); ?></th>
+					<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['unread']); ?></td>
+					<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['unread']); ?></td>
+				</tr>
+				<tr>
+					<th><?php echo Minz_Translate::t ('status_favorites'); ?></th>
+					<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['favorite']); ?></td>
+					<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['favorite']); ?></td>
+				</tr>
+			</tbody>
+		</table>
+	</div>
+	
+	<div class="stat">
+		<h2><?php echo Minz_Translate::t ('stats_entry_per_day'); ?></h2>
+		<div id="statsEntryPerDay" style="height: 300px"></div>
+	</div>
+	
+	<div class="stat">
+		<h2><?php echo Minz_Translate::t ('stats_feed_per_category'); ?></h2>
+		<div id="statsFeedPerCategory" style="height: 300px"></div>
+		<div id="statsFeedPerCategoryLegend"></div>
+	</div>
+	
+	<div class="stat">
+		<h2><?php echo Minz_Translate::t ('stats_entry_per_category'); ?></h2>
+		<div id="statsEntryPerCategory" style="height: 300px"></div>
+		<div id="statsEntryPerCategoryLegend"></div>
+	</div>
+	
+	<div class="stat">
+		<h2><?php echo Minz_Translate::t ('stats_top_feed'); ?></h2>
+		<table>
+			<thead>
+				<tr>
+					<th><?php echo Minz_Translate::t ('feed'); ?></th>
+					<th><?php echo Minz_Translate::t ('category'); ?></th>
+					<th><?php echo Minz_Translate::t ('stats_entry_count'); ?></th>
+				</tr>
+			</thead>
+			<tbody>
+				<?php foreach ($this->topFeed as $feed): ?>
+					<tr>
+						<td><?php echo $feed['name']; ?></td>
+						<td><?php echo $feed['category']; ?></td>
+						<td class="numeric"><?php echo formatNumber($feed['count']); ?></td>
+					</tr>
+				<?php endforeach;?>
+			</tbody>
+		</table>
+	</div>
+</div>
+
+<script>
+"use strict";
+function initStats() {
+	if (!window.Flotr) {
+		if (window.console) {
+			console.log('FreshRSS waiting for Flotr…');
+		}
+		window.setTimeout(initStats, 50);
+		return;
+	}
+	// Entry per day
+	Flotr.draw(document.getElementById('statsEntryPerDay'),
+		[<?php echo $this->count ?>],
+		{
+			grid: {verticalLines: false},
+			bars: {horizontal: false, show: true},
+			xaxis: {noTicks: 6, showLabels: false, tickDecimals: 0},
+			yaxis: {min: 0},
+			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}}
+		});
+	// Feed per category
+	Flotr.draw(document.getElementById('statsFeedPerCategory'),
+		<?php echo $this->feedByCategory ?>,
+		{
+			grid: {verticalLines: false, horizontalLines: false},
+			pie: {explode: 10, show: true, labelFormatter: function(){return '';}},
+			xaxis: {showLabels: false},
+			yaxis: {showLabels: false},
+			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return obj.series.label + ' - '+ numberFormat(obj.y) + ' ('+ (obj.fraction * 100).toFixed(1) + '%)';}},
+			legend: {container: document.getElementById('statsFeedPerCategoryLegend'), noColumns: 3}
+		});
+	// Entry per category
+	Flotr.draw(document.getElementById('statsEntryPerCategory'),
+		<?php echo $this->entryByCategory ?>,
+		{
+			grid: {verticalLines: false, horizontalLines: false},
+			pie: {explode: 10, show: true, labelFormatter: function(){return '';}},
+			xaxis: {showLabels: false},
+			yaxis: {showLabels: false},
+			mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return obj.series.label + ' - '+ numberFormat(obj.y) + ' ('+ (obj.fraction * 100).toFixed(1) + '%)';}},
+			legend: {container: document.getElementById('statsEntryPerCategoryLegend'), noColumns: 3}
+		});
+}
+initStats();
+</script>

+ 19 - 15
app/views/javascript/actualize.phtml

@@ -1,37 +1,41 @@
-var feeds = new Array ();
+"use strict";
+var feeds = [];
 <?php foreach ($this->feeds as $feed) { ?>
-feeds.push ("<?php echo Url::display (array ('c' => 'feed', 'a' => 'actualize', 'params' => array ('id' => $feed->id (), 'ajax' => '1')), 'php'); ?>");
+feeds.push("<?php echo Minz_Url::display (array ('c' => 'feed', 'a' => 'actualize', 'params' => array ('id' => $feed->id (), 'ajax' => '1')), 'php'); ?>");
 <?php } ?>
 
-function initProgressBar (init) {
+function initProgressBar(init) {
 	if (init) {
-		$("body").after ("\<div id=\"actualizeProgress\" class=\"actualizeProgress\">\
-			<?php echo Translate::t ('refresh'); ?> <span class=\"progress\">0 / " + feeds.length + "</span><br />\
+		$("body").after("\<div id=\"actualizeProgress\" class=\"actualizeProgress\">\
+			<?php echo Minz_Translate::t ('refresh'); ?> <span class=\"progress\">0 / " + feeds.length + "</span><br />\
 			<progress id=\"actualizeProgressBar\" value=\"0\" max=\"" + feeds.length + "\"></progress>\
 		</div>");
 	} else {
-		window.location.reload ();
+		window.location.reload();
 	}
 }
-function updateProgressBar (i) {
+function updateProgressBar(i) {
 	$("#actualizeProgressBar").val(i);
-	$("#actualizeProgress .progress").html (i + " / " + feeds.length);
+	$("#actualizeProgress .progress").html(i + " / " + feeds.length);
 }
 
-function updateFeeds () {
-	initProgressBar (true);
+function updateFeeds() {
+	if (feeds.length === 0) {
+		return;
+	}
+	initProgressBar(true);
 
 	var i = 0;
 	for (var f in feeds) {
-		$.ajax ({
+		$.ajax({
 			type: 'POST',
 			url: feeds[f],
-		}).done (function (data) {
+		}).done(function (data) {
 			i++;
-			updateProgressBar (i);
+			updateProgressBar(i);
 
-			if (i == feeds.length) {
-				initProgressBar (false);
+			if (i === feeds.length) {
+				initProgressBar(false);
 			}
 		});
 	}

+ 8 - 0
app/views/javascript/nbUnreadsPerFeed.phtml

@@ -0,0 +1,8 @@
+<?php
+$result = array();
+foreach ($this->categories as $cat) {
+	foreach ($cat->feeds() as $feed) {
+		$result[$feed->id()] = $feed->nbNotRead();
+	}
+}
+echo json_encode($result);

+ 2 - 0
app/views/javascript/nonce.phtml

@@ -0,0 +1,2 @@
+<?php
+echo json_encode(array('salt1' => $this->salt1, 'nonce' => $this->nonce));

+ 0 - 1
cache/.gitignore

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

+ 17 - 0
constants.php

@@ -0,0 +1,17 @@
+<?php
+define('FRESHRSS_VERSION', '0.7');
+define('FRESHRSS_WEBSITE', 'http://freshrss.org');
+
+// Constantes de chemins
+define('FRESHRSS_PATH', dirname(__FILE__));
+
+	define('PUBLIC_PATH', FRESHRSS_PATH . '/p');
+		define('INDEX_PATH', PUBLIC_PATH . '/i');
+		define('PUBLIC_RELATIVE', '..');
+
+	define('DATA_PATH', FRESHRSS_PATH . '/data');
+		define('LOG_PATH', DATA_PATH . '/log');
+		define('CACHE_PATH', DATA_PATH . '/cache');
+
+	define('LIB_PATH', FRESHRSS_PATH . '/lib');
+		define('APP_PATH', FRESHRSS_PATH . '/app');

+ 8 - 0
data/.gitignore

@@ -0,0 +1,8 @@
+application.ini
+config.php
+*_user.php
+*.sqlite
+touch.txt
+no-cache.txt
+*.bak.php
+*.lock.txt

+ 3 - 0
data/.htaccess

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

+ 1 - 0
data/cache/.gitignore

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

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

+ 2 - 0
data/favicons/.gitignore

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

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

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

+ 1 - 0
data/log/.gitignore

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

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

+ 1 - 0
data/persona/.gitignore

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

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

+ 13 - 0
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=p/" />
+<title>Redirection</title>
+<meta name="robots" content="noindex,nofollow" />
+</head>
+
+<body>
+<p><a href="p/">FreshRSS</a></p>
+</body>
+</html>

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor